feat: complete website implementation
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Wind, Wrench, ThermometerSun, Paintbrush, Droplet, Tv, HelpCircle, ArrowRight,
|
||||
PaintBucket, Refrigerator, Hammer, Key, Bath, PaintRoller, Grid, Layers,
|
||||
AlertTriangle, Waves, Droplets, WashingMachine, Snowflake, Store, ScanLine, Settings
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface IconCardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
iconName: string;
|
||||
href?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
Wind,
|
||||
Wrench,
|
||||
ThermometerSun,
|
||||
Paintbrush,
|
||||
Droplet,
|
||||
Tv,
|
||||
PaintBucket,
|
||||
Refrigerator,
|
||||
Hammer,
|
||||
Key,
|
||||
Bath,
|
||||
PaintRoller,
|
||||
Grid,
|
||||
Layers,
|
||||
AlertTriangle,
|
||||
Waves,
|
||||
Droplets,
|
||||
WashingMachine,
|
||||
Snowflake,
|
||||
Store,
|
||||
ScanLine,
|
||||
Settings,
|
||||
};
|
||||
|
||||
export function IconCard({ title, description, iconName, href, className }: IconCardProps) {
|
||||
const Icon = iconMap[iconName] || HelpCircle;
|
||||
|
||||
const cardContent = (
|
||||
<>
|
||||
<div className="p-3 rounded-lg bg-slate-100 text-primary w-fit group-hover:bg-accent-light group-hover:text-accent transition-colors duration-300">
|
||||
<Icon className="size-6" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-primary font-heading mt-4 group-hover:text-accent transition-colors">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-sm text-text-muted mt-2 leading-relaxed flex-grow">
|
||||
{description}
|
||||
</p>
|
||||
{href && (
|
||||
<div className="flex items-center gap-1 text-xs font-semibold text-primary group-hover:text-accent mt-4 transition-colors">
|
||||
<span>Dowiedz się więcej</span>
|
||||
<ArrowRight className="size-3.5 group-hover:translate-x-1 transition-transform" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const cardClasses = cn(
|
||||
"group border border-border rounded-xl p-6 bg-white hover:shadow-md transition-all duration-300 flex flex-col justify-between h-full",
|
||||
className
|
||||
);
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<Link href={href} className={cardClasses}>
|
||||
{cardContent}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={cardClasses}>{cardContent}</div>;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Image as ImageIcon } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface PlaceholderProps {
|
||||
text: string;
|
||||
className?: string;
|
||||
aspect?: "video" | "square" | "portrait" | "auto" | "banner";
|
||||
}
|
||||
|
||||
export function Placeholder({ text, className, aspect = "video" }: PlaceholderProps) {
|
||||
const aspectClasses = {
|
||||
video: "aspect-video",
|
||||
square: "aspect-square",
|
||||
portrait: "aspect-[3/4]",
|
||||
banner: "aspect-[21/9] min-h-[300px]",
|
||||
auto: "h-full w-full",
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-full bg-slate-100 dark:bg-slate-800 border-2 border-dashed border-slate-200 dark:border-slate-700 rounded-xl flex flex-col items-center justify-center text-center p-6 text-slate-400 dark:text-slate-500 overflow-hidden group hover:bg-slate-150 transition-colors duration-250",
|
||||
aspectClasses[aspect],
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* TODO: docelowe zdjęcie — {text} */}
|
||||
<ImageIcon className="size-10 mb-3 group-hover:scale-110 transition-transform duration-300" />
|
||||
<span className="text-sm font-medium px-4 leading-normal">{text}</span>
|
||||
<span className="text-[10px] text-slate-400 mt-2 uppercase tracking-widest font-semibold font-heading opacity-70">
|
||||
Placeholder Zdjęcia
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SectionHeadingProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
eyebrow?: string;
|
||||
align?: "left" | "center";
|
||||
dark?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SectionHeading({
|
||||
title,
|
||||
subtitle,
|
||||
eyebrow,
|
||||
align = "center",
|
||||
dark = false,
|
||||
className,
|
||||
}: SectionHeadingProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-2 mb-10 md:mb-14",
|
||||
align === "center" ? "items-center text-center" : "items-start text-left",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{eyebrow && (
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs md:text-sm font-semibold uppercase tracking-wider font-heading",
|
||||
dark ? "text-accent" : "text-cta"
|
||||
)}
|
||||
>
|
||||
{eyebrow}
|
||||
</span>
|
||||
)}
|
||||
<h2
|
||||
className={cn(
|
||||
"text-3xl md:text-4xl font-bold leading-tight font-heading max-w-3xl",
|
||||
dark ? "text-white" : "text-primary"
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
{subtitle && (
|
||||
<p
|
||||
className={cn(
|
||||
"text-base md:text-lg max-w-2xl leading-relaxed mt-1",
|
||||
dark ? "text-slate-300" : "text-text-muted"
|
||||
)}
|
||||
>
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { Loader2, CheckCircle2, AlertCircle } from 'lucide-react';
|
||||
import { submitContactForm } from '@/app/kontakt/actions';
|
||||
|
||||
const contactFormSchema = z.object({
|
||||
name: z.string().min(2, "Imię musi mieć min. 2 znaki"),
|
||||
phone: z.string().min(9, "Wprowadź poprawny numer telefonu"),
|
||||
email: z.string().email("Niepoprawny adres e-mail"),
|
||||
subject: z.string().min(1, "Wybierz temat"),
|
||||
message: z.string().min(10, "Wiadomość musi mieć min. 10 znaków"),
|
||||
consent: z.boolean().refine((val) => val === true, "Zgoda RODO jest wymagana"),
|
||||
});
|
||||
|
||||
type ContactFormValues = z.infer<typeof contactFormSchema>;
|
||||
|
||||
export function ContactForm() {
|
||||
const [serverState, setServerState] = useState<{ success?: boolean; message?: string } | null>(null);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
reset
|
||||
} = useForm<ContactFormValues>({
|
||||
resolver: zodResolver(contactFormSchema),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
subject: '',
|
||||
message: '',
|
||||
consent: false,
|
||||
}
|
||||
});
|
||||
|
||||
const onSubmit = async (data: ContactFormValues) => {
|
||||
setServerState(null);
|
||||
|
||||
// Przekształcamy dane do formatu FormData dla Server Action
|
||||
const formData = new FormData();
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
formData.append(key, value.toString());
|
||||
});
|
||||
|
||||
const response = await submitContactForm(null, formData);
|
||||
|
||||
setServerState({
|
||||
success: response.success,
|
||||
message: response.message
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
if (serverState?.success) {
|
||||
return (
|
||||
<div className="bg-green-50 text-green-800 p-8 rounded-2xl border border-green-200 flex flex-col items-center text-center">
|
||||
<CheckCircle2 className="w-16 h-16 text-green-500 mb-4" />
|
||||
<h3 className="text-2xl font-bold font-heading mb-2">Wiadomość wysłana!</h3>
|
||||
<p className="text-green-700">{serverState.message}</p>
|
||||
<button
|
||||
onClick={() => setServerState(null)}
|
||||
className="mt-6 px-6 py-2 bg-green-600 text-white rounded-lg font-semibold hover:bg-green-700 transition-colors"
|
||||
>
|
||||
Wyślij kolejną wiadomość
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 bg-white p-6 md:p-8 rounded-2xl border border-border shadow-sm">
|
||||
{serverState && !serverState.success && (
|
||||
<div className="bg-red-50 text-red-800 p-4 rounded-xl border border-red-200 flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 shrink-0 mt-0.5" />
|
||||
<p className="text-sm font-medium">{serverState.message}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
{/* Imię */}
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="name" className="text-sm font-semibold text-primary">Imię i nazwisko</label>
|
||||
<input
|
||||
{...register("name")}
|
||||
id="name"
|
||||
className={`w-full px-4 py-3 rounded-xl border ${errors.name ? 'border-red-500 focus:ring-red-500' : 'border-border focus:border-primary focus:ring-primary/20'} outline-none focus:ring-2 transition-all bg-slate-50`}
|
||||
placeholder="Jan Kowalski"
|
||||
aria-invalid={errors.name ? "true" : "false"}
|
||||
/>
|
||||
{errors.name && <p className="text-red-500 text-xs font-medium">{errors.name.message}</p>}
|
||||
</div>
|
||||
|
||||
{/* Telefon */}
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="phone" className="text-sm font-semibold text-primary">Telefon</label>
|
||||
<input
|
||||
{...register("phone")}
|
||||
id="phone"
|
||||
type="tel"
|
||||
className={`w-full px-4 py-3 rounded-xl border ${errors.phone ? 'border-red-500 focus:ring-red-500' : 'border-border focus:border-primary focus:ring-primary/20'} outline-none focus:ring-2 transition-all bg-slate-50`}
|
||||
placeholder="+48 123 456 789"
|
||||
/>
|
||||
{errors.phone && <p className="text-red-500 text-xs font-medium">{errors.phone.message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
{/* Email */}
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="email" className="text-sm font-semibold text-primary">Adres e-mail</label>
|
||||
<input
|
||||
{...register("email")}
|
||||
id="email"
|
||||
type="email"
|
||||
className={`w-full px-4 py-3 rounded-xl border ${errors.email ? 'border-red-500 focus:ring-red-500' : 'border-border focus:border-primary focus:ring-primary/20'} outline-none focus:ring-2 transition-all bg-slate-50`}
|
||||
placeholder="[email protected]"
|
||||
/>
|
||||
{errors.email && <p className="text-red-500 text-xs font-medium">{errors.email.message}</p>}
|
||||
</div>
|
||||
|
||||
{/* Temat */}
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="subject" className="text-sm font-semibold text-primary">Temat rozmowy</label>
|
||||
<select
|
||||
{...register("subject")}
|
||||
id="subject"
|
||||
className={`w-full px-4 py-3 rounded-xl border ${errors.subject ? 'border-red-500 focus:ring-red-500' : 'border-border focus:border-primary focus:ring-primary/20'} outline-none focus:ring-2 transition-all bg-slate-50 appearance-none`}
|
||||
>
|
||||
<option value="">Wybierz temat...</option>
|
||||
<option value="klimatyzacja">Klimatyzacja</option>
|
||||
<option value="pompy-ciepla">Pompy ciepła</option>
|
||||
<option value="remonty">Remonty i wykończenia</option>
|
||||
<option value="hydraulika">Hydraulika</option>
|
||||
<option value="serwis-agd">Serwis AGD</option>
|
||||
<option value="inne">Inne zapytanie</option>
|
||||
</select>
|
||||
{errors.subject && <p className="text-red-500 text-xs font-medium">{errors.subject.message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Wiadomość */}
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="message" className="text-sm font-semibold text-primary">Wiadomość</label>
|
||||
<textarea
|
||||
{...register("message")}
|
||||
id="message"
|
||||
rows={5}
|
||||
className={`w-full px-4 py-3 rounded-xl border ${errors.message ? 'border-red-500 focus:ring-red-500' : 'border-border focus:border-primary focus:ring-primary/20'} outline-none focus:ring-2 transition-all bg-slate-50 resize-y`}
|
||||
placeholder="Opisz krótko, w czym możemy Ci pomóc..."
|
||||
/>
|
||||
{errors.message && <p className="text-red-500 text-xs font-medium">{errors.message.message}</p>}
|
||||
</div>
|
||||
|
||||
{/* RODO */}
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-start gap-3 cursor-pointer group">
|
||||
<div className="relative flex items-start pt-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
{...register("consent")}
|
||||
className="w-5 h-5 rounded border-border text-primary focus:ring-primary focus:ring-offset-0 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-text-muted leading-relaxed select-none">
|
||||
Wyrażam zgodę na przetwarzanie moich danych osobowych podanych w formularzu w celu obsługi zapytania przez firmę AeroService. Zapoznałem/am się z Polityką Prywatności.
|
||||
</span>
|
||||
</label>
|
||||
{errors.consent && <p className="text-red-500 text-xs font-medium">{errors.consent.message}</p>}
|
||||
</div>
|
||||
|
||||
{/* Button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full bg-cta hover:bg-cta-hover text-white font-bold py-4 rounded-xl transition-colors flex items-center justify-center gap-2 disabled:opacity-70 disabled:cursor-not-allowed shadow-md"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
<span>Wysyłanie...</span>
|
||||
</>
|
||||
) : (
|
||||
<span>Wyślij zapytanie</span>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import Link from "next/link";
|
||||
import { Phone, Mail, MapPin, Clock, Facebook, Instagram, ShieldCheck } from "lucide-react";
|
||||
import { companyDetails } from "@/content/company";
|
||||
|
||||
export function Footer() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="bg-primary text-primary-foreground">
|
||||
{/* Main Footer Content */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12">
|
||||
{/* Column 1: Company details */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<Link href="/" className="inline-block">
|
||||
<span className="text-2xl font-bold font-heading text-white">
|
||||
{companyDetails.shortName}
|
||||
<span className="text-accent">.</span>
|
||||
</span>
|
||||
</Link>
|
||||
<p className="text-sm text-slate-300 max-w-xs leading-relaxed">
|
||||
Kompleksowe usługi HVAC, klimatyzacji, pomp ciepła oraz prace wykończeniowe i serwisowe dla Twojego domu.
|
||||
</p>
|
||||
<div className="flex flex-col gap-3 mt-2 text-sm text-slate-300">
|
||||
<a
|
||||
href={`tel:${companyDetails.phoneRaw}`}
|
||||
className="flex items-center gap-2.5 hover:text-accent transition-colors"
|
||||
>
|
||||
<Phone className="size-4 shrink-0 text-accent" />
|
||||
<span>{companyDetails.phone}</span>
|
||||
</a>
|
||||
<a
|
||||
href={`mailto:${companyDetails.email}`}
|
||||
className="flex items-center gap-2.5 hover:text-accent transition-colors"
|
||||
>
|
||||
<Mail className="size-4 shrink-0 text-accent" />
|
||||
<span>{companyDetails.email}</span>
|
||||
</a>
|
||||
<div className="flex items-start gap-2.5">
|
||||
<MapPin className="size-4 shrink-0 mt-0.5 text-accent" />
|
||||
<span>
|
||||
{companyDetails.address.street},<br />
|
||||
{companyDetails.address.zipCode} {companyDetails.address.city}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2.5">
|
||||
<Clock className="size-4 shrink-0 mt-0.5 text-accent" />
|
||||
<span>{companyDetails.hours}</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-400 mt-1">
|
||||
NIP: {companyDetails.nip}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Column 2: Services */}
|
||||
<div>
|
||||
<h3 className="text-base font-semibold font-heading text-white mb-6 uppercase tracking-wider">
|
||||
Usługi
|
||||
</h3>
|
||||
<ul className="flex flex-col gap-3 text-sm text-slate-300">
|
||||
<li>
|
||||
<Link href="/klimatyzacja" className="hover:text-accent transition-colors">
|
||||
Klimatyzacja i Montaż
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/pompy-ciepla" className="hover:text-accent transition-colors">
|
||||
Pompy Ciepła
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/uslugi-domowe/remonty" className="hover:text-accent transition-colors">
|
||||
Remonty i Wykończenia
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/uslugi-domowe/hydraulika" className="hover:text-accent transition-colors">
|
||||
Usługi Hydrauliczne
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/uslugi-domowe/serwis-agd" className="hover:text-accent transition-colors">
|
||||
Serwis AGD i Lodówek
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/sklep" className="hover:text-accent transition-colors">
|
||||
Sklep online
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Column 3: Information */}
|
||||
<div>
|
||||
<h3 className="text-base font-semibold font-heading text-white mb-6 uppercase tracking-wider">
|
||||
Informacje
|
||||
</h3>
|
||||
<ul className="flex flex-col gap-3 text-sm text-slate-300">
|
||||
<li>
|
||||
<Link href="/o-nas" className="hover:text-accent transition-colors">
|
||||
O nas
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/realizacje" className="hover:text-accent transition-colors">
|
||||
Realizacje (Portfolio)
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/blog" className="hover:text-accent transition-colors">
|
||||
Blog i Poradniki
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/regulamin" className="hover:text-accent transition-colors">
|
||||
Regulamin
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/polityka-prywatnosci" className="hover:text-accent transition-colors">
|
||||
Polityka prywatności
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/reklamacje" className="hover:text-accent transition-colors">
|
||||
Reklamacje
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/kontakt" className="hover:text-accent transition-colors">
|
||||
Kontakt
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Column 4: Account */}
|
||||
<div>
|
||||
<h3 className="text-base font-semibold font-heading text-white mb-6 uppercase tracking-wider">
|
||||
Twoje Konto
|
||||
</h3>
|
||||
<ul className="flex flex-col gap-3 text-sm text-slate-300">
|
||||
<li>
|
||||
<Link href="/koszyk" className="hover:text-accent transition-colors">
|
||||
Twój Koszyk
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/sklep" className="hover:text-accent transition-colors">
|
||||
Zamów urządzenia
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/kontakt" className="hover:text-accent transition-colors">
|
||||
Zgłoś awarię / serwis
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Bar */}
|
||||
<div className="border-t border-slate-800 bg-slate-950/40 py-6">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col sm:flex-row justify-between items-center gap-4">
|
||||
<p className="text-xs text-slate-400 text-center sm:text-left">
|
||||
© {currentYear} {companyDetails.name}. Wszelkie prawa zastrzeżone. Projekt strony i wdrożenie AeroService.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
{/* Social Icons */}
|
||||
<div className="flex gap-4">
|
||||
<a
|
||||
href={companyDetails.socials.facebook}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-slate-400 hover:text-white transition-colors"
|
||||
aria-label="Odwiedź nasz profil na Facebooku"
|
||||
>
|
||||
<Facebook className="size-4" />
|
||||
</a>
|
||||
<a
|
||||
href={companyDetails.socials.instagram}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-slate-400 hover:text-white transition-colors"
|
||||
aria-label="Odwiedź nasz profil na Instagramie"
|
||||
>
|
||||
<Instagram className="size-4" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Trust Symbol */}
|
||||
<div className="flex items-center gap-1.5 text-xs text-slate-400 border-l border-slate-800 pl-6">
|
||||
<ShieldCheck className="size-4 text-accent" />
|
||||
<span>Bezpieczne płatności SSL</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Search } from "lucide-react";
|
||||
import { getProducts } from "@/content/products";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { formatCurrency } from "@/lib/utils";
|
||||
|
||||
export function GlobalSearch() {
|
||||
const [query, setQuery] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const products = getProducts();
|
||||
const results = query.length > 1
|
||||
? products.filter(p => p.name.toLowerCase().includes(query.toLowerCase()) || p.brand.toLowerCase().includes(query.toLowerCase()) || p.category.toLowerCase().includes(query.toLowerCase()))
|
||||
: [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="p-2 text-primary hover:text-accent hover:bg-background rounded-lg transition-colors outline-none cursor-pointer"
|
||||
aria-label="Szukaj na stronie"
|
||||
>
|
||||
<Search className="size-5" />
|
||||
</button>
|
||||
|
||||
<dialog className={`modal ${isOpen ? "modal-open" : ""}`}>
|
||||
<div className="modal-box p-0 max-w-2xl bg-white rounded-2xl overflow-hidden mt-[-20vh]">
|
||||
<div className="flex items-center border-b border-border px-4 py-3 bg-slate-50">
|
||||
<Search className="size-5 text-accent mr-3 shrink-0" />
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder="Wpisz nazwę klimatyzatora (np. Rotenso, Pular)..."
|
||||
className="flex-1 outline-none text-base placeholder:text-text-muted/50 bg-transparent py-2 text-primary font-medium"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
/>
|
||||
<button onClick={() => setIsOpen(false)} className="text-xs font-semibold text-text-muted hover:text-primary transition-colors bg-white px-2 py-1 rounded border border-border shadow-sm">
|
||||
ESC
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{query.length > 1 ? (
|
||||
<div className="max-h-[60vh] overflow-y-auto p-2 bg-white">
|
||||
{results.length > 0 ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
{results.map((product) => (
|
||||
<Link
|
||||
key={product.slug}
|
||||
href={`/sklep/${product.slug}`}
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex items-center gap-4 p-3 hover:bg-slate-50 rounded-lg transition-colors group border border-transparent hover:border-border"
|
||||
>
|
||||
<div className="w-16 h-16 bg-white border border-border rounded-md overflow-hidden relative shrink-0">
|
||||
<Image
|
||||
src={product.image || 'https://images.unsplash.com/photo-1621905251189-08b45d6a269e?w=800&q=80'}
|
||||
alt={product.name}
|
||||
fill
|
||||
className="object-contain p-1 mix-blend-multiply"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col">
|
||||
<span className="text-[10px] font-bold text-text-muted uppercase tracking-wider">{product.brand}</span>
|
||||
<span className="text-sm font-semibold text-primary group-hover:text-accent transition-colors">{product.name}</span>
|
||||
<span className="text-xs text-text-muted mt-0.5">{product.power} kW • {product.category}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-sm font-bold text-primary block">{formatCurrency(product.price)}</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 text-center text-text-muted">
|
||||
Brak wyników dla <span className="font-semibold">"{query}"</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 text-center text-text-muted/50 text-sm bg-white">
|
||||
Wpisz co najmniej 2 znaki, aby rozpocząć wyszukiwanie w sklepie
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<form method="dialog" className="modal-backdrop">
|
||||
<button onClick={() => setIsOpen(false)}>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { User, ShoppingBag, Menu, Phone } from "lucide-react";
|
||||
import { NavDropdown } from "./NavDropdown";
|
||||
import { GlobalSearch } from "./GlobalSearch";
|
||||
import { MobileMenu } from "./MobileMenu";
|
||||
import { useCartStore } from "@/lib/cart-store";
|
||||
import { companyDetails } from "@/content/company";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Header() {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const cartItems = useCartStore((state) => state.items);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
const handleScroll = () => {
|
||||
if (window.scrollY > 40) {
|
||||
setIsScrolled(true);
|
||||
} else {
|
||||
setIsScrolled(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
const cartCount = mounted
|
||||
? cartItems.reduce((total, item) => total + item.quantity, 0)
|
||||
: 0;
|
||||
|
||||
const acSections = [
|
||||
{
|
||||
title: "Klimatyzatory",
|
||||
items: [
|
||||
{
|
||||
label: "Ścienne split",
|
||||
href: "/sklep?category=scienne",
|
||||
description: "Klasyczne jednostki ścienne do mieszkań",
|
||||
},
|
||||
{
|
||||
label: "Multisplit",
|
||||
href: "/sklep?category=multisplit",
|
||||
description: "Systemy wielopokojowe (2-5 pomieszczeń)",
|
||||
},
|
||||
{
|
||||
label: "Przenośne",
|
||||
href: "/sklep?category=przenosne",
|
||||
description: "Mobilne chłodzenie bez stałej instalacji",
|
||||
},
|
||||
{
|
||||
label: "Kasetonowe i konsolowe",
|
||||
href: "/sklep?category=kasetonowe",
|
||||
description: "Modele biurowe i przypodłogowe",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Usługi HVAC",
|
||||
items: [
|
||||
{
|
||||
label: "Montaż klimatyzacji",
|
||||
href: "/klimatyzacja",
|
||||
description: "Kompleksowy dobór, audyt i montaż",
|
||||
},
|
||||
{
|
||||
label: "Serwis i czyszczenie",
|
||||
href: "/klimatyzacja",
|
||||
description: "Odgrzybianie, naprawy i przeglądy",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const homeSections = [
|
||||
{
|
||||
title: "Prace wykończeniowe",
|
||||
items: [
|
||||
{
|
||||
label: "Remonty i wykończenia",
|
||||
href: "/uslugi-domowe/remonty",
|
||||
description: "Kompleksowe remonty mieszkań i łazienek",
|
||||
},
|
||||
{
|
||||
label: "Drobne naprawy",
|
||||
href: "/uslugi-domowe",
|
||||
description: "Pomoc „złotej rączki” w drobnych usterkach",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Instalacje i AGD",
|
||||
items: [
|
||||
{
|
||||
label: "Hydraulika",
|
||||
href: "/uslugi-domowe/hydraulika",
|
||||
description: "Montaż armatury, naprawy i instalacje wod-kan",
|
||||
},
|
||||
{
|
||||
label: "Serwis AGD / Lodówki",
|
||||
href: "/uslugi-domowe/serwis-agd",
|
||||
description: "Szybka naprawa lodówek i urządzeń chłodniczych",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ----------------- DESKTOP: TOP BAR ----------------- */}
|
||||
<div className="hidden lg:block bg-muted py-2 border-b border-border/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-between">
|
||||
{/* Top Bar Left: Logo */}
|
||||
<Link href="/" className="shrink-0 flex items-center gap-3">
|
||||
<span className="text-2xl font-bold font-heading text-primary">
|
||||
{companyDetails.shortName}
|
||||
<span className="text-accent">.</span>
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground font-medium border-l border-border pl-3">
|
||||
Klimatyzacja i usługi domowe
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Top Bar Right: Phone & Icons */}
|
||||
<div className="flex items-center gap-6">
|
||||
<a
|
||||
href={`tel:${companyDetails.phoneRaw}`}
|
||||
className="flex items-center gap-2 text-sm font-semibold text-primary hover:text-accent transition-colors whitespace-nowrap"
|
||||
>
|
||||
<Phone className="size-4 text-accent" />
|
||||
<span>{companyDetails.phone}</span>
|
||||
</a>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<GlobalSearch />
|
||||
|
||||
<Link
|
||||
href="/konto"
|
||||
className="p-2 text-primary hover:text-accent hover:bg-background rounded-lg transition-colors outline-none"
|
||||
aria-label="Konto użytkownika"
|
||||
>
|
||||
<User className="size-5" />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/koszyk"
|
||||
className="p-2 text-primary hover:text-accent hover:bg-background rounded-lg transition-colors relative outline-none"
|
||||
aria-label={`Koszyk z ${cartCount} przedmiotami`}
|
||||
>
|
||||
<ShoppingBag className="size-5" />
|
||||
{cartCount > 0 && (
|
||||
<span className="absolute top-0 right-0 bg-cta text-white text-[10px] font-bold size-4 rounded-full flex items-center justify-center translate-x-0.5 -translate-y-0.5">
|
||||
{cartCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ----------------- DESKTOP: NAV BAR (STICKY) ----------------- */}
|
||||
<header
|
||||
className={cn(
|
||||
"hidden lg:block sticky top-0 z-40 bg-background transition-all duration-300 border-b border-border",
|
||||
isScrolled ? "shadow-md py-2" : "py-4"
|
||||
)}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-between">
|
||||
{/* Main Navigation */}
|
||||
<nav className="flex items-center gap-8">
|
||||
{isScrolled && (
|
||||
<Link href="/" className="shrink-0 flex items-center mr-2">
|
||||
<span className="text-xl font-bold font-heading text-primary">
|
||||
{companyDetails.shortName}
|
||||
<span className="text-accent">.</span>
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<NavDropdown
|
||||
label="Klimatyzacja"
|
||||
triggerHref="/klimatyzacja"
|
||||
sections={acSections}
|
||||
align="left"
|
||||
/>
|
||||
<Link
|
||||
href="/pompy-ciepla"
|
||||
className="text-sm font-medium text-primary hover:text-accent transition-colors"
|
||||
>
|
||||
Pompy ciepła
|
||||
</Link>
|
||||
<NavDropdown
|
||||
label="Usługi domowe"
|
||||
triggerHref="/uslugi-domowe"
|
||||
sections={homeSections}
|
||||
/>
|
||||
<Link
|
||||
href="/realizacje"
|
||||
className="text-sm font-medium text-primary hover:text-accent transition-colors"
|
||||
>
|
||||
Realizacje
|
||||
</Link>
|
||||
<Link
|
||||
href="/blog"
|
||||
className="text-sm font-medium text-primary hover:text-accent transition-colors"
|
||||
>
|
||||
Blog
|
||||
</Link>
|
||||
<Link
|
||||
href="/o-nas"
|
||||
className="text-sm font-medium text-primary hover:text-accent transition-colors"
|
||||
>
|
||||
O nas
|
||||
</Link>
|
||||
<Link
|
||||
href="/kontakt"
|
||||
className="text-sm font-medium text-primary hover:text-accent transition-colors"
|
||||
>
|
||||
Kontakt
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
{/* CTA Button */}
|
||||
<Link href="/kontakt" className="hidden lg:flex">
|
||||
<button className="btn btn-primary font-bold">
|
||||
Darmowa wycena
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* ----------------- MOBILE: HEADER (STICKY) ----------------- */}
|
||||
<header
|
||||
className={cn(
|
||||
"lg:hidden sticky top-0 z-40 bg-background border-b border-border transition-all duration-300",
|
||||
isScrolled ? "shadow-md py-3" : "py-4"
|
||||
)}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 flex items-center justify-between">
|
||||
{/* Mobile Logo */}
|
||||
<Link href="/" className="shrink-0 flex items-center">
|
||||
<span className="text-xl font-bold font-heading text-primary">
|
||||
{companyDetails.shortName}
|
||||
<span className="text-accent">.</span>
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Mobile Icons (Cart + Hamburger) */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
href="/koszyk"
|
||||
className="p-2 text-primary hover:text-accent transition-colors relative outline-none"
|
||||
aria-label={`Koszyk z ${cartCount} przedmiotami`}
|
||||
>
|
||||
<ShoppingBag className="size-6" />
|
||||
{cartCount > 0 && (
|
||||
<span className="absolute top-0 right-0 bg-cta text-white text-[10px] font-bold size-4 rounded-full flex items-center justify-center translate-x-0.5 -translate-y-0.5">
|
||||
{cartCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => setIsMobileMenuOpen(true)}
|
||||
className="p-2 text-primary hover:text-accent hover:bg-muted transition-colors rounded-lg outline-none"
|
||||
aria-label="Otwórz menu"
|
||||
>
|
||||
<Menu className="size-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* ----------------- MOBILE MENU DRAWER ----------------- */}
|
||||
<MobileMenu
|
||||
isOpen={isMobileMenuOpen}
|
||||
onClose={() => setIsMobileMenuOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { X, Phone, ChevronRight } from "lucide-react";
|
||||
import { companyDetails } from "@/content/company";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface MobileMenuProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function MobileMenu({ isOpen, onClose }: MobileMenuProps) {
|
||||
// Prevent body scrolling when menu is open
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
return () => {
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const hvacSections = [
|
||||
{ label: "Ofertowana klimatyzacja", href: "/klimatyzacja" },
|
||||
{ label: "Klimatyzatory ścienne", href: "/sklep?category=scienne" },
|
||||
{ label: "Klimatyzatory multisplit", href: "/sklep?category=multisplit" },
|
||||
{ label: "Klimatyzatory przenośne", href: "/sklep?category=przenosne" },
|
||||
{ label: "Klimatyzatory kasetonowe", href: "/sklep?category=kasetonowe" },
|
||||
{ label: "Klimatyzator konsolowe", href: "/sklep?category=konsolowe" },
|
||||
{ label: "Montaż klimatyzacji", href: "/klimatyzacja" },
|
||||
{ label: "Serwis i przeglądy", href: "/klimatyzacja" },
|
||||
];
|
||||
|
||||
const homeServices = [
|
||||
{ label: "Wszystkie usługi domowe", href: "/uslugi-domowe" },
|
||||
{ label: "Remonty i wykończenia", href: "/uslugi-domowe/remonty" },
|
||||
{ label: "Hydraulika i instalacje", href: "/uslugi-domowe/hydraulika" },
|
||||
{ label: "Serwis AGD / Lodówki", href: "/uslugi-domowe/serwis-agd" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 lg:hidden transition-all duration-300",
|
||||
isOpen ? "pointer-events-auto" : "pointer-events-none"
|
||||
)}
|
||||
>
|
||||
{/* Dark Overlay */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 bg-black/60 transition-opacity duration-300",
|
||||
isOpen ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Drawer Panel */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-0 right-0 bottom-0 w-full max-w-sm bg-white shadow-xl flex flex-col transition-transform duration-300 ease-in-out",
|
||||
isOpen ? "translate-x-0" : "translate-x-full"
|
||||
)}
|
||||
>
|
||||
{/* Drawer Header */}
|
||||
<div className="flex justify-between items-center px-6 py-4 border-b border-border">
|
||||
<span className="text-xl font-bold font-heading text-primary">
|
||||
Menu<span className="text-accent">.</span>
|
||||
</span>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 rounded-lg hover:bg-muted text-primary transition-colors focus:ring-2 focus:ring-accent outline-none"
|
||||
aria-label="Zamknij menu"
|
||||
>
|
||||
<X className="size-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Drawer Content */}
|
||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||
<div className="w-full">
|
||||
{/* 1. Klimatyzacja */}
|
||||
<details className="collapse collapse-arrow border-b border-border rounded-none">
|
||||
<summary className="collapse-title text-sm font-medium text-primary hover:text-accent py-3 px-0 min-h-0">
|
||||
Klimatyzacja
|
||||
</summary>
|
||||
<div className="collapse-content px-0 pb-2">
|
||||
<ul className="flex flex-col gap-2.5 pl-4 pb-2">
|
||||
{hvacSections.map((item, idx) => (
|
||||
<li key={idx}>
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={onClose}
|
||||
className="text-sm text-text-muted hover:text-accent transition-colors flex items-center justify-between py-1"
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
<ChevronRight className="size-3.5 opacity-60" />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{/* 2. Pompy Ciepła */}
|
||||
<div className="border-b border-border py-3">
|
||||
<Link
|
||||
href="/pompy-ciepla"
|
||||
onClick={onClose}
|
||||
className="text-sm font-medium text-primary hover:text-accent block"
|
||||
>
|
||||
Pompy ciepła
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* 3. Usługi Domowe */}
|
||||
<details className="collapse collapse-arrow border-b border-border rounded-none">
|
||||
<summary className="collapse-title text-sm font-medium text-primary hover:text-accent py-3 px-0 min-h-0">
|
||||
Usługi domowe
|
||||
</summary>
|
||||
<div className="collapse-content px-0 pb-2">
|
||||
<ul className="flex flex-col gap-2.5 pl-4 pb-2">
|
||||
{homeServices.map((item, idx) => (
|
||||
<li key={idx}>
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={onClose}
|
||||
className="text-sm text-text-muted hover:text-accent transition-colors flex items-center justify-between py-1"
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
<ChevronRight className="size-3.5 opacity-60" />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{/* 4. Realizacje */}
|
||||
<div className="border-b border-border py-3">
|
||||
<Link
|
||||
href="/realizacje"
|
||||
onClick={onClose}
|
||||
className="text-sm font-medium text-primary hover:text-accent block"
|
||||
>
|
||||
Realizacje
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* 5. Blog */}
|
||||
<div className="border-b border-border py-3">
|
||||
<Link
|
||||
href="/blog"
|
||||
onClick={onClose}
|
||||
className="text-sm font-medium text-primary hover:text-accent block"
|
||||
>
|
||||
Blog
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* 6. O Nas */}
|
||||
<div className="border-b border-border py-3">
|
||||
<Link
|
||||
href="/o-nas"
|
||||
onClick={onClose}
|
||||
className="text-sm font-medium text-primary hover:text-accent block"
|
||||
>
|
||||
O nas
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* 7. Kontakt */}
|
||||
<div className="border-b border-border py-3">
|
||||
<Link
|
||||
href="/kontakt"
|
||||
onClick={onClose}
|
||||
className="text-sm font-medium text-primary hover:text-accent block"
|
||||
>
|
||||
Kontakt
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Drawer Footer */}
|
||||
<div className="border-t border-border p-6 bg-slate-50 flex flex-col gap-4">
|
||||
<a
|
||||
href={`tel:${companyDetails.phoneRaw}`}
|
||||
className="flex items-center justify-center gap-2 p-3 bg-white border border-border rounded-xl text-primary font-semibold hover:text-accent transition-colors shadow-sm"
|
||||
>
|
||||
<Phone className="size-4 text-accent" />
|
||||
<span>Zadzwoń: {companyDetails.phone}</span>
|
||||
</a>
|
||||
<Link href="/kontakt" onClick={onClose}>
|
||||
<button className="btn btn-primary w-full font-bold">
|
||||
Darmowa konsultacja
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface DropdownItem {
|
||||
label: string;
|
||||
href: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface DropdownSection {
|
||||
title?: string;
|
||||
items: DropdownItem[];
|
||||
}
|
||||
|
||||
interface NavDropdownProps {
|
||||
label: string;
|
||||
triggerHref?: string;
|
||||
sections: DropdownSection[];
|
||||
align?: "left" | "center" | "right";
|
||||
}
|
||||
|
||||
export function NavDropdown({ label, triggerHref, sections, align = "center" }: NavDropdownProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative group"
|
||||
onMouseEnter={() => setIsOpen(true)}
|
||||
onMouseLeave={() => setIsOpen(false)}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<div className="flex items-center gap-1 py-2 text-sm font-medium text-primary hover:text-accent cursor-pointer transition-colors focus-within:text-accent">
|
||||
{triggerHref ? (
|
||||
<Link href={triggerHref} className="outline-none">
|
||||
{label}
|
||||
</Link>
|
||||
) : (
|
||||
<span tabIndex={0} className="outline-none select-none">
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
<ChevronDown className="size-4 transition-transform duration-200 group-hover:rotate-180" />
|
||||
</div>
|
||||
|
||||
{/* Dropdown Panel */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-full pt-2 z-50 w-[480px] transition-all duration-200 origin-top",
|
||||
align === "left" && "left-0",
|
||||
align === "center" && "left-1/2 -translate-x-1/2",
|
||||
align === "right" && "right-0",
|
||||
isOpen
|
||||
? "opacity-100 translate-y-0 scale-100 pointer-events-auto"
|
||||
: "opacity-0 -translate-y-2 scale-95 pointer-events-none"
|
||||
)}
|
||||
>
|
||||
<div className="bg-white rounded-xl shadow-lg border border-border p-6 grid grid-cols-2 gap-8">
|
||||
{sections.map((section, sIdx) => (
|
||||
<div key={sIdx} className="flex flex-col gap-3">
|
||||
{section.title && (
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-text-muted mb-1 font-heading">
|
||||
{section.title}
|
||||
</h4>
|
||||
)}
|
||||
<ul className="flex flex-col gap-2">
|
||||
{section.items.map((item, iIdx) => (
|
||||
<li key={iIdx}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className="group/item flex flex-col gap-0.5 p-2 -mx-2 rounded-lg hover:bg-muted transition-colors"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
<span className="text-sm font-medium text-primary group-hover/item:text-accent transition-colors">
|
||||
{item.label}
|
||||
</span>
|
||||
{item.description && (
|
||||
<span className="text-xs text-text-muted leading-snug">
|
||||
{item.description}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Clock, ShieldCheck, Shield, Percent, Star } from "lucide-react";
|
||||
|
||||
export function TrustBar() {
|
||||
const items = [
|
||||
{
|
||||
icon: Clock,
|
||||
title: "Szybki termin realizacji",
|
||||
description: "Ekspresowy montaż i serwis",
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
title: "15+ lat doświadczenia",
|
||||
description: "Tysiące zrealizowanych projektów",
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
title: "Pełna gwarancja",
|
||||
description: "Bezpieczeństwo i pewność usług",
|
||||
},
|
||||
{
|
||||
icon: Star,
|
||||
title: "Opinie Google 5.0",
|
||||
description: "Zaufanie setek klientów",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="bg-muted border-t border-b border-border py-8">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 text-center lg:text-left">
|
||||
{items.map((item, idx) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex flex-col lg:flex-row items-center lg:items-start gap-3 hover:translate-y-[-2px] transition-transform duration-200"
|
||||
>
|
||||
<div className="p-2 rounded-lg bg-accent-light text-accent shrink-0">
|
||||
<Icon className="size-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-primary font-heading">
|
||||
{item.title}
|
||||
</h4>
|
||||
<p className="text-xs text-text-muted mt-0.5">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import { SectionHeading } from '../common/SectionHeading';
|
||||
import { CheckCircle2 } from 'lucide-react';
|
||||
import { companyDetails } from '@/content/company';
|
||||
|
||||
export function AboutStory() {
|
||||
return (
|
||||
<section className="py-16 md:py-24 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
|
||||
{/* Lewa: Obraz */}
|
||||
<div className="relative">
|
||||
<div className="relative aspect-[4/5] rounded-3xl overflow-hidden shadow-2xl">
|
||||
<Image
|
||||
src="https://images.unsplash.com/photo-1621905251189-08b45d6a269e?w=800&q=80"
|
||||
alt="Zespół firmy w trakcie pracy"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
{/* Ozdobny element zaufania */}
|
||||
<div className="absolute -bottom-8 -right-8 bg-white p-6 rounded-2xl shadow-xl hidden md:block">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-shrink-0 w-16 h-16 bg-accent/10 rounded-full flex items-center justify-center">
|
||||
<span className="text-2xl font-black text-accent font-heading">
|
||||
{companyDetails.stats.find(s => s.label.includes("doświadczenia"))?.value || "15+"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-primary font-heading text-lg leading-none mb-1">Lat na rynku</p>
|
||||
<p className="text-sm text-text-muted">Doświadczenie i zaufanie</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Prawa: Tekst */}
|
||||
<div>
|
||||
<SectionHeading
|
||||
eyebrow="Nasza Historia"
|
||||
title="Budujemy komfort od fundamentów aż po dach"
|
||||
align="left"
|
||||
/>
|
||||
|
||||
<div className="prose prose-lg text-text-muted mt-8 mb-10">
|
||||
<p>
|
||||
Firma {companyDetails.name} powstała z pasji do nowoczesnych technologii oraz chęci dostarczania kompleksowych usług dla domów i firm. Zaczynaliśmy jako niewielka, dwuosobowa ekipa specjalizująca się w prostych remontach. Dziś jesteśmy liderem w regionie, realizując setki zaawansowanych projektów instalacyjnych rocznie.
|
||||
</p>
|
||||
<p>
|
||||
Naszym celem jest upraszczanie życia naszym klientom. Zamiast szukać oddzielnych fachowców od hydrauliki, klimatyzacji i wykończeń, u nas otrzymujesz całościowe wsparcie z gwarancją jakości.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-4">
|
||||
{[
|
||||
"Certyfikowani instalatorzy z uprawnieniami F-Gaz",
|
||||
"Dbałość o czystość podczas prac montażowych",
|
||||
"Gwarancja i szybki serwis na terenie miasta i okolic",
|
||||
"Pomoc w uzyskiwaniu dofinansowań"
|
||||
].map((point, idx) => (
|
||||
<li key={idx} className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-accent shrink-0" />
|
||||
<span className="text-primary font-medium">{point}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Wind, LayoutGrid, Sparkles, ArrowRight } from 'lucide-react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const acTypes = [
|
||||
{
|
||||
title: 'Klimatyzatory SPLIT',
|
||||
description: 'Najpopularniejsze rozwiązanie do jednego pomieszczenia. Składa się z jednej jednostki wewnętrznej i jednej zewnętrznej.',
|
||||
icon: Wind,
|
||||
subtypes: ['Ścienne', 'Konsolowe', 'Kasetonowe', 'Kanałowe'],
|
||||
link: '/sklep/klimatyzatory-split',
|
||||
},
|
||||
{
|
||||
title: 'Systemy MULTISPLIT',
|
||||
description: 'Idealne do chłodzenia całego domu. Do jednej jednostki zewnętrznej można podłączyć od 2 do 5 jednostek wewnętrznych.',
|
||||
icon: LayoutGrid,
|
||||
subtypes: ['Dla 2 pomieszczeń', 'Dla 3 pomieszczeń', 'Dla 4-5 pomieszczeń', 'Mieszane typy jednostek'],
|
||||
link: '/sklep/klimatyzatory-multisplit',
|
||||
},
|
||||
{
|
||||
title: 'Pozostałe rozwiązania',
|
||||
description: 'Specjalistyczne urządzenia łączące funkcje chłodzenia z zaawansowanym grzaniem lub oczyszczaniem powietrza.',
|
||||
icon: Sparkles,
|
||||
subtypes: ['Z funkcją ogrzewania (do -25°C)', 'Z zaawansowanym oczyszczaczem', 'Klimatyzatory przenośne', 'Klimatyzatory bez jednostki zewn.'],
|
||||
link: '/sklep/inne',
|
||||
},
|
||||
];
|
||||
|
||||
export const AcTypesSection = () => {
|
||||
return (
|
||||
<section className="py-16 bg-gray-50">
|
||||
<div className="container mx-auto px-4 md:px-8 max-w-7xl">
|
||||
<div className="text-center max-w-2xl mx-auto mb-12">
|
||||
<h2 className="text-3xl font-bold text-primary mb-4">Rodzaje klimatyzatorów</h2>
|
||||
<p className="text-gray-600">
|
||||
Dobierzemy rozwiązanie idealnie dopasowane do Twoich potrzeb, niezależnie od tego czy chłodzisz jedno pomieszczenie, czy cały dom.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{acTypes.map((type, idx) => {
|
||||
const Icon = type.icon;
|
||||
return (
|
||||
<div key={idx} className="bg-white rounded-xl shadow-sm border border-gray-100 p-8 flex flex-col h-full hover:shadow-md transition-shadow">
|
||||
<div className="w-14 h-14 bg-accent/10 rounded-xl flex items-center justify-center mb-6 text-accent">
|
||||
<Icon className="w-7 h-7" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-primary mb-3">{type.title}</h3>
|
||||
<p className="text-gray-600 mb-6 text-sm flex-grow">
|
||||
{type.description}
|
||||
</p>
|
||||
|
||||
<ul className="space-y-2 mb-8">
|
||||
{type.subtypes.map((subtype, sIdx) => (
|
||||
<li key={sIdx} className="flex items-center text-sm text-gray-700">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-accent mr-3"></div>
|
||||
{subtype}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Link
|
||||
href={type.link}
|
||||
className={cn("btn btn-outline btn-primary bg-white w-full justify-between group mt-auto")}
|
||||
>
|
||||
Sprawdź modele
|
||||
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { CheckCircle2, Zap, ShieldCheck, Banknote } from "lucide-react";
|
||||
|
||||
export function AdvantagesBar() {
|
||||
const advantages = [
|
||||
{ icon: Zap, text: "Szybki termin realizacji" },
|
||||
{ icon: ShieldCheck, text: "Gwarancja na sprzęt i montaż" },
|
||||
{ icon: CheckCircle2, text: "Uprawnienia F-gaz" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-primary text-primary-foreground py-6 border-y border-border/10">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-6">
|
||||
{advantages.map((adv, idx) => (
|
||||
<div key={idx} className="flex items-center justify-center md:justify-start gap-3">
|
||||
<adv.icon className="size-5 text-accent shrink-0" />
|
||||
<span className="text-sm font-medium leading-tight">
|
||||
{adv.text}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useRef, useEffect, MouseEvent, TouchEvent } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { projectsData } from '@/content/projects';
|
||||
import { GripVertical } from 'lucide-react';
|
||||
|
||||
export const BeforeAfterSlider = () => {
|
||||
const [sliderPosition, setSliderPosition] = useState(50);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Pobierz pierwszy projekt z kategorii remonty, który ma beforeAfter
|
||||
const project = projectsData.find(p => p.category === 'remonty' && p.beforeAfter);
|
||||
|
||||
if (!project || !project.beforeAfter) return null;
|
||||
|
||||
const handleMove = (clientX: number) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const x = Math.max(0, Math.min(clientX - rect.left, rect.width));
|
||||
const percentage = (x / rect.width) * 100;
|
||||
setSliderPosition(percentage);
|
||||
};
|
||||
|
||||
const onMouseMove = (e: globalThis.MouseEvent) => {
|
||||
if (!isDragging) return;
|
||||
handleMove(e.clientX);
|
||||
};
|
||||
|
||||
const onTouchMove = (e: globalThis.TouchEvent) => {
|
||||
if (!isDragging) return;
|
||||
handleMove(e.touches[0].clientX);
|
||||
};
|
||||
|
||||
const stopDragging = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
window.addEventListener('mousemove', onMouseMove);
|
||||
window.addEventListener('mouseup', stopDragging);
|
||||
window.addEventListener('touchmove', onTouchMove);
|
||||
window.addEventListener('touchend', stopDragging);
|
||||
} else {
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('mouseup', stopDragging);
|
||||
window.removeEventListener('touchmove', onTouchMove);
|
||||
window.removeEventListener('touchend', stopDragging);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('mouseup', stopDragging);
|
||||
window.removeEventListener('touchmove', onTouchMove);
|
||||
window.removeEventListener('touchend', stopDragging);
|
||||
};
|
||||
}, [isDragging]);
|
||||
|
||||
return (
|
||||
<section className="py-16 bg-white">
|
||||
<div className="container mx-auto px-4 md:px-8 max-w-5xl">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-bold text-primary mb-4">Przed i Po</h2>
|
||||
<p className="text-gray-600">Zobacz metamorfozę na jednym z naszych ostatnich projektów.</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative w-full aspect-[4/3] md:aspect-[16/9] rounded-2xl overflow-hidden select-none cursor-ew-resize bg-gray-100"
|
||||
onMouseDown={() => setIsDragging(true)}
|
||||
onTouchStart={() => setIsDragging(true)}
|
||||
>
|
||||
{/* Obrazek PO (na spodzie) */}
|
||||
<div className="absolute inset-0">
|
||||
<Image
|
||||
src={project.beforeAfter.afterUrl}
|
||||
alt="Wygląd po remoncie"
|
||||
fill
|
||||
className="object-cover pointer-events-none"
|
||||
/>
|
||||
<div className="absolute top-4 right-4 bg-white/80 backdrop-blur text-primary font-semibold px-4 py-1.5 rounded-full text-sm">
|
||||
Po
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Obrazek PRZED (na wierzchu, obcięty) */}
|
||||
<div
|
||||
className="absolute inset-0 overflow-hidden"
|
||||
style={{ width: `${sliderPosition}%` }}
|
||||
>
|
||||
<div className="absolute inset-0 w-[100vw] min-w-full">
|
||||
{/* Używamy native img dla prostszego manipulowania wymiarami wewnątrz overflow */}
|
||||
<img
|
||||
src={project.beforeAfter.beforeUrl}
|
||||
alt="Wygląd przed remontem"
|
||||
className="absolute inset-0 w-full h-full object-cover pointer-events-none"
|
||||
style={{ width: containerRef.current?.getBoundingClientRect().width || '100%' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute top-4 left-4 bg-white/80 backdrop-blur text-gray-700 font-semibold px-4 py-1.5 rounded-full text-sm">
|
||||
Przed
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Suwak (linia i uchwyt) */}
|
||||
<div
|
||||
className="absolute top-0 bottom-0 w-1 bg-white cursor-ew-resize flex items-center justify-center shadow-[0_0_10px_rgba(0,0,0,0.3)]"
|
||||
style={{ left: `calc(${sliderPosition}% - 2px)` }}
|
||||
>
|
||||
<div className="w-10 h-10 bg-white rounded-full flex items-center justify-center shadow-lg border border-gray-100 text-primary hover:scale-110 transition-transform">
|
||||
<GripVertical className="w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
import Link from "next/link";
|
||||
import { ArrowRight, Calendar, User } from "lucide-react";
|
||||
import { SectionHeading } from "../common/SectionHeading";
|
||||
import { Placeholder } from "../common/Placeholder";
|
||||
import { getLatestPosts } from "@/content/posts";
|
||||
|
||||
export function BlogPreview() {
|
||||
const latestPosts = getLatestPosts(3);
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleDateString("pl-PL", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const getCategoryLabel = (cat: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
klimatyzacja: "Klimatyzacja",
|
||||
"pompy-ciepla": "Pompy ciepła",
|
||||
remonty: "Remonty",
|
||||
hydraulika: "Hydraulika",
|
||||
porady: "Porady ogólne",
|
||||
};
|
||||
return labels[cat] || cat;
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="py-16 md:py-24 bg-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
eyebrow="Aktualności"
|
||||
title="Najnowsze poradniki i artykuły"
|
||||
subtitle="Dzielimy się wiedzą i doświadczeniem. Zobacz nasze wpisy dotyczące HVAC, remontów i oszczędzania energii."
|
||||
align="center"
|
||||
/>
|
||||
|
||||
{/* Blog Posts Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{latestPosts.map((post) => (
|
||||
<article
|
||||
key={post.slug}
|
||||
className="group flex flex-col justify-between border border-border rounded-xl overflow-hidden hover:shadow-md transition-shadow h-full"
|
||||
>
|
||||
<div>
|
||||
{/* Thumbnail */}
|
||||
<div className="bg-slate-50 border-b border-border">
|
||||
<Placeholder
|
||||
text={`Miniatura artykułu: ${post.title}`}
|
||||
aspect="video"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
{/* Category & Date */}
|
||||
<div className="flex items-center gap-4 text-xs text-text-muted mb-3">
|
||||
<span className="font-semibold uppercase tracking-wider text-accent">
|
||||
{getCategoryLabel(post.category)}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="size-3.5" />
|
||||
<span>{formatDate(post.date)}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="text-lg font-bold text-primary font-heading line-clamp-2 leading-snug group-hover:text-accent transition-colors">
|
||||
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
|
||||
</h3>
|
||||
|
||||
{/* Excerpt */}
|
||||
<p className="text-sm text-text-muted mt-3 line-clamp-2 leading-relaxed">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Read More Link */}
|
||||
<div className="p-6 pt-0 mt-auto">
|
||||
<Link
|
||||
href={`/blog/${post.slug}`}
|
||||
className="inline-flex items-center gap-1.5 text-sm font-semibold text-primary group-hover:text-accent transition-colors"
|
||||
>
|
||||
<span>Czytaj więcej</span>
|
||||
<ArrowRight className="size-4 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Blog Catalog Button */}
|
||||
<div className="flex justify-center mt-12">
|
||||
<Link href="/blog">
|
||||
<button className="btn btn-outline btn-primary bg-white font-bold flex items-center gap-2 group">
|
||||
<span>Przejdź do bloga</span>
|
||||
<ArrowRight className="size-4 group-hover:translate-x-1 transition-transform" />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
const brands = [
|
||||
{ name: 'Daikin', logo: '/brands/daikin.svg' }, // Placeholders, we will just show text if image fails or use a generic box
|
||||
{ name: 'Mitsubishi', logo: '/brands/mitsubishi.svg' },
|
||||
{ name: 'Gree', logo: '/brands/gree.svg' },
|
||||
{ name: 'Rotenso', logo: '/brands/rotenso.svg' },
|
||||
{ name: 'LG', logo: '/brands/lg.svg' },
|
||||
{ name: 'Samsung', logo: '/brands/samsung.svg' },
|
||||
{ name: 'Panasonic', logo: '/brands/panasonic.svg' },
|
||||
{ name: 'Aux', logo: '/brands/aux.svg' },
|
||||
];
|
||||
|
||||
export const BrandsGrid = () => {
|
||||
return (
|
||||
<section className="py-16 bg-white border-t border-gray-100">
|
||||
<div className="container mx-auto px-4 md:px-8 max-w-7xl text-center">
|
||||
<h3 className="text-xl font-semibold text-gray-500 mb-8 uppercase tracking-wider text-sm">
|
||||
Autoryzowany partner czołowych producentów
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-6 items-center opacity-60 grayscale hover:grayscale-0 transition-all duration-500">
|
||||
{brands.map((brand, idx) => (
|
||||
<div key={idx} className="flex justify-center items-center h-16 bg-gray-50 rounded-lg p-3 hover:bg-gray-100 transition-colors">
|
||||
{/* Fallback to text for placeholder since we don't have images */}
|
||||
<span className="font-bold text-gray-600 text-lg tracking-tight">{brand.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import { SectionHeading } from '../common/SectionHeading';
|
||||
import { companyDetails } from '@/content/company';
|
||||
|
||||
export function CertificatesSection() {
|
||||
return (
|
||||
<section className="py-16 md:py-24 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
eyebrow="Uprawnienia"
|
||||
title="Nasze certyfikaty"
|
||||
subtitle="Zaufaj wykwalifikowanym specjalistom. Posiadamy wszystkie licencje wymagane do profesjonalnego montażu urządzeń HVAC."
|
||||
align="center"
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 md:gap-10 mt-16">
|
||||
{companyDetails.certificates.map((cert, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="group flex flex-col items-center text-center gap-4 p-6 bg-slate-50 rounded-2xl border border-border hover:border-accent/50 transition-colors"
|
||||
>
|
||||
{/* W rzeczywistości użylibyśmy obrazków. Tutaj dajemy szary prostokąt jako placeholder wg wytycznych */}
|
||||
<div className="relative w-full aspect-[4/3] bg-white rounded-lg border border-border flex items-center justify-center p-4 overflow-hidden group-hover:shadow-md transition-shadow">
|
||||
{/* Jeśli cert.image istnieje realnie, można by użyć Image, na razie tekstowy placeholder */}
|
||||
<div className="text-sm font-bold text-slate-300 font-heading">
|
||||
LOGO / CERTYFIKAT
|
||||
</div>
|
||||
</div>
|
||||
<p className="font-semibold text-primary text-sm leading-tight group-hover:text-accent transition-colors">
|
||||
{cert.name}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Settings2, Zap, Palette, Calculator, Lightbulb } from 'lucide-react';
|
||||
|
||||
const steps = [
|
||||
{ icon: Settings2, label: 'Marka i model' },
|
||||
{ icon: Zap, label: 'Moc chłodnicza' },
|
||||
{ icon: Palette, label: 'Design i funkcje' },
|
||||
{ icon: Calculator, label: 'Wycena montażu' },
|
||||
];
|
||||
|
||||
export const ConfiguratorTeaser = () => {
|
||||
return (
|
||||
<section className="py-20 bg-primary text-white relative overflow-hidden">
|
||||
{/* Background decoration */}
|
||||
<div className="absolute top-0 right-0 w-1/2 h-full bg-gradient-to-l from-accent/20 to-transparent pointer-events-none" />
|
||||
<div className="absolute -bottom-24 -left-24 w-64 h-64 bg-accent rounded-full blur-[120px] opacity-20 pointer-events-none" />
|
||||
|
||||
<div className="container mx-auto px-4 md:px-8 max-w-7xl relative z-10">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||
<div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6">
|
||||
Skonfiguruj swój idealny zestaw w 4 krokach
|
||||
</h2>
|
||||
<p className="text-gray-300 text-lg mb-8 max-w-xl">
|
||||
Nie wiesz jakie urządzenie wybrać? Nasz prosty konfigurator krok po kroku pomoże Ci dobrać sprzęt i natychmiast sprawdzić szacunkowy koszt wraz z montażem.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-10 lg:mb-0">
|
||||
<Link
|
||||
href="/kalkulator"
|
||||
className={cn("btn btn-primary text-white group")}
|
||||
>
|
||||
Uruchom konfigurator
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
{/* Visual representation of steps */}
|
||||
<div className="bg-white/10 backdrop-blur-sm border border-white/20 rounded-2xl p-8">
|
||||
<div className="space-y-6 relative before:absolute before:inset-0 before:ml-6 before:-translate-x-px md:before:mx-auto md:before:translate-x-0 before:h-full before:w-0.5 before:bg-gradient-to-b before:from-transparent before:via-white/30 before:to-transparent">
|
||||
{steps.map((step, idx) => {
|
||||
const Icon = step.icon;
|
||||
return (
|
||||
<div key={idx} className="relative flex items-center justify-between md:justify-normal md:odd:flex-row-reverse group">
|
||||
<div className="flex items-center justify-center w-12 h-12 rounded-full border-4 border-primary bg-accent text-white shadow md:order-1 md:group-odd:-translate-x-1/2 md:group-even:translate-x-1/2 z-10">
|
||||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="w-[calc(100%-4rem)] md:w-[calc(50%-3rem)] bg-white/10 border border-white/20 p-4 rounded-xl backdrop-blur-sm group-hover:bg-white/20 transition-colors">
|
||||
<h4 className="font-semibold text-lg">{step.label}</h4>
|
||||
<p className="text-sm text-gray-300">Krok {idx + 1}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Pro tip card */}
|
||||
<div className="mt-8 bg-accent/20 border border-accent/40 rounded-xl p-4 flex items-start">
|
||||
<Lightbulb className="w-6 h-6 text-accent shrink-0 mr-3 mt-0.5" />
|
||||
<div>
|
||||
<h5 className="font-semibold text-accent-light mb-1">Porada eksperta</h5>
|
||||
<p className="text-sm text-gray-200">
|
||||
Szybki przelicznik mocy: <strong>10 m² = 1 kW</strong> mocy chłodniczej. Dla pokoju 25 m² szukaj urządzenia o mocy ok. 2.5 kW.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { companyDetails } from '@/content/company';
|
||||
import { ContactForm } from '../forms/ContactForm';
|
||||
import { MapPin, Phone, Mail, Clock, FileText } from 'lucide-react';
|
||||
|
||||
export function ContactSection() {
|
||||
return (
|
||||
<section className="py-16 md:py-24 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 lg:gap-24">
|
||||
{/* Lewa kolumna: Dane kontaktowe */}
|
||||
<div className="flex flex-col">
|
||||
<h2 className="text-3xl md:text-4xl font-extrabold text-primary font-heading mb-6">
|
||||
Porozmawiajmy o Twoim projekcie
|
||||
</h2>
|
||||
<p className="text-text-muted text-lg mb-12">
|
||||
Jesteśmy do Twojej dyspozycji. Zadzwoń, napisz e-mail lub skorzystaj z formularza obok. Odpowiadamy najszybciej jak to możliwe, najczęściej w ciągu kilku godzin.
|
||||
</p>
|
||||
|
||||
<div className="space-y-8 flex-grow">
|
||||
{/* Telefon */}
|
||||
<div className="flex items-start gap-4 group">
|
||||
<div className="w-12 h-12 rounded-xl bg-accent/10 text-accent flex items-center justify-center shrink-0 group-hover:bg-accent group-hover:text-white transition-colors">
|
||||
<Phone className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-text-muted uppercase tracking-wider mb-1">Zadzwoń do nas</p>
|
||||
<a href={`tel:${companyDetails.phoneRaw}`} className="text-2xl font-bold text-primary hover:text-accent transition-colors font-heading">
|
||||
{companyDetails.phone}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div className="flex items-start gap-4 group">
|
||||
<div className="w-12 h-12 rounded-xl bg-accent/10 text-accent flex items-center justify-center shrink-0 group-hover:bg-accent group-hover:text-white transition-colors">
|
||||
<Mail className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-text-muted uppercase tracking-wider mb-1">Napisz e-mail</p>
|
||||
<a href={`mailto:${companyDetails.email}`} className="text-xl font-bold text-primary hover:text-accent transition-colors font-heading">
|
||||
{companyDetails.email}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Adres */}
|
||||
<div className="flex items-start gap-4 group">
|
||||
<div className="w-12 h-12 rounded-xl bg-accent/10 text-accent flex items-center justify-center shrink-0 group-hover:bg-accent group-hover:text-white transition-colors">
|
||||
<MapPin className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-text-muted uppercase tracking-wider mb-1">Nasze biuro</p>
|
||||
<div className="text-lg font-medium text-primary">
|
||||
{companyDetails.name}<br />
|
||||
{companyDetails.address.street}<br />
|
||||
{companyDetails.address.zipCode} {companyDetails.address.city}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Godziny i NIP */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-8 pt-8 border-t border-border">
|
||||
<div className="flex items-start gap-3">
|
||||
<Clock className="w-5 h-5 text-primary/60 mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-text-muted mb-1">Godziny pracy</p>
|
||||
<p className="text-sm text-primary font-medium">{companyDetails.hours}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<FileText className="w-5 h-5 text-primary/60 mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-text-muted mb-1">Dane firmy</p>
|
||||
<p className="text-sm text-primary font-medium">NIP: {companyDetails.nip}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Prawa kolumna: Formularz */}
|
||||
<div className="relative">
|
||||
{/* Tło dekoracyjne */}
|
||||
<div className="absolute inset-0 bg-primary/5 rounded-[2.5rem] transform translate-x-4 translate-y-4 -z-10 hidden sm:block" />
|
||||
<ContactForm />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import Link from "next/link";
|
||||
import { Phone, ArrowRight } from "lucide-react";
|
||||
import { companyDetails } from "@/content/company";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface CtaBannerProps {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
ctaText?: string;
|
||||
ctaLink?: string;
|
||||
bgVariant?: "primary" | "accent-light";
|
||||
}
|
||||
|
||||
export function CtaBanner({
|
||||
title = "Masz pytania? Umów darmową konsultację",
|
||||
subtitle = "Skontaktuj się z nami! Doradzimy najlepsze rozwiązania, dobierzemy moc urządzeń i przygotujemy bezpłatną wycenę.",
|
||||
ctaText = "Zamów bezpłatną konsultację",
|
||||
ctaLink = "/kontakt",
|
||||
bgVariant = "primary",
|
||||
}: CtaBannerProps) {
|
||||
const isPrimary = bgVariant === "primary";
|
||||
|
||||
return (
|
||||
<section className="relative overflow-hidden py-16 md:py-20">
|
||||
{/* Background and overlays */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 z-0",
|
||||
isPrimary
|
||||
? "bg-[#0F2A47] text-white"
|
||||
: "bg-accent-light text-primary"
|
||||
)}
|
||||
/>
|
||||
|
||||
{isPrimary && (
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_bottom_left,rgba(43,168,224,0.12),transparent_40%)] z-0" />
|
||||
)}
|
||||
|
||||
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-col lg:flex-row items-center justify-between gap-10">
|
||||
{/* Text Area */}
|
||||
<div className="max-w-2xl text-center lg:text-left flex flex-col gap-3">
|
||||
<h2
|
||||
className={cn(
|
||||
"text-2xl sm:text-3xl md:text-4xl font-extrabold font-heading leading-tight",
|
||||
isPrimary ? "text-white" : "text-primary"
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<p
|
||||
className={cn(
|
||||
"text-sm sm:text-base leading-relaxed max-w-xl",
|
||||
isPrimary ? "text-slate-300" : "text-text-muted"
|
||||
)}
|
||||
>
|
||||
{subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex flex-col sm:flex-row items-center gap-4 mt-8 w-full sm:w-auto shrink-0">
|
||||
<Link href={ctaLink} className="w-full sm:w-auto">
|
||||
<button className="btn btn-primary w-full sm:w-auto font-bold text-white">
|
||||
<span>{ctaText}</span>
|
||||
<ArrowRight className="size-5" />
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
<a
|
||||
href={`tel:${companyDetails.phoneRaw}`}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
<button
|
||||
className={cn(
|
||||
"btn btn-outline btn-primary bg-white w-full sm:w-auto font-bold flex items-center gap-2",
|
||||
isPrimary
|
||||
? "text-white hover:bg-white/20 hover:text-white border-white/30"
|
||||
: "text-primary hover:bg-primary/10 border-primary/20"
|
||||
)}
|
||||
>
|
||||
<Phone className="size-5" />
|
||||
<span>{companyDetails.phone}</span>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { Post } from '@/types/types';
|
||||
import { Calendar, User, ArrowRight } from 'lucide-react';
|
||||
|
||||
interface FeaturedPostProps {
|
||||
post: Post;
|
||||
}
|
||||
|
||||
export function FeaturedPost({ post }: FeaturedPostProps) {
|
||||
// Formatowanie daty
|
||||
const dateObj = new Date(post.date);
|
||||
const formattedDate = dateObj.toLocaleDateString('pl-PL', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
});
|
||||
|
||||
// Tłumaczenie kategorii
|
||||
const categoryLabels: Record<string, string> = {
|
||||
'klimatyzacja': 'Klimatyzacja',
|
||||
'pompy-ciepla': 'Pompy Ciepła',
|
||||
'remonty': 'Remonty',
|
||||
'hydraulika': 'Hydraulika',
|
||||
'porady': 'Porady'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="group relative rounded-2xl overflow-hidden bg-white border border-border shadow-sm hover:shadow-lg transition-all duration-300">
|
||||
<div className="flex flex-col md:flex-row">
|
||||
{/* Obraz */}
|
||||
<div className="relative w-full md:w-3/5 aspect-video md:aspect-auto md:min-h-[400px]">
|
||||
<Image
|
||||
src={post.image || 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=1200&q=80'}
|
||||
alt={post.title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-700 group-hover:scale-105"
|
||||
/>
|
||||
<div className="absolute top-4 left-4">
|
||||
<span className="px-3 py-1 bg-accent text-white text-xs font-bold rounded-full uppercase tracking-wider shadow-md">
|
||||
{categoryLabels[post.category] || post.category}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Treść */}
|
||||
<div className="p-8 md:p-12 flex flex-col justify-center w-full md:w-2/5">
|
||||
<div className="flex items-center gap-4 text-text-muted text-sm font-medium mb-4">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Calendar className="w-4 h-4 text-primary/60" />
|
||||
<span>{formattedDate}</span>
|
||||
</div>
|
||||
{post.author && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<User className="w-4 h-4 text-primary/60" />
|
||||
<span>{post.author}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl md:text-3xl font-extrabold text-primary font-heading leading-tight mb-4 group-hover:text-accent transition-colors">
|
||||
<Link href={`/blog/${post.slug}`} className="before:absolute before:inset-0">
|
||||
{post.title}
|
||||
</Link>
|
||||
</h3>
|
||||
|
||||
<p className="text-text-muted leading-relaxed mb-8 line-clamp-3">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-2 text-primary font-semibold group-hover:text-accent transition-colors mt-auto">
|
||||
<span>Czytaj pełny artykuł</span>
|
||||
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import Link from "next/link";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { SectionHeading } from "../common/SectionHeading";
|
||||
import { ProductCard } from "../shop/ProductCard";
|
||||
import { getFeaturedProducts } from "@/content/products";
|
||||
|
||||
export function FeaturedProducts() {
|
||||
const featured = getFeaturedProducts(4);
|
||||
|
||||
return (
|
||||
<section className="py-16 md:py-24 bg-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
eyebrow="Nasz Asortyment"
|
||||
title="Polecane klimatyzatory z montażem"
|
||||
subtitle="Poznaj wybrane modele urządzeń chłodzących i grzewczych od wiodących marek. Zapewniamy kompleksowy montaż."
|
||||
align="center"
|
||||
/>
|
||||
|
||||
{/* Products Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8">
|
||||
{featured.map((product) => (
|
||||
<ProductCard key={product.slug} product={product} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Catalog Link */}
|
||||
<div className="flex justify-center mt-12">
|
||||
<Link href="/sklep">
|
||||
<button className="btn btn-outline btn-primary bg-white font-bold flex items-center gap-2 group">
|
||||
<span>Zobacz cały sklep</span>
|
||||
<ArrowRight className="size-4 group-hover:translate-x-1 transition-transform" />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -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 "Czyste Powietrze" oraz "Mój Prąd", które pozwalają na zwrot znacznej części kosztów inwestycji. Pomożemy Ci przejść przez cały proces formalny i przygotować niezbędne wnioski.
|
||||
</p>
|
||||
|
||||
<ul className="space-y-4 mb-10">
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-accent shrink-0" />
|
||||
<span className="font-medium">Bezpłatny audyt możliwości dofinansowania</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-accent shrink-0" />
|
||||
<span className="font-medium">Pomoc w wypełnieniu dokumentacji</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-6 h-6 text-accent shrink-0" />
|
||||
<span className="font-medium">Możliwość prefinansowania inwestycji</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<Link
|
||||
href="/dofinansowania"
|
||||
className={cn("btn btn-primary group")}
|
||||
>
|
||||
Dowiedz się więcej o dotacjach
|
||||
</Link>
|
||||
<p className="text-xs text-primary/60 mt-4 max-w-sm">
|
||||
* Wysokość dotacji uzależniona jest od progu dochodowego i zakresu termomodernizacji. Skontaktuj się z nami w celu dokładnej wyceny.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
{/* Dekoracyjna karta */}
|
||||
<div className="bg-white rounded-3xl p-8 shadow-xl relative z-10 border border-white/50">
|
||||
<div className="flex justify-between items-start mb-8">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 font-semibold mb-1">Twój kosztorys z dotacją</p>
|
||||
<h4 className="text-xl font-bold text-primary">Przykładowa kalkulacja</h4>
|
||||
</div>
|
||||
<div className="w-12 h-12 bg-accent/10 rounded-full flex items-center justify-center">
|
||||
<FileText className="w-6 h-6 text-accent" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 text-sm">
|
||||
<div className="flex justify-between pb-4 border-b border-gray-100">
|
||||
<span className="text-gray-600">Wartość inwestycji</span>
|
||||
<span className="font-semibold text-gray-900">45 000 zł</span>
|
||||
</div>
|
||||
<div className="flex justify-between pb-4 border-b border-gray-100 text-green-600 font-medium">
|
||||
<span>Dotacja "Czyste Powietrze"</span>
|
||||
<span>- 20 500 zł</span>
|
||||
</div>
|
||||
<div className="flex justify-between pb-4 border-b border-gray-100 text-green-600 font-medium">
|
||||
<span>Ulga termomodernizacyjna</span>
|
||||
<span>- 4 600 zł</span>
|
||||
</div>
|
||||
<div className="flex justify-between pt-2">
|
||||
<span className="text-gray-900 font-bold">Ostateczny koszt inwestora</span>
|
||||
<span className="font-bold text-2xl text-primary">19 900 zł</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Background elements */}
|
||||
<div className="absolute -top-6 -right-6 w-full h-full bg-accent/20 rounded-3xl -z-10 blur-sm"></div>
|
||||
<div className="absolute -bottom-8 -left-8 w-32 h-32 bg-white/40 rounded-full -z-10 blur-xl"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { ArrowRight, Star } from "lucide-react";
|
||||
|
||||
export interface HeroSectionProps {
|
||||
eyebrow?: string;
|
||||
title?: React.ReactNode;
|
||||
subtitle?: string;
|
||||
ctaText?: string;
|
||||
ctaLink?: string;
|
||||
secondaryCtaText?: string;
|
||||
secondaryCtaLink?: string;
|
||||
backgroundImage?: string;
|
||||
features?: string[];
|
||||
}
|
||||
|
||||
export function HeroSection({
|
||||
eyebrow = "HVAC & Usługi Wykończeniowe",
|
||||
title = (
|
||||
<>
|
||||
Klimatyzacja, pompy ciepła<br className="hidden sm:inline" />
|
||||
<span className="text-accent"> i usługi domowe</span>
|
||||
</>
|
||||
),
|
||||
subtitle = "Oferujemy profesjonalny dobór, czysty montaż i sprawny serwis. Wykończymy Twoje wnętrza i naprawimy instalacje. Solidnie i z polecenia.",
|
||||
ctaText = "Darmowa wycena",
|
||||
ctaLink = "/kontakt",
|
||||
secondaryCtaText = "Zobacz ofertę",
|
||||
secondaryCtaLink = "/uslugi-domowe",
|
||||
backgroundImage = "/hero.jpg",
|
||||
features
|
||||
}: HeroSectionProps = {}) {
|
||||
return (
|
||||
<section className="relative bg-primary min-h-[480px] md:min-h-[600px] flex items-center overflow-hidden">
|
||||
{/* TODO ZDJĘCIE HERO: realne zdjęcie firmy/montażu, format poziomy min. 1920×1080, plik public/hero.jpg */}
|
||||
<Image
|
||||
src={backgroundImage}
|
||||
alt="Montaż klimatyzacji i pomp ciepła"
|
||||
fill
|
||||
priority
|
||||
sizes="100vw"
|
||||
className="object-cover"
|
||||
/>
|
||||
|
||||
{/* NAKŁADKA POD TEKST - jednolita, przyciemniająca */}
|
||||
<div className="absolute inset-0 bg-black/50" />
|
||||
|
||||
<div className="relative z-10 w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 md:py-24">
|
||||
<div className="flex flex-col items-start text-left max-w-3xl">
|
||||
{/* Badge */}
|
||||
{eyebrow && (
|
||||
<div className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-full bg-accent/20 border border-accent/30 text-white text-xs font-semibold uppercase tracking-wider mb-6 font-heading">
|
||||
<span className="size-2 rounded-full bg-accent" />
|
||||
{eyebrow}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Heading */}
|
||||
<h1 className="text-3xl md:text-5xl lg:text-6xl font-extrabold tracking-tight font-heading leading-[1.1] text-white">
|
||||
{title}
|
||||
</h1>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p className="mt-6 text-base md:text-lg lg:text-xl text-white/90 max-w-2xl leading-relaxed">
|
||||
{subtitle}
|
||||
</p>
|
||||
|
||||
{/* Optional Features List */}
|
||||
{features && features.length > 0 && (
|
||||
<ul className="mt-6 grid grid-cols-2 gap-3 text-white/90 font-medium">
|
||||
{features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-center gap-2">
|
||||
<Star className="size-4 text-accent fill-accent" />
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{/* CTA Buttons */}
|
||||
<div className="mt-10 flex flex-col sm:flex-row items-center gap-4 w-full sm:w-auto">
|
||||
{ctaText && ctaLink && (
|
||||
<Link href={ctaLink} className="w-full sm:w-auto group">
|
||||
<button className="btn btn-primary text-white font-bold w-full sm:w-auto">
|
||||
<span>{ctaText}</span>
|
||||
<ArrowRight className="size-5 group-hover:translate-x-1 transition-transform" />
|
||||
</button>
|
||||
</Link>
|
||||
)}
|
||||
{secondaryCtaText && secondaryCtaLink && (
|
||||
<Link href={secondaryCtaLink} className="w-full sm:w-auto">
|
||||
<button className="btn btn-outline btn-primary bg-white font-bold w-full sm:w-auto">
|
||||
{secondaryCtaText}
|
||||
</button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Trust Bar (Hero specific) */}
|
||||
<div className="mt-8 flex items-center gap-3 text-sm font-medium text-white/80">
|
||||
<div className="flex items-center text-cta">
|
||||
<Star className="size-4 fill-current" />
|
||||
<Star className="size-4 fill-current" />
|
||||
<Star className="size-4 fill-current" />
|
||||
<Star className="size-4 fill-current" />
|
||||
<Star className="size-4 fill-current" />
|
||||
</div>
|
||||
<span>
|
||||
{/* MOCK: do podmiany */}
|
||||
4,9/5 w Google · 500+ realizacji · 12 lat na rynku
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import Link from "next/link";
|
||||
import { ThermometerSun, Home, ChevronRight, Wrench, ShieldCheck, ArrowRight } from "lucide-react";
|
||||
|
||||
export function PillarsSection() {
|
||||
return (
|
||||
<section className="py-16 md:py-24 bg-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Pillar 1: Chłód i Ogrzewanie */}
|
||||
<div className="group relative rounded-2xl border border-border p-8 md:p-12 bg-slate-50 hover:bg-white hover:border-accent hover:shadow-xl transition-all duration-300 flex flex-col justify-between overflow-hidden">
|
||||
{/* Visual background gradient glow on hover */}
|
||||
<div className="absolute right-0 top-0 size-48 rounded-full bg-accent/5 filter blur-3xl opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
|
||||
<div>
|
||||
<div className="inline-flex p-4 rounded-xl bg-accent-light text-accent mb-6 group-hover:scale-110 transition-transform duration-300">
|
||||
<ThermometerSun className="size-8" />
|
||||
</div>
|
||||
<h3 className="text-2xl md:text-3xl font-bold text-primary font-heading mb-4">
|
||||
Chłód i Ogrzewanie
|
||||
</h3>
|
||||
<p className="text-text-muted text-base leading-relaxed mb-8 max-w-md">
|
||||
Klimatyzacja i pompy ciepła dla Twojego domu lub biura. Oferujemy profesjonalny audyt, dobór optymalnej mocy, autoryzowany montaż i stałą opiekę serwisową.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Link href="/klimatyzacja">
|
||||
<button className="btn btn-primary text-white flex items-center gap-2">
|
||||
<span>Przejdź do oferty HVAC</span>
|
||||
<ChevronRight className="size-5 group-hover:translate-x-1 transition-transform" />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pillar 2: Twój Dom */}
|
||||
<div className="group relative rounded-2xl border border-border p-8 md:p-12 bg-slate-50 hover:bg-white hover:border-cta hover:shadow-xl transition-all duration-300 flex flex-col justify-between overflow-hidden">
|
||||
{/* Visual background gradient glow on hover */}
|
||||
<div className="absolute right-0 top-0 size-48 rounded-full bg-cta/5 filter blur-3xl opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
|
||||
<div>
|
||||
<div className="inline-flex p-4 rounded-xl bg-cta/10 text-cta mb-6 group-hover:scale-110 transition-transform duration-300">
|
||||
<Home className="size-8" />
|
||||
</div>
|
||||
<h3 className="text-2xl md:text-3xl font-bold text-primary font-heading mb-4">
|
||||
Twój Dom
|
||||
</h3>
|
||||
<p className="text-text-muted text-base leading-relaxed mb-8 max-w-md">
|
||||
Usługi remontowo-budowlane, prace wykończeniowe łazienek i kuchni, instalacje hydrauliczne oraz kompleksowy serwis sprzętu chłodniczego i AGD.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 text-center">
|
||||
<Link href="/uslugi-domowe">
|
||||
<button className="btn btn-outline btn-primary bg-white font-bold flex items-center gap-2 mx-auto group">
|
||||
<span>Pełna oferta usług</span>
|
||||
<ArrowRight className="size-4 group-hover:translate-x-1 transition-transform" />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { Post } from '@/types/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Calendar, ArrowRight } from 'lucide-react';
|
||||
|
||||
interface PostsGridProps {
|
||||
posts: Post[];
|
||||
}
|
||||
|
||||
const POSTS_PER_PAGE = 6;
|
||||
|
||||
const categories = [
|
||||
{ id: 'wszystkie', label: 'Wszystkie' },
|
||||
{ id: 'klimatyzacja', label: 'Klimatyzacja' },
|
||||
{ id: 'pompy-ciepla', label: 'Pompy ciepła' },
|
||||
{ id: 'remonty', label: 'Remonty' },
|
||||
{ id: 'hydraulika', label: 'Hydraulika' },
|
||||
{ id: 'porady', label: 'Porady' },
|
||||
];
|
||||
|
||||
export function PostsGrid({ posts }: PostsGridProps) {
|
||||
const [activeCategory, setActiveCategory] = useState('wszystkie');
|
||||
const [visibleCount, setVisibleCount] = useState(POSTS_PER_PAGE);
|
||||
|
||||
const filteredPosts = posts.filter(
|
||||
(post) => activeCategory === 'wszystkie' || post.category === activeCategory
|
||||
);
|
||||
|
||||
const visiblePosts = filteredPosts.slice(0, visibleCount);
|
||||
const hasMore = visibleCount < filteredPosts.length;
|
||||
|
||||
const handleShowMore = () => {
|
||||
setVisibleCount(prev => prev + POSTS_PER_PAGE);
|
||||
};
|
||||
|
||||
const handleCategoryChange = (categoryId: string) => {
|
||||
setActiveCategory(categoryId);
|
||||
setVisibleCount(POSTS_PER_PAGE); // Reset paginacji przy zmianie kategorii
|
||||
};
|
||||
|
||||
const categoryLabels: Record<string, string> = {
|
||||
'klimatyzacja': 'Klimatyzacja',
|
||||
'pompy-ciepla': 'Pompy Ciepła',
|
||||
'remonty': 'Remonty',
|
||||
'hydraulika': 'Hydraulika',
|
||||
'porady': 'Porady'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="py-12">
|
||||
{/* Filtry */}
|
||||
<div className="flex flex-wrap items-center gap-3 mb-12">
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={category.id}
|
||||
onClick={() => handleCategoryChange(category.id)}
|
||||
className={cn(
|
||||
"px-4 py-2 rounded-full text-sm font-semibold transition-all duration-300 border",
|
||||
activeCategory === category.id
|
||||
? "bg-primary text-white border-primary shadow-md"
|
||||
: "bg-white text-text-muted border-border hover:border-primary/30 hover:text-primary"
|
||||
)}
|
||||
>
|
||||
{category.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Siatka Artykułów */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{visiblePosts.map((post) => {
|
||||
const dateObj = new Date(post.date);
|
||||
const formattedDate = dateObj.toLocaleDateString('pl-PL', {
|
||||
day: 'numeric', month: 'long', year: 'numeric'
|
||||
});
|
||||
|
||||
return (
|
||||
<article
|
||||
key={post.slug}
|
||||
className="group flex flex-col bg-white rounded-2xl overflow-hidden border border-border hover:shadow-lg transition-all duration-300"
|
||||
>
|
||||
<Link href={`/blog/${post.slug}`} className="relative aspect-[16/10] overflow-hidden">
|
||||
<Image
|
||||
src={post.image || 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=800&q=80'}
|
||||
alt={post.title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-700 group-hover:scale-105"
|
||||
/>
|
||||
<div className="absolute top-4 left-4">
|
||||
<span className="px-3 py-1 bg-accent text-white text-[10px] font-bold rounded-full uppercase tracking-wider shadow-sm">
|
||||
{categoryLabels[post.category] || post.category}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className="flex flex-col flex-grow p-6 md:p-8">
|
||||
<div className="flex items-center gap-2 text-xs font-medium text-text-muted mb-3">
|
||||
<Calendar className="w-3.5 h-3.5 text-primary/50" />
|
||||
<span>{formattedDate}</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold text-primary font-heading leading-snug mb-3 group-hover:text-accent transition-colors">
|
||||
<Link href={`/blog/${post.slug}`}>
|
||||
{post.title}
|
||||
</Link>
|
||||
</h3>
|
||||
|
||||
<p className="text-sm text-text-muted leading-relaxed line-clamp-3 mb-6 flex-grow">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
|
||||
<div className="mt-auto">
|
||||
<Link
|
||||
href={`/blog/${post.slug}`}
|
||||
className="inline-flex items-center gap-1.5 text-sm font-semibold text-primary group-hover:text-accent transition-colors"
|
||||
>
|
||||
<span>Czytaj więcej</span>
|
||||
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{visiblePosts.length === 0 && (
|
||||
<div className="text-center py-16 bg-slate-50 rounded-2xl border border-border">
|
||||
<p className="text-lg text-text-muted font-medium">Brak artykułów w tej kategorii.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pokaż więcej */}
|
||||
{hasMore && (
|
||||
<div className="mt-12 flex justify-center">
|
||||
<button
|
||||
onClick={handleShowMore}
|
||||
className="btn btn-outline btn-primary bg-white font-bold"
|
||||
>
|
||||
Wczytaj więcej artykułów
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
import { pricingData, PricingItem } from '@/content/pricing';
|
||||
import { Check, Info, ArrowRight, Download } from 'lucide-react';
|
||||
|
||||
export const PricingTable = () => {
|
||||
const splitItems = pricingData.filter((item) => item.category === 'split');
|
||||
const multisplitItems = pricingData.filter((item) => item.category === 'multisplit');
|
||||
const additionalItems = pricingData.filter((item) => item.category === 'additional');
|
||||
|
||||
const renderTableRows = (items: PricingItem[]) => {
|
||||
return items.map((item) => (
|
||||
<tr key={item.id} className="border-b border-gray-100 last:border-0 hover:bg-gray-50/50 transition-colors">
|
||||
<td className="py-4 px-4 text-gray-800 font-medium">{item.name}</td>
|
||||
<td className="py-4 px-4 text-center whitespace-nowrap text-gray-600">{item.priceNet} zł</td>
|
||||
<td className="py-4 px-4 text-center whitespace-nowrap font-semibold text-primary">{item.priceGrossPrivate} zł</td>
|
||||
<td className="py-4 px-4 text-center whitespace-nowrap text-gray-600 hidden sm:table-cell">{item.priceGrossCompany} zł</td>
|
||||
</tr>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="py-16 bg-white" id="cennik">
|
||||
<div className="container mx-auto px-4 md:px-8 max-w-5xl">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-bold text-primary mb-4">Cennik usług instalacyjnych</h2>
|
||||
<p className="text-gray-600 max-w-2xl mx-auto">
|
||||
Przejrzyste ceny bez ukrytych kosztów. Ostateczna wycena zależy od specyfiki budynku i trudności montażu – dokonywana jest zawsze przed rozpoczęciem prac.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded-xl border border-gray-200 shadow-sm mb-8">
|
||||
<table className="w-full text-left border-collapse min-w-[600px]">
|
||||
<thead>
|
||||
<tr className="bg-gray-50 border-b border-gray-200 text-sm uppercase text-gray-500">
|
||||
<th className="py-4 px-4 font-semibold w-1/2">Usługa</th>
|
||||
<th className="py-4 px-4 font-semibold text-center">Cena netto</th>
|
||||
<th className="py-4 px-4 font-semibold text-center">Brutto (8% VAT)*</th>
|
||||
<th className="py-4 px-4 font-semibold text-center hidden sm:table-cell">Brutto (23% VAT)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{/* Split */}
|
||||
<tr>
|
||||
<td colSpan={4} className="bg-gray-50/80 py-3 px-4 font-semibold text-primary text-sm uppercase">
|
||||
Montaż układów SPLIT
|
||||
</td>
|
||||
</tr>
|
||||
{renderTableRows(splitItems)}
|
||||
|
||||
{/* Multisplit */}
|
||||
<tr>
|
||||
<td colSpan={4} className="bg-gray-50/80 py-3 px-4 font-semibold text-primary text-sm uppercase">
|
||||
Montaż układów MULTISPLIT
|
||||
</td>
|
||||
</tr>
|
||||
{renderTableRows(multisplitItems)}
|
||||
|
||||
{/* Usługi dodatkowe */}
|
||||
<tr>
|
||||
<td colSpan={4} className="bg-gray-50/80 py-3 px-4 font-semibold text-primary text-sm uppercase">
|
||||
Usługi dodatkowe i serwis
|
||||
</td>
|
||||
</tr>
|
||||
{renderTableRows(additionalItems)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-8 items-start justify-between bg-gray-50 p-6 rounded-xl border border-gray-100">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-primary mb-3 flex items-center gap-2">
|
||||
<Check className="w-5 h-5 text-accent" />
|
||||
Co obejmuje montaż podstawowy?
|
||||
</h4>
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm text-gray-600">
|
||||
<li className="flex items-start gap-2"><div className="w-1.5 h-1.5 rounded-full bg-accent mt-1.5 shrink-0" /> Wykonanie przewiertu przez ścianę</li>
|
||||
<li className="flex items-start gap-2"><div className="w-1.5 h-1.5 rounded-full bg-accent mt-1.5 shrink-0" /> Instalacja chłodnicza do 3 mb</li>
|
||||
<li className="flex items-start gap-2"><div className="w-1.5 h-1.5 rounded-full bg-accent mt-1.5 shrink-0" /> Podłączenie do gniazdka (kabel 3m)</li>
|
||||
<li className="flex items-start gap-2"><div className="w-1.5 h-1.5 rounded-full bg-accent mt-1.5 shrink-0" /> Grawitacyjne odprowadzenie skroplin</li>
|
||||
<li className="flex items-start gap-2"><div className="w-1.5 h-1.5 rounded-full bg-accent mt-1.5 shrink-0" /> Montaż jednostek do wysokości 3m</li>
|
||||
<li className="flex items-start gap-2"><div className="w-1.5 h-1.5 rounded-full bg-accent mt-1.5 shrink-0" /> Próba szczelności i uruchomienie</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="md:w-64 shrink-0 flex flex-col gap-4">
|
||||
<button className="btn btn-outline btn-primary bg-white w-full justify-between group">
|
||||
Pobierz cennik PDF
|
||||
<Download className="w-4 h-4 ml-2 group-hover:-translate-y-1 transition-transform" />
|
||||
</button>
|
||||
|
||||
<div className="text-xs text-gray-500 flex items-start gap-2 bg-white p-3 rounded-lg border border-gray-100">
|
||||
<Info className="w-4 h-4 shrink-0 text-accent mt-0.5" />
|
||||
<p>
|
||||
*Stawka 8% VAT obowiązuje przy montażu w obiektach mieszkalnych (domy do 300m², mieszkania do 150m²).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
PhoneCall, ClipboardCheck, Cpu, HeartHandshake, HelpCircle,
|
||||
ShieldAlert, Construction, Layers, MonitorDown, Server, Gauge, Zap, BookOpen
|
||||
} from "lucide-react";
|
||||
import { SectionHeading } from "../common/SectionHeading";
|
||||
import { mainProcessSteps } from "@/content/process";
|
||||
import { ProcessStep } from "@/types/types";
|
||||
|
||||
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
PhoneCall,
|
||||
ClipboardCheck,
|
||||
Cpu,
|
||||
HeartHandshake,
|
||||
ShieldAlert,
|
||||
Construction,
|
||||
Layers,
|
||||
MonitorDown,
|
||||
Server,
|
||||
Gauge,
|
||||
Zap,
|
||||
BookOpen,
|
||||
};
|
||||
|
||||
interface ProcessStepsProps {
|
||||
steps?: ProcessStep[];
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
eyebrow?: string;
|
||||
}
|
||||
|
||||
export function ProcessSteps({
|
||||
steps = mainProcessSteps,
|
||||
title = "Prosty proces współpracy w 4 krokach",
|
||||
subtitle = "Od momentu kontaktu do pełnego uruchomienia i serwisu — dbamy o każdy detal na każdym etapie.",
|
||||
eyebrow = "Jak Działamy"
|
||||
}: ProcessStepsProps) {
|
||||
return (
|
||||
<section className="py-16 md:py-24 bg-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
eyebrow={eyebrow}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
align="center"
|
||||
/>
|
||||
|
||||
{/* Process Steps Timeline */}
|
||||
<div className="relative mt-12">
|
||||
{/* Desktop connecting line (only if few steps, otherwise wrap) */}
|
||||
{steps.length <= 4 && (
|
||||
<div className="hidden lg:block absolute top-10 left-[12%] right-[12%] h-0.5 bg-slate-200 z-0" />
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-y-12 gap-x-8 relative z-10">
|
||||
{steps.map((step, idx) => {
|
||||
const Icon = iconMap[step.icon || ""] || HelpCircle;
|
||||
return (
|
||||
<div key={idx} className="flex flex-col items-center text-center group">
|
||||
{/* Circle Badge with Icon & Number */}
|
||||
<div className="relative size-20 rounded-full bg-slate-50 border-2 border-slate-200 flex items-center justify-center text-primary group-hover:border-accent group-hover:bg-accent-light group-hover:text-accent transition-all duration-300 shadow-sm z-10">
|
||||
{/* Step Number */}
|
||||
<span className="absolute -top-1.5 -right-1.5 size-6 rounded-full bg-cta text-white text-xs font-bold flex items-center justify-center font-heading border-2 border-white shadow-sm">
|
||||
{step.number}
|
||||
</span>
|
||||
<Icon className="size-8 group-hover:scale-110 transition-transform duration-300" />
|
||||
</div>
|
||||
|
||||
{/* Title & Description */}
|
||||
<h3 className="text-lg font-bold text-primary font-heading mt-6 mb-2">
|
||||
{step.title}
|
||||
</h3>
|
||||
<p className="text-sm text-text-muted leading-relaxed max-w-[240px]">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { projectsData, Project } from '@/content/projects';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { MapPin, ArrowRight } from 'lucide-react';
|
||||
|
||||
type Category = 'wszystkie' | Project['category'];
|
||||
|
||||
const categories: { id: Category; label: string }[] = [
|
||||
{ id: 'wszystkie', label: 'Wszystkie' },
|
||||
{ id: 'klimatyzacja', label: 'Klimatyzacja' },
|
||||
{ id: 'pompy-ciepla', label: 'Pompy ciepła' },
|
||||
{ id: 'remonty', label: 'Remonty' },
|
||||
{ id: 'hydraulika', label: 'Hydraulika' },
|
||||
];
|
||||
|
||||
export function ProjectsGrid() {
|
||||
const [activeCategory, setActiveCategory] = useState<Category>('wszystkie');
|
||||
|
||||
const filteredProjects = projectsData.filter(
|
||||
(project) => activeCategory === 'wszystkie' || project.category === activeCategory
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="py-16 md:py-24 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Filtry */}
|
||||
<div className="flex flex-wrap items-center justify-center gap-3 mb-12">
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={category.id}
|
||||
onClick={() => setActiveCategory(category.id)}
|
||||
className={cn(
|
||||
"px-5 py-2.5 rounded-full text-sm font-semibold transition-all duration-300 border",
|
||||
activeCategory === category.id
|
||||
? "bg-primary text-white border-primary shadow-md"
|
||||
: "bg-white text-text-muted border-border hover:border-primary/30 hover:text-primary"
|
||||
)}
|
||||
>
|
||||
{category.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Siatka */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{filteredProjects.map((project) => (
|
||||
<div
|
||||
key={project.id}
|
||||
className="group relative rounded-2xl overflow-hidden bg-slate-100 aspect-[4/3]"
|
||||
>
|
||||
<Image
|
||||
src={project.imageUrl}
|
||||
alt={project.title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-700 group-hover:scale-105"
|
||||
/>
|
||||
|
||||
{/* Nakładka gradientowa */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-primary/90 via-primary/40 to-transparent opacity-80 group-hover:opacity-90 transition-opacity duration-300" />
|
||||
|
||||
{/* Treść (Zawsze widoczna lokalizacja i kategoria, pojawiający się opis) */}
|
||||
<div className="absolute inset-0 p-6 flex flex-col justify-end">
|
||||
<div className="transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<span className="px-3 py-1 bg-accent text-white text-xs font-bold rounded-full uppercase tracking-wider">
|
||||
{categories.find(c => c.id === project.category)?.label}
|
||||
</span>
|
||||
<div className="flex items-center gap-1 text-white/80 text-sm font-medium">
|
||||
<MapPin className="w-3.5 h-3.5" />
|
||||
<span>{project.location}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold text-white mb-2 font-heading">
|
||||
{project.title}
|
||||
</h3>
|
||||
|
||||
<p className="text-white/80 text-sm line-clamp-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300 delay-100">
|
||||
{project.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredProjects.length === 0 && (
|
||||
<div className="text-center py-20 text-text-muted">
|
||||
<p className="text-lg">Brak realizacji w tej kategorii.</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { SectionHeading } from "../common/SectionHeading";
|
||||
import { IconCard } from "../common/IconCard";
|
||||
import { services as defaultServices } from "@/content/services";
|
||||
|
||||
interface ServicesGridProps {
|
||||
services?: {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
href?: string;
|
||||
}[];
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
eyebrow?: string;
|
||||
}
|
||||
|
||||
export function ServicesGrid({
|
||||
services = defaultServices,
|
||||
title = "Kompleksowe usługi dla Twojego domu",
|
||||
subtitle = "Zapewniamy profesjonalne wsparcie w zakresie instalacji sanitarnych, grzewczych, chłodniczych oraz prac wykończeniowych.",
|
||||
eyebrow = "Pełna Oferta"
|
||||
}: ServicesGridProps) {
|
||||
return (
|
||||
<section className="py-16 md:py-24 bg-slate-50 border-t border-b border-border">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
eyebrow={eyebrow}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
align="center"
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8 mt-10">
|
||||
{services.map((service, idx) => (
|
||||
<IconCard
|
||||
key={idx}
|
||||
title={service.title}
|
||||
description={service.description}
|
||||
iconName={service.icon}
|
||||
href={service.href}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Star, Quote } from "lucide-react";
|
||||
import { SectionHeading } from "../common/SectionHeading";
|
||||
import { testimonials } from "@/content/testimonials";
|
||||
|
||||
export function Testimonials() {
|
||||
return (
|
||||
<section className="py-16 md:py-24 bg-slate-50 border-t border-b border-border">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
eyebrow="Opinie Klientów"
|
||||
title="Co mówią o nas nasi klienci?"
|
||||
subtitle="Zadowolenie klientów to nasz najwyższy cel. Zobacz opinie osób, które zaufały naszym instalatorom i ekipom budowlanym."
|
||||
align="center"
|
||||
/>
|
||||
|
||||
{/* Testimonials Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{testimonials.map((test, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="relative p-8 rounded-xl bg-white border border-border hover:shadow-md transition-shadow flex flex-col justify-between"
|
||||
>
|
||||
{/* Quote icon background decoration */}
|
||||
<Quote className="absolute top-6 right-6 size-12 text-slate-100 -z-0" />
|
||||
|
||||
<div className="relative z-10 flex flex-col gap-4">
|
||||
{/* Stars */}
|
||||
<div className="flex gap-1 text-cta">
|
||||
{Array.from({ length: test.rating }).map((_, iIdx) => (
|
||||
<Star key={iIdx} className="size-4 fill-cta text-cta" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Review Text */}
|
||||
<p className="text-sm md:text-base text-primary italic leading-relaxed">
|
||||
“{test.text}”
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Reviewer Details */}
|
||||
<div className="relative z-10 border-t border-slate-100 mt-6 pt-4 flex justify-between items-center text-xs">
|
||||
<span className="font-bold text-primary font-heading">
|
||||
{test.name}
|
||||
</span>
|
||||
{test.source && (
|
||||
<span className="px-2.5 py-1 rounded bg-slate-100 text-text-muted font-medium">
|
||||
{test.source}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Award, FileText, ShieldCheck, Sparkles } from "lucide-react";
|
||||
import { companyDetails } from "@/content/company";
|
||||
|
||||
export function WhyUs() {
|
||||
const advantages = [
|
||||
{
|
||||
icon: Award,
|
||||
title: "Uprawnienia F-Gaz i certyfikaty",
|
||||
description: "Posiadamy wszystkie wymagane certyfikaty UDT oraz autoryzacje wiodących producentów klimatyzacji.",
|
||||
},
|
||||
{
|
||||
icon: FileText,
|
||||
title: "Jasna wycena bez ukrytych kosztów",
|
||||
description: "Przed montażem otrzymujesz pełny kosztorys. Ceny podane w ofercie są cenami ostatecznymi.",
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
title: "Gwarancja i wsparcie serwisowe",
|
||||
description: "Zapewniamy pełną opiekę pogwarancyjną oraz coroczne przeglądy zachowujące gwarancję producenta.",
|
||||
},
|
||||
{
|
||||
icon: Sparkles,
|
||||
title: "Czystość i dbałość o detale",
|
||||
description: "Pracujemy z użyciem odkurzaczy przemysłowych. Po montażu zostawiamy pomieszczenia idealnie czyste.",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="py-16 md:py-24 bg-slate-50 border-t border-b border-border">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-16 items-center">
|
||||
{/* Left Column: Title, Intro and Statistics */}
|
||||
<div className="lg:col-span-5 flex flex-col gap-6">
|
||||
<div>
|
||||
<span className="text-xs md:text-sm font-semibold uppercase tracking-wider text-cta font-heading">
|
||||
Dlaczego AeroService
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-primary font-heading leading-tight mt-2">
|
||||
Zaufany partner w instalacjach domowych
|
||||
</h2>
|
||||
<p className="text-sm md:text-base text-text-muted mt-4 leading-relaxed">
|
||||
Stawiamy na najwyższą jakość wykonania, terminowość oraz bezkompromisowe bezpieczeństwo. Pomagamy na każdym kroku — od wyboru odpowiedniego urządzenia, poprzez załatwienie dotacji, aż po finalny montaż i regularny serwis.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Counters Grid */}
|
||||
<div className="grid grid-cols-2 gap-6 mt-6 border-t border-border pt-8">
|
||||
{companyDetails.stats.map((stat, idx) => (
|
||||
<div key={idx} className="flex flex-col">
|
||||
<span className="text-3xl md:text-4xl font-extrabold text-primary font-heading">
|
||||
{stat.value}
|
||||
</span>
|
||||
<span className="text-xs md:text-sm text-text-muted mt-1 leading-snug">
|
||||
{stat.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Advantages Grid */}
|
||||
<div className="lg:col-span-7 grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
{advantages.map((adv, idx) => {
|
||||
const Icon = adv.icon;
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="p-6 rounded-xl bg-white border border-border hover:shadow-md transition-shadow flex flex-col gap-4"
|
||||
>
|
||||
<div className="size-12 rounded-lg bg-accent-light text-accent flex items-center justify-center shrink-0">
|
||||
<Icon className="size-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-bold text-primary font-heading">
|
||||
{adv.title}
|
||||
</h3>
|
||||
<p className="text-xs md:text-sm text-text-muted mt-1.5 leading-relaxed">
|
||||
{adv.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useCartStore } from '@/lib/cart-store';
|
||||
import { formatCurrency } from '@/lib/utils';
|
||||
import { Minus, Plus, Trash2, Wrench } from 'lucide-react';
|
||||
|
||||
export function CartItems() {
|
||||
const { items, removeItem, updateQuantity, toggleInstall } = useCartStore();
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div className="bg-white p-12 rounded-2xl border border-border text-center">
|
||||
<div className="w-20 h-20 bg-slate-50 text-slate-300 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<Trash2 className="w-10 h-10" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-primary font-heading mb-4">Twój koszyk jest pusty</h2>
|
||||
<p className="text-text-muted mb-8">Nie dodałeś jeszcze żadnych produktów do koszyka.</p>
|
||||
<Link href="/sklep" className="inline-flex bg-cta hover:bg-cta-hover text-white font-bold py-3 px-8 rounded-xl transition-colors">
|
||||
Przejdź do sklepu
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{items.map((item) => {
|
||||
const itemPrice = item.withInstall ? item.priceWithInstall : item.price;
|
||||
const totalGross = itemPrice * item.quantity;
|
||||
|
||||
return (
|
||||
<div key={item.id} className="bg-white p-6 rounded-2xl border border-border flex flex-col sm:flex-row gap-6 relative">
|
||||
{/* Usuń pozycję */}
|
||||
<button
|
||||
onClick={() => removeItem(item.id)}
|
||||
className="absolute top-4 right-4 p-2 text-text-muted hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
|
||||
aria-label="Usuń z koszyka"
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* Obraz */}
|
||||
<div className="relative w-24 h-24 sm:w-32 sm:h-32 shrink-0 bg-slate-50 rounded-xl p-2 border border-border">
|
||||
<Image
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Treść */}
|
||||
<div className="flex flex-col flex-grow justify-between">
|
||||
<div>
|
||||
<div className="text-xs font-bold text-text-muted uppercase tracking-wider mb-1">
|
||||
{item.brand}
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-primary font-heading leading-tight mb-2 pr-10">
|
||||
<Link href={`/sklep/${item.slug}`} className="hover:text-accent transition-colors">
|
||||
{item.name}
|
||||
</Link>
|
||||
</h3>
|
||||
{/* Wariant mocowy */}
|
||||
{(item.label || item.power) && (
|
||||
<div className="text-sm text-text-muted mb-3">
|
||||
Wariant: <span className="font-semibold text-primary">{item.label || `${item.power} kW`}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Opcja Montażu i Cena jednostkowa */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mt-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer group">
|
||||
<div className="relative flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.withInstall}
|
||||
onChange={() => toggleInstall(item.id)}
|
||||
className="w-5 h-5 rounded border-border text-green-600 focus:ring-green-600"
|
||||
/>
|
||||
</div>
|
||||
<span className={`text-sm font-semibold flex items-center gap-1.5 ${item.withInstall ? 'text-green-700' : 'text-text-muted group-hover:text-primary'}`}>
|
||||
<Wrench className="w-4 h-4" />
|
||||
Z montażem
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{/* Ilość i Cena */}
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center border border-border rounded-lg bg-slate-50">
|
||||
<button
|
||||
onClick={() => updateQuantity(item.id, item.quantity - 1)}
|
||||
disabled={item.quantity <= 1}
|
||||
className="w-8 h-8 flex items-center justify-center text-text-muted hover:text-primary disabled:opacity-50"
|
||||
>
|
||||
<Minus className="w-4 h-4" />
|
||||
</button>
|
||||
<span className="w-8 text-center text-sm font-bold">{item.quantity}</span>
|
||||
<button
|
||||
onClick={() => updateQuantity(item.id, item.quantity + 1)}
|
||||
className="w-8 h-8 flex items-center justify-center text-text-muted hover:text-primary"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-xl font-extrabold text-primary font-heading w-24 text-right">
|
||||
{formatCurrency(totalGross)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useCartStore } from '@/lib/cart-store';
|
||||
import { formatCurrency } from '@/lib/utils';
|
||||
import Link from 'next/link';
|
||||
import { ShieldCheck, ArrowRight, Tag } from 'lucide-react';
|
||||
|
||||
export function CartSummary() {
|
||||
const { items, getTotals } = useCartStore();
|
||||
const [promoCode, setPromoCode] = useState('');
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
const { subtotal, vat, total } = getTotals();
|
||||
|
||||
// Obliczamy ile pozycji ma montaż by wyświetlić odpowiednią informację
|
||||
const itemsWithInstall = items.filter(i => i.withInstall).length;
|
||||
|
||||
return (
|
||||
<div className="bg-slate-50 p-6 md:p-8 rounded-2xl border border-border sticky top-24">
|
||||
<h3 className="text-xl font-bold text-primary font-heading mb-6">Podsumowanie zamówienia</h3>
|
||||
|
||||
{/* Kod rabatowy */}
|
||||
<div className="mb-8">
|
||||
<label htmlFor="promo" className="text-sm font-semibold text-text-muted mb-2 block">Posiadasz kod rabatowy?</label>
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-grow">
|
||||
<Tag className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
|
||||
<input
|
||||
id="promo"
|
||||
type="text"
|
||||
value={promoCode}
|
||||
onChange={(e) => setPromoCode(e.target.value)}
|
||||
placeholder="Wprowadź kod"
|
||||
className="w-full pl-9 pr-4 py-2.5 rounded-xl border border-border focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-all text-sm"
|
||||
/>
|
||||
</div>
|
||||
<button className="btn btn-outline btn-primary bg-white shrink-0">Zastosuj</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Kwoty */}
|
||||
<div className="space-y-4 mb-6 border-b border-border/50 pb-6">
|
||||
<div className="flex justify-between text-sm text-text-muted">
|
||||
<span>Suma netto</span>
|
||||
<span className="font-semibold text-primary">{formatCurrency(subtotal)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm text-text-muted">
|
||||
<span>Podatek VAT (mieszany)</span>
|
||||
<span className="font-semibold text-primary">{formatCurrency(vat)}</span>
|
||||
</div>
|
||||
{itemsWithInstall > 0 && (
|
||||
<div className="text-xs text-green-700 bg-green-50 p-2 rounded border border-green-100 mt-2">
|
||||
Zastosowano obniżoną stawkę VAT 8% dla {itemsWithInstall} poz. z montażem (dla os. fizycznych).
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Total */}
|
||||
<div className="flex justify-between items-end mb-8">
|
||||
<span className="text-lg font-bold text-primary">Do zapłaty brutto</span>
|
||||
<span className="text-3xl font-extrabold text-primary font-heading text-right">{formatCurrency(total)}</span>
|
||||
</div>
|
||||
|
||||
{/* Przycisk (Placeholder - brak właściwego checkoutu) */}
|
||||
<Link href="/zamowienie" className="block w-full">
|
||||
<button className="btn btn-primary w-full font-bold group cursor-pointer text-white">
|
||||
Przejdź do zamówienia
|
||||
<ArrowRight className="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" />
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
{/* Zaufanie */}
|
||||
<div className="mt-6 flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2 text-xs text-text-muted font-medium justify-center">
|
||||
<ShieldCheck className="w-4 h-4 text-green-600" />
|
||||
Bezpieczne zakupy z certyfikatem SSL
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { Product } from '@/types/types';
|
||||
import { Heart, Wrench } from 'lucide-react';
|
||||
import { formatCurrency } from '@/lib/utils';
|
||||
|
||||
interface ProductCardProps {
|
||||
product: Product;
|
||||
}
|
||||
|
||||
export function ProductCard({ product }: ProductCardProps) {
|
||||
// Jeśli ma warianty, przycisk prowadzi do karty z wariantami
|
||||
const hasVariants = product.variants && product.variants.length > 0;
|
||||
|
||||
return (
|
||||
<div className="group flex flex-col bg-white rounded-2xl border border-border overflow-hidden hover:shadow-xl transition-all duration-300">
|
||||
{/* Obraz */}
|
||||
<Link href={`/sklep/${product.slug}`} className="relative aspect-square p-6 bg-slate-50 flex items-center justify-center overflow-hidden">
|
||||
{/* Placeholder tagów */}
|
||||
<div className="absolute top-4 left-4 z-10 flex flex-col gap-2">
|
||||
{product.features?.slice(0, 2).map((feat, idx) => (
|
||||
<span key={idx} className="bg-white/90 backdrop-blur-sm text-primary text-[10px] font-bold px-2 py-1 rounded shadow-sm border border-border/50 uppercase">
|
||||
{feat}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Serce */}
|
||||
<button className="absolute top-4 right-4 z-10 p-2 bg-white/90 backdrop-blur-sm rounded-full text-text-muted hover:text-red-500 hover:bg-white transition-colors shadow-sm">
|
||||
<Heart className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<Image
|
||||
src={product.image || 'https://images.unsplash.com/photo-1581094794329-c8112a89af12?w=800&q=80'}
|
||||
alt={product.name}
|
||||
width={400}
|
||||
height={400}
|
||||
className="object-contain w-full h-full transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* Treść */}
|
||||
<div className="flex flex-col flex-grow p-6">
|
||||
<div className="text-xs font-bold text-text-muted uppercase tracking-wider mb-1">
|
||||
{product.brand}
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-primary font-heading leading-tight mb-2 group-hover:text-accent transition-colors">
|
||||
<Link href={`/sklep/${product.slug}`}>
|
||||
{product.name}
|
||||
</Link>
|
||||
</h3>
|
||||
|
||||
{/* Informacja o mocy */}
|
||||
<div className="text-sm font-medium text-text-muted mb-4">
|
||||
Moc chłodnicza: <span className="text-primary">{product.power} kW</span>
|
||||
</div>
|
||||
|
||||
{/* Ceny */}
|
||||
<div className="mt-auto space-y-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-2xl font-extrabold text-primary font-heading">
|
||||
{formatCurrency(product.price)} <span className="text-sm text-text-muted font-normal">samo urządzenie</span>
|
||||
</div>
|
||||
{product.priceWithInstall && (
|
||||
<div className="flex items-center gap-1.5 text-sm font-bold text-green-600 bg-green-50 px-2 py-1 rounded w-fit">
|
||||
<Wrench className="w-3.5 h-3.5" />
|
||||
od {formatCurrency(product.priceWithInstall)} z montażem
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link href={`/sklep/${product.slug}`} className="block mt-4">
|
||||
<button className="btn btn-outline btn-primary bg-white w-full font-semibold">
|
||||
{hasVariants ? 'Wybierz wariant' : 'Zobacz szczegóły'}
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface ProductGalleryProps {
|
||||
image: string;
|
||||
name: string;
|
||||
images?: string[]; // opcjonalna dodatkowa galeria z contentu, u nas na razie w mocku mamy 1 image
|
||||
}
|
||||
|
||||
export function ProductGallery({ image, name, images }: ProductGalleryProps) {
|
||||
const [mainImage, setMainImage] = useState(image || 'https://images.unsplash.com/photo-1581094794329-c8112a89af12?w=800&q=80');
|
||||
|
||||
// Tworzymy tablicę miniatur, używamy placeholdera jako drugiej by pokazać funkcjonalność
|
||||
const gallery = images && images.length > 0
|
||||
? images
|
||||
: [image, 'https://images.unsplash.com/photo-1584622650111-993a426fbf0a?w=800&q=80'];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Główne zdjęcie */}
|
||||
<div className="relative aspect-square w-full bg-slate-50 rounded-2xl border border-border overflow-hidden flex items-center justify-center p-8">
|
||||
<Image
|
||||
src={mainImage}
|
||||
alt={name}
|
||||
width={800}
|
||||
height={800}
|
||||
className="object-contain w-full h-full"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Miniatury */}
|
||||
{gallery.length > 1 && (
|
||||
<div className="flex gap-4 overflow-x-auto pb-2">
|
||||
{gallery.map((img, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => setMainImage(img)}
|
||||
className={`relative w-24 h-24 shrink-0 rounded-xl border-2 overflow-hidden bg-slate-50 p-2 transition-all ${mainImage === img ? 'border-primary' : 'border-border hover:border-primary/50'}`}
|
||||
>
|
||||
<Image
|
||||
src={img}
|
||||
alt={`${name} - miniatura ${idx + 1}`}
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Product } from '@/types/types';
|
||||
import { formatCurrency } from '@/lib/utils';
|
||||
import { useCartStore } from '@/lib/cart-store';
|
||||
import { ShoppingCart, Heart, Scale, ShieldCheck, Truck, Clock, AlertCircle, Wrench, ChevronDown, Check, Zap, Volume2, ThermometerSun, Settings, ArrowRightLeft, Sun, Mail, Info, Snowflake, CreditCard } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface ProductInfoProps {
|
||||
product: Product;
|
||||
}
|
||||
|
||||
export function ProductInfo({ product }: ProductInfoProps) {
|
||||
const router = useRouter();
|
||||
const addItem = useCartStore((state) => state.addItem);
|
||||
|
||||
// Wariant
|
||||
const hasVariants = product.variants && product.variants.length > 0;
|
||||
const defaultVariant = hasVariants ? product.variants![0] : null;
|
||||
const [selectedVariant, setSelectedVariant] = useState(defaultVariant);
|
||||
const [hoursPerDay, setHoursPerDay] = useState<number>(1);
|
||||
|
||||
// Aktualnie wybrana cena
|
||||
const currentPrice = selectedVariant ? selectedVariant.price : product.price;
|
||||
const currentPriceWithInstall = selectedVariant ? selectedVariant.priceWithInstall : product.priceWithInstall;
|
||||
const currentPower = selectedVariant ? selectedVariant.power : product.power;
|
||||
const currentLabel = selectedVariant ? selectedVariant.label : undefined;
|
||||
|
||||
const [addedMessage, setAddedMessage] = useState('');
|
||||
|
||||
// Kalkulator energii
|
||||
const costPerKwh = 1.15; // Przykładowy koszt 1 kWh w PLN
|
||||
const consumptionCooling = currentPower / 4;
|
||||
const consumptionHeating = currentPower / 3.8;
|
||||
const costHourCooling = consumptionCooling * costPerKwh;
|
||||
const costHourHeating = consumptionHeating * costPerKwh;
|
||||
|
||||
const monthlyCostCooling = costHourCooling * hoursPerDay * 30;
|
||||
const monthlyCostHeating = costHourHeating * hoursPerDay * 30;
|
||||
|
||||
const handleAddToCart = (withInstall: boolean) => {
|
||||
addItem({
|
||||
slug: product.slug,
|
||||
name: product.name,
|
||||
brand: product.brand,
|
||||
image: product.image || '/hero.jpg',
|
||||
power: currentPower,
|
||||
label: currentLabel,
|
||||
price: currentPrice,
|
||||
priceWithInstall: currentPriceWithInstall || currentPrice,
|
||||
withInstall
|
||||
});
|
||||
|
||||
setAddedMessage(withInstall ? 'Dodano z montażem' : 'Dodano do koszyka');
|
||||
setTimeout(() => setAddedMessage(''), 3000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<span className="text-sm font-bold text-text-muted uppercase tracking-wider">{product.brand}</span>
|
||||
<div className="flex items-center gap-2 text-text-muted">
|
||||
<button className="btn btn-ghost btn-circle btn-sm hover:text-accent" aria-label="Dodaj do listy życzeń">
|
||||
<Heart className="w-5 h-5" />
|
||||
</button>
|
||||
<button className="btn btn-ghost btn-circle btn-sm hover:text-accent" aria-label="Porównaj">
|
||||
<ArrowRightLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl md:text-4xl font-extrabold text-primary font-heading leading-tight mb-4">
|
||||
{product.name}
|
||||
</h1>
|
||||
|
||||
{/* Cechy skrócone */}
|
||||
{product.features && (
|
||||
<div className="flex flex-wrap gap-2 mb-8">
|
||||
{product.features.map((feat, idx) => (
|
||||
<span key={idx} className="bg-slate-100 text-text-muted text-xs font-semibold px-2.5 py-1 rounded-md border border-border">
|
||||
{feat}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Wybór Wariantu */}
|
||||
{hasVariants && (
|
||||
<div className="mb-6">
|
||||
<span className="block text-xs font-semibold text-text-muted uppercase tracking-wider mb-3">Wybierz wariant mocy:</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{product.variants!.map((variant, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => setSelectedVariant(variant)}
|
||||
className={`px-4 py-2 rounded-lg border text-sm font-bold transition-all ${
|
||||
selectedVariant?.label === variant.label
|
||||
? 'border-primary bg-primary text-white shadow-md'
|
||||
: 'border-border bg-white text-text-muted hover:border-primary/50 hover:text-primary'
|
||||
}`}
|
||||
>
|
||||
{variant.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Szybka specyfikacja z ikonami */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6 text-sm">
|
||||
<div className="flex items-center gap-2 text-primary font-semibold">
|
||||
<Snowflake className="w-4 h-4 text-accent" />
|
||||
<span>{product.specs?.["Klasa energetyczna chłodzenie/grzanie"]?.split("/")[0]?.trim() || "A++"}</span>
|
||||
</div>
|
||||
{product.heating !== false && (
|
||||
<div className="flex items-center gap-2 text-primary font-semibold">
|
||||
<Sun className="w-4 h-4 text-orange-500" />
|
||||
<span>{product.specs?.["Klasa energetyczna chłodzenie/grzanie"]?.split("/")[1]?.trim() || "A+"}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-primary font-semibold">
|
||||
<Zap className="w-4 h-4 text-yellow-500" />
|
||||
<span>{(currentPower / 4).toFixed(2)} kW/h</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-primary font-semibold">
|
||||
<Zap className="w-4 h-4 text-yellow-500" />
|
||||
<span>{currentPower} kW</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Szczegółowa specyfikacja skrócona (lista) */}
|
||||
<ul className="space-y-2.5 mb-8 text-sm text-text-muted">
|
||||
<li className="flex gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent mt-2 shrink-0"></span>
|
||||
<span><strong className="text-primary font-semibold">Wydajność chłodnicza:</strong> {(currentPower).toFixed(2)} kW</span>
|
||||
</li>
|
||||
{product.heating !== false && (
|
||||
<li className="flex gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent mt-2 shrink-0"></span>
|
||||
<span><strong className="text-primary font-semibold">Wydajność grzewcza:</strong> {(currentPower * 1.1).toFixed(2)} kW</span>
|
||||
</li>
|
||||
)}
|
||||
<li className="flex gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent mt-2 shrink-0"></span>
|
||||
<span><strong className="text-primary font-semibold">Średnie zużycie energii (chłodzenie/grzanie):</strong> {(consumptionCooling).toFixed(2)} / {(consumptionHeating).toFixed(2)} kW</span>
|
||||
</li>
|
||||
<li className="flex gap-2 flex-col">
|
||||
<div className="flex gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent mt-2 shrink-0"></span>
|
||||
<span><strong className="text-primary font-semibold">Szacunkowy koszt energii – 1 h (chłodzenie/grzanie):</strong> {costHourCooling.toFixed(2)} / {costHourHeating.toFixed(2)} zł</span>
|
||||
</div>
|
||||
<div className="ml-3.5 mt-2 p-4 bg-white rounded-xl border border-border shadow-sm inline-block w-full max-w-sm">
|
||||
<div className="flex items-center justify-between gap-4 mb-3">
|
||||
<span className="text-sm font-medium text-text-muted">Ile godzin dziennie używasz?</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="24"
|
||||
value={hoursPerDay}
|
||||
onChange={(e) => setHoursPerDay(Number(e.target.value) || 1)}
|
||||
className="w-16 px-2 py-1.5 text-sm font-bold text-center border border-border rounded-lg outline-none focus:border-primary transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-text-muted leading-relaxed">
|
||||
Twój miesięczny koszt wyniesie: <br />
|
||||
<strong className="text-primary font-bold">Chłodzenie {monthlyCostCooling.toFixed(2)} zł</strong> / <strong className="text-primary font-bold">Grzanie {monthlyCostHeating.toFixed(2)} zł</strong>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent mt-2 shrink-0"></span>
|
||||
<span><strong className="text-primary font-semibold">SEER / SCOP:</strong> {product.specs?.["SEER / SCOP"] || "6.10 / 4.00"}</span>
|
||||
</li>
|
||||
<li className="flex gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent mt-2 shrink-0"></span>
|
||||
<span><strong className="text-primary font-semibold">Klasa energetyczna (chłodzenie/grzanie):</strong> {product.specs?.["Klasa energetyczna chłodzenie/grzanie"] || "A++ / A+"}</span>
|
||||
</li>
|
||||
<li className="flex gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent mt-2 shrink-0"></span>
|
||||
<span><strong className="text-primary font-semibold">Poziom głośności jednostki wewnętrznej:</strong> {product.specs?.["Poziom hałasu min/max (dB)"] || "24–38 dB"}</span>
|
||||
</li>
|
||||
<li className="flex gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent mt-2 shrink-0"></span>
|
||||
<span><strong className="text-primary font-semibold">Zakres pracy - chłodzenie:</strong> {product.specs?.["Zakres temperatur zewnętrznych - chłodzenie"] || "-15~43 °C"}</span>
|
||||
</li>
|
||||
<li className="flex gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent mt-2 shrink-0"></span>
|
||||
<span><strong className="text-primary font-semibold">Czas gwarancji:</strong> 5 lat</span>
|
||||
</li>
|
||||
{product.features && (
|
||||
<li className="flex gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent mt-2 shrink-0"></span>
|
||||
<span><strong className="text-primary font-semibold">Funkcje:</strong> {product.features.join(", ")}</span>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
{/* Dostępność i Kontakt */}
|
||||
<div className="flex items-center gap-6 py-4 border-y border-border mb-8">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3.5 h-3.5 rounded-full bg-green-500 shadow-sm shadow-green-200"></div>
|
||||
<span className="text-sm font-medium text-primary">Dostępny od ręki</span>
|
||||
</div>
|
||||
<Link href="/kontakt" className="flex items-center gap-2 text-text-muted hover:text-primary transition-colors">
|
||||
<Mail className="w-4 h-4" />
|
||||
<span className="text-sm">Zapytaj o produkt</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Ceny i CTA */}
|
||||
<div className="bg-slate-50 p-6 rounded-2xl border border-border mb-8 space-y-6">
|
||||
|
||||
{/* Cena z montażem (Promowana) */}
|
||||
{currentPriceWithInstall && (
|
||||
<div className="flex flex-col gap-3 pb-6 border-b border-border/50">
|
||||
<div className="flex justify-between items-end flex-wrap gap-4">
|
||||
<div>
|
||||
<span className="flex items-center gap-1 text-text-muted text-xs font-semibold mb-1 uppercase tracking-wider">
|
||||
Cena z montażem dla konsumenta (8% VAT) <Info className="w-3 h-3" />
|
||||
</span>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-3xl font-extrabold text-primary font-heading">{formatCurrency(currentPriceWithInstall)}</span>
|
||||
</div>
|
||||
<div className="text-xs text-accent font-medium mt-1">Darmowa wycena</div>
|
||||
</div>
|
||||
<button onClick={() => handleAddToCart(true)} className="btn btn-primary text-white">
|
||||
<ShoppingCart className="w-4 h-4 mr-2" />
|
||||
Kup z montażem
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cena bez montażu */}
|
||||
<div className="flex justify-between items-end flex-wrap gap-4">
|
||||
<div>
|
||||
<span className="block text-text-muted text-xs font-semibold mb-1 uppercase tracking-wider">Cena urządzenia bez montażu (23% VAT)</span>
|
||||
<div className="text-xl font-bold text-primary font-heading">{formatCurrency(currentPrice)}</div>
|
||||
</div>
|
||||
<button onClick={() => handleAddToCart(false)} className="btn btn-outline btn-primary bg-white">
|
||||
Do koszyka
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Feedback o dodaniu */}
|
||||
{addedMessage && (
|
||||
<div className="flex items-center gap-2 text-sm font-bold text-green-600 bg-green-50 p-3 rounded-lg border border-green-200 animate-in fade-in slide-in-from-top-2">
|
||||
<Check className="w-4 h-4" />
|
||||
{addedMessage}. <button onClick={() => router.push('/koszyk')} className="underline hover:text-green-800">Przejdź do koszyka</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Atuty */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="flex flex-col items-center text-center p-4 bg-white rounded-xl border border-border">
|
||||
<Truck className="w-6 h-6 text-accent mb-2" />
|
||||
<span className="text-xs font-bold text-primary">Darmowa dostawa</span>
|
||||
<span className="text-[10px] text-text-muted">na terenie Polski</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center text-center p-4 bg-white rounded-xl border border-border">
|
||||
<ShieldCheck className="w-6 h-6 text-accent mb-2" />
|
||||
<span className="text-xs font-bold text-primary">5 lat gwarancji</span>
|
||||
<span className="text-[10px] text-text-muted">na urządzenie i montaż</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center text-center p-4 bg-white rounded-xl border border-border">
|
||||
<Settings className="w-6 h-6 text-accent mb-2" />
|
||||
<span className="text-xs font-bold text-primary">Pełne wsparcie</span>
|
||||
<span className="text-[10px] text-text-muted">na każdym etapie</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Product } from '@/types/types';
|
||||
import { ProductCard } from './ProductCard';
|
||||
import { SlidersHorizontal, X, Search } from 'lucide-react';
|
||||
|
||||
interface ProductsGridProps {
|
||||
products: Product[];
|
||||
}
|
||||
|
||||
export function ProductsGrid({ products }: ProductsGridProps) {
|
||||
const [selectedBrands, setSelectedBrands] = useState<string[]>([]);
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
||||
const [sortBy, setSortBy] = useState<string>('recommended');
|
||||
const [showFiltersMobile, setShowFiltersMobile] = useState(false);
|
||||
|
||||
// Pobranie unikalnych wartości do filtrów
|
||||
const brands = useMemo(() => Array.from(new Set(products.map(p => p.brand))), [products]);
|
||||
|
||||
const categoryLabels: Record<string, string> = {
|
||||
'scienne': 'Ścienne',
|
||||
'multisplit': 'Multisplit',
|
||||
'kasetonowe': 'Kasetonowe',
|
||||
'przenosne': 'Przenośne',
|
||||
};
|
||||
const categories = useMemo(() => Array.from(new Set(products.map(p => p.category))), [products]);
|
||||
|
||||
// Logika filtrowania i sortowania
|
||||
const filteredProducts = useMemo(() => {
|
||||
let result = [...products];
|
||||
|
||||
if (selectedBrands.length > 0) {
|
||||
result = result.filter(p => selectedBrands.includes(p.brand));
|
||||
}
|
||||
if (selectedCategories.length > 0) {
|
||||
result = result.filter(p => selectedCategories.includes(p.category));
|
||||
}
|
||||
|
||||
if (sortBy === 'price-asc') {
|
||||
result.sort((a, b) => a.price - b.price);
|
||||
} else if (sortBy === 'price-desc') {
|
||||
result.sort((a, b) => b.price - a.price);
|
||||
} else if (sortBy === 'power-asc') {
|
||||
result.sort((a, b) => a.power - b.power);
|
||||
} else if (sortBy === 'power-desc') {
|
||||
result.sort((a, b) => b.power - a.power);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [products, selectedBrands, selectedCategories, sortBy]);
|
||||
|
||||
const toggleBrand = (brand: string) => {
|
||||
setSelectedBrands(prev =>
|
||||
prev.includes(brand) ? prev.filter(b => b !== brand) : [...prev, brand]
|
||||
);
|
||||
};
|
||||
|
||||
const toggleCategory = (category: string) => {
|
||||
setSelectedCategories(prev =>
|
||||
prev.includes(category) ? prev.filter(c => c !== category) : [...prev, category]
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row gap-8 py-12">
|
||||
{/* Mobile Filter Toggle */}
|
||||
<div className="md:hidden flex justify-between items-center mb-4">
|
||||
<span className="font-semibold">{filteredProducts.length} wyników</span>
|
||||
<button className="btn btn-outline btn-primary bg-white" onClick={() => setShowFiltersMobile(!showFiltersMobile)}>
|
||||
<SlidersHorizontal className="w-4 h-4 mr-2" />
|
||||
Filtry
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Sidebar (Filtry) */}
|
||||
<aside className={`w-full md:w-64 shrink-0 ${showFiltersMobile ? 'block' : 'hidden md:block'}`}>
|
||||
<div className="bg-white p-6 rounded-2xl border border-border sticky top-24">
|
||||
<h3 className="font-bold text-lg font-heading text-primary mb-6">Filtrowanie</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Marki */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm mb-3">Marka urządzenia</h4>
|
||||
<div className="space-y-2">
|
||||
{brands.map(brand => (
|
||||
<label key={brand} className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedBrands.includes(brand)}
|
||||
onChange={() => toggleBrand(brand)}
|
||||
className="rounded border-border text-primary focus:ring-primary"
|
||||
/>
|
||||
<span className="text-sm text-text-muted">{brand}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Kategorie */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm mb-3">Typ urządzenia</h4>
|
||||
<div className="space-y-2">
|
||||
{categories.map(category => (
|
||||
<label key={category} className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedCategories.includes(category)}
|
||||
onChange={() => toggleCategory(category)}
|
||||
className="rounded border-border text-primary focus:ring-primary"
|
||||
/>
|
||||
<span className="text-sm text-text-muted">{categoryLabels[category] || category}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Główna sekcja (Narzędzia + Siatka) */}
|
||||
<div className="flex-grow">
|
||||
{/* Pasek narzędzi */}
|
||||
<div className="hidden md:flex justify-between items-center bg-white p-4 rounded-xl border border-border mb-8">
|
||||
<span className="text-text-muted font-medium">{filteredProducts.length} wyników</span>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<label htmlFor="sort" className="text-sm font-semibold text-primary">Sortuj według:</label>
|
||||
<select
|
||||
id="sort"
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value)}
|
||||
className="border border-border rounded-lg px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary/20 bg-slate-50"
|
||||
>
|
||||
<option value="recommended">Rekomendowane</option>
|
||||
<option value="price-asc">Cena rosnąco</option>
|
||||
<option value="price-desc">Cena malejąco</option>
|
||||
<option value="power-asc">Moc rosnąco</option>
|
||||
<option value="power-desc">Moc malejąco</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Siatka */}
|
||||
{filteredProducts.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredProducts.map(product => (
|
||||
<ProductCard key={product.slug} product={product} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20 bg-slate-50 rounded-2xl border border-border">
|
||||
<h3 className="text-xl font-bold text-primary mb-2">Brak wyników</h3>
|
||||
<p className="text-text-muted">Spróbuj zmienić kryteria filtrowania.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Product } from '@/types/types';
|
||||
import { Star } from 'lucide-react';
|
||||
|
||||
interface TabsProps {
|
||||
product: Product;
|
||||
}
|
||||
|
||||
export function Tabs({ product }: TabsProps) {
|
||||
const [activeTab, setActiveTab] = useState<'opis' | 'dane' | 'opinie'>('opis');
|
||||
|
||||
return (
|
||||
<div className="mt-16 bg-white rounded-3xl border border-border overflow-hidden">
|
||||
{/* Nawigacja */}
|
||||
<div className="flex border-b border-border">
|
||||
{(['opis', 'dane', 'opinie'] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`flex-1 py-4 text-sm md:text-base font-bold uppercase tracking-wider transition-colors ${
|
||||
activeTab === tab
|
||||
? 'bg-slate-50 text-primary border-b-2 border-primary'
|
||||
: 'bg-white text-text-muted hover:bg-slate-50 hover:text-primary'
|
||||
}`}
|
||||
>
|
||||
{tab === 'opis' && 'Opis produktu'}
|
||||
{tab === 'dane' && 'Dane techniczne'}
|
||||
{tab === 'opinie' && 'Opinie'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Zawartość */}
|
||||
<div className="p-8 md:p-12">
|
||||
{/* Opis */}
|
||||
{activeTab === 'opis' && (
|
||||
<div className="prose prose-slate max-w-none">
|
||||
<h3 className="text-2xl font-bold text-primary font-heading mb-4">Szczegółowe informacje</h3>
|
||||
<p className="text-lg text-text-muted leading-relaxed">
|
||||
{product.description}
|
||||
</p>
|
||||
{product.heating && (
|
||||
<p className="mt-4 font-semibold text-accent">
|
||||
Ten model posiada funkcję efektywnego ogrzewania, dzięki czemu sprawdzi się doskonale jako dodatkowe źródło ciepła w okresach przejściowych lub główny system grzewczy dla wybranych pomieszczeń.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dane techniczne */}
|
||||
{activeTab === 'dane' && (
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold text-primary font-heading mb-6">Specyfikacja techniczna</h3>
|
||||
{product.specs ? (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<tbody>
|
||||
{Object.entries(product.specs).map(([key, value], idx) => (
|
||||
<tr key={idx} className="border-b border-border hover:bg-slate-50 transition-colors">
|
||||
<th className="py-4 px-4 font-semibold text-primary w-1/2">{key}</th>
|
||||
<td className="py-4 px-4 text-text-muted w-1/2">{value as React.ReactNode}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-text-muted">Brak szczegółowych danych technicznych dla tego modelu.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Opinie */}
|
||||
{activeTab === 'opinie' && (
|
||||
<div>
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<div className="text-4xl font-extrabold text-primary font-heading">4.9</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex text-yellow-400">
|
||||
<Star className="w-5 h-5 fill-current" />
|
||||
<Star className="w-5 h-5 fill-current" />
|
||||
<Star className="w-5 h-5 fill-current" />
|
||||
<Star className="w-5 h-5 fill-current" />
|
||||
<Star className="w-5 h-5 fill-current" />
|
||||
</div>
|
||||
<span className="text-sm text-text-muted font-medium mt-1">Na podstawie 12 opinii</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Przykładowa opinia */}
|
||||
<div className="p-6 bg-slate-50 rounded-2xl border border-border">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<span className="font-bold text-primary">Janusz z Wrocławia</span>
|
||||
<span className="text-xs text-text-muted block mt-0.5">Potwierdzony zakup z montażem</span>
|
||||
</div>
|
||||
<div className="flex text-yellow-400">
|
||||
<Star className="w-4 h-4 fill-current" />
|
||||
<Star className="w-4 h-4 fill-current" />
|
||||
<Star className="w-4 h-4 fill-current" />
|
||||
<Star className="w-4 h-4 fill-current" />
|
||||
<Star className="w-4 h-4 fill-current" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-text-muted text-sm leading-relaxed">
|
||||
Sprzęt najwyższej klasy. Bardzo cichy, w ogóle go nie słychać w sypialni. Montaż wykonany przez panów z AirProject przebiegł super szybko i czyściutko. Polecam!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user