Uporzadkowanie nawigacji i formularzy: ukrycie O nas, ulepszenie kalkulatora i uproszczenie zgloszenia awarii

This commit is contained in:
AI Assistant
2026-07-05 20:34:44 +02:00
parent a0eb555e22
commit 59fd9a9b97
8 changed files with 120 additions and 110 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
'/realizacje', '/realizacje',
'/blog', '/blog',
'/o-nas', // '/o-nas',
'/kontakt', '/kontakt',
'/sklep', '/sklep',
'/koszyk', '/koszyk',
+3 -34
View File
@@ -111,8 +111,6 @@ export function AwariaClient() {
const [name, setName] = useState(''); const [name, setName] = useState('');
const [phone, setPhone] = useState(''); const [phone, setPhone] = useState('');
const [city, setCity] = useState(''); const [city, setCity] = useState('');
const [preferredDate, setPreferredDate] = useState('');
const [preferredTime, setPreferredTime] = useState('');
const [consent, setConsent] = useState(false); const [consent, setConsent] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
@@ -139,8 +137,8 @@ export function AwariaClient() {
name, name,
phone, phone,
city, city,
preferredDate, preferredDate: '',
preferredTime, preferredTime: '',
}); });
setIsSubmitting(false); setIsSubmitting(false);
@@ -329,36 +327,7 @@ export function AwariaClient() {
</div> </div>
</div> </div>
{/* Sekcja 3: Termin */}
<div className="bg-white rounded-2xl border border-border p-6">
<h2 className="text-base font-bold text-primary font-heading mb-4 flex items-center gap-2">
<Clock className="size-4 text-accent" /> Preferowany termin
</h2>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-primary mb-1 block">Data</label>
<input
type="date" value={preferredDate} onChange={e => setPreferredDate(e.target.value)}
min={new Date().toISOString().split('T')[0]}
className="w-full h-[42px] rounded-xl px-3 py-2.5 text-sm border border-border focus:border-accent outline-none"
/>
</div>
<div>
<label className="text-xs font-medium text-primary mb-1 block">Godzina</label>
<select
value={preferredTime} onChange={e => setPreferredTime(e.target.value)}
className="w-full h-[42px] rounded-xl px-3 py-2.5 text-sm border border-border focus:border-accent outline-none bg-white"
>
<option value="">Dowolna</option>
<option value="8:00-10:00">8:00 – 10:00</option>
<option value="10:00-12:00">10:00 – 12:00</option>
<option value="12:00-14:00">12:00 – 14:00</option>
<option value="14:00-16:00">14:00 – 16:00</option>
<option value="16:00-18:00">16:00 – 18:00</option>
</select>
</div>
</div>
</div>
{/* Sekcja 4: Dane kontaktowe */} {/* Sekcja 4: Dane kontaktowe */}
<div className="bg-white rounded-2xl border border-border p-6"> <div className="bg-white rounded-2xl border border-border p-6">
+34
View File
@@ -21,6 +21,7 @@ type ContactFormValues = z.infer<typeof contactFormSchema>;
export function ContactForm() { export function ContactForm() {
const [serverState, setServerState] = useState<{ success?: boolean; message?: string } | null>(null); const [serverState, setServerState] = useState<{ success?: boolean; message?: string } | null>(null);
const [consent, setConsent] = useState(false); const [consent, setConsent] = useState(false);
const [files, setFiles] = useState<File[]>([]);
const { const {
register, register,
@@ -199,6 +200,39 @@ export function ContactForm() {
{errors.message && <p className="text-red-500 text-xs font-medium">{errors.message.message}</p>} {errors.message && <p className="text-red-500 text-xs font-medium">{errors.message.message}</p>}
</div> </div>
{/* Załączniki */}
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium text-primary">
Załącz zdjęcia <span className="text-text-muted font-normal">(opcjonalnie)</span>
</label>
<label className="flex flex-col items-center justify-center gap-2 border-2 border-dashed border-border rounded-xl p-6 cursor-pointer hover:border-accent hover:bg-accent/5 transition-colors">
<input
type="file"
multiple
accept="image/*,.pdf"
className="sr-only"
onChange={(e) => setFiles(Array.from(e.target.files || []))}
/>
<svg xmlns="http://www.w3.org/2000/svg" className="size-8 text-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 00-2 2z" />
</svg>
<span className="text-sm text-text-muted text-center">
Kliknij aby wybrać zdjęcia lub przeciągnij tutaj<br />
<span className="text-xs">JPG, PNG, WEBP, PDF — maks. 10 MB każdy</span>
</span>
{files.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{files.map((f, i) => (
<span key={i} className="text-xs bg-accent/10 text-accent px-2 py-1 rounded-md font-medium">
{f.name}
</span>
))}
</div>
)}
</label>
<p className="text-xs text-text-muted">Zdjęcia miejsca montażu znacznie przyspieszają wycenę.</p>
</div>
{/* RODO */} {/* RODO */}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-start gap-3 cursor-pointer" onClick={() => setConsent(!consent)}> <div className="flex items-start gap-3 cursor-pointer" onClick={() => setConsent(!consent)}>
+68 -16
View File
@@ -33,7 +33,7 @@ function calculatePower(m2: number, floor: string, sun: string, building: string
// Szacunkowy koszt montażu // Szacunkowy koszt montażu
function estimateInstall(pipes: string, rooms: number): { min: number; max: number } { function estimateInstall(pipes: string, rooms: number): { min: number; max: number } {
const base = rooms === 1 ? 1200 : rooms === 2 ? 2800 : rooms === 3 ? 4200 : 5500; const base = rooms === 1 ? 1400 : rooms === 2 ? 2400 : rooms === 3 ? 3200 : 3800;
const pipeExtra = pipes === 'short' ? 0 : pipes === 'medium' ? 300 : pipes === 'long' ? 700 : 1200; const pipeExtra = pipes === 'short' ? 0 : pipes === 'medium' ? 300 : pipes === 'long' ? 700 : 1200;
return { min: base + pipeExtra, max: base + pipeExtra + 600 }; return { min: base + pipeExtra, max: base + pipeExtra + 600 };
} }
@@ -332,7 +332,7 @@ export function KalkulatorClient({ products }: Props) {
</div> </div>
{deviceType === 'pompa' && !pumpFormSent && ( {deviceType === 'pompa' && !pumpFormSent && (
<div className="mt-4 p-5 bg-white border border-border rounded-2xl flex flex-col gap-4"> <div className="mt-4 p-5 bg-white border border-border rounded-2xl flex flex-col gap-4">
<p className="text-sm font-bold text-primary">Wypełnij formularz — oddzwonimy z bezpłatną wyceną</p> <p className="text-sm font-bold text-primary">Wypełnij formularz — oddzwonimy w celu ustalenia terminu bezpłatnej i niezobowiązującej wizji lokalnej</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div> <div>
<label className="text-xs font-medium text-primary mb-1 block">Imię i nazwisko *</label> <label className="text-xs font-medium text-primary mb-1 block">Imię i nazwisko *</label>
@@ -382,6 +382,9 @@ export function KalkulatorClient({ products }: Props) {
> >
Wyślij zapytanie o pompę ciepła Wyślij zapytanie o pompę ciepła
</button> </button>
<p className="text-xs text-text-muted text-center mt-2">
Wizja lokalna jest bezpłatna i niezobowiązująca. Oddzwaniamy w ciągu 24 godzin.
</p>
</div> </div>
)} )}
@@ -407,17 +410,30 @@ export function KalkulatorClient({ products }: Props) {
</div> </div>
</div> </div>
<div> <div className="flex flex-col gap-1.5">
<label className="text-sm font-semibold text-primary mb-3 block"> <div className="flex items-center justify-between">
Powierzchnia pomieszczenia: <span className="text-accent">{m2} m²</span> <span className="text-xs text-text-muted">Powierzchnia</span>
</label> <span className="text-xs font-bold text-primary bg-accent/10 px-2 py-0.5 rounded-md">{m2} m²</span>
</div>
<div className="relative h-5 flex items-center">
<div className="absolute w-full h-1.5 bg-slate-200 rounded-full" />
<div
className="absolute h-1.5 bg-accent rounded-full"
style={{ width: `${((m2 - 10) / 90) * 100}%` }}
/>
<input <input
type="range" min={10} max={100} step={5} value={m2} type="range" min={10} max={100} step={5} value={m2}
onChange={e => setM2(Number(e.target.value))} onChange={e => setM2(Number(e.target.value))}
className="w-full accent-accent" className="absolute w-full h-1.5 opacity-0 cursor-pointer z-10"
/> />
<div className="flex justify-between text-xs text-text-muted mt-1"> <div
<span>10 m²</span><span>100 m²</span> className="absolute size-4 bg-white border-2 border-accent rounded-full shadow-md pointer-events-none"
style={{ left: `calc(${((m2 - 10) / 90) * 100}% - 8px)` }}
/>
</div>
<div className="flex justify-between text-xs text-text-muted">
<span>10 m²</span>
<span>100 m²</span>
</div> </div>
<div className="mt-3 p-3 bg-blue-50 rounded-xl text-sm text-blue-700 font-medium"> <div className="mt-3 p-3 bg-blue-50 rounded-xl text-sm text-blue-700 font-medium">
Wymagana moc: ok. <strong>{requiredPower} kW</strong> Wymagana moc: ok. <strong>{requiredPower} kW</strong>
@@ -585,6 +601,30 @@ export function KalkulatorClient({ products }: Props) {
Szacowany koszt montażu dla Twojej konfiguracji: Szacowany koszt montażu dla Twojej konfiguracji:
<strong className="ml-1">{installCost.min.toLocaleString('pl-PL')}–{installCost.max.toLocaleString('pl-PL')} zł</strong> <strong className="ml-1">{installCost.min.toLocaleString('pl-PL')}–{installCost.max.toLocaleString('pl-PL')} zł</strong>
</div> </div>
<div className="mt-3 p-4 bg-blue-50 border border-blue-200 rounded-xl">
<p className="text-sm font-semibold text-blue-900 mb-1">📐 Chcesz dokładną wycenę?</p>
<p className="text-xs text-blue-800 leading-relaxed">
Skontaktuj się z nami — wycena jest <strong>bezpłatna i niezobowiązująca</strong>.
Aby przyspieszyć wycenę, wyślij nam zdjęcia:
</p>
<ul className="mt-2 flex flex-col gap-1">
{[
'Miejsca gdzie ma wisieć jednostka wewnętrzna',
'Ściany przez które przejdzie instalacja',
'Miejsca gdzie stanie jednostka zewnętrzna',
'Skrzynki elektrycznej (bezpieczniki)',
].map((item, i) => (
<li key={i} className="flex items-start gap-2 text-xs text-blue-800">
<span className="text-blue-500 shrink-0">→</span>
{item}
</li>
))}
</ul>
<a href="/kontakt" className="mt-3 inline-flex items-center gap-1.5 text-xs font-bold text-white bg-accent px-3 py-1.5 rounded-lg hover:bg-accent/90 transition-colors">
Wyślij zdjęcia i zapytaj o wycenę →
</a>
</div>
</div> </div>
)} )}
@@ -601,18 +641,30 @@ export function KalkulatorClient({ products }: Props) {
</div> </div>
</div> </div>
<div> <div className="flex flex-col gap-1.5">
<div className="flex justify-between items-center mb-3"> <div className="flex items-center justify-between">
<label className="text-sm font-semibold text-primary">Budżet całkowity</label> <span className="text-xs text-text-muted">Budżet</span>
<span className="text-lg font-extrabold text-accent font-heading">{budget.toLocaleString('pl-PL')} zł</span> <span className="text-xs font-bold text-primary bg-accent/10 px-2 py-0.5 rounded-md">{budget.toLocaleString('pl-PL')} zł</span>
</div> </div>
<div className="relative h-5 flex items-center">
<div className="absolute w-full h-1.5 bg-slate-200 rounded-full" />
<div
className="absolute h-1.5 bg-accent rounded-full"
style={{ width: `${((budget - 3000) / 22000) * 100}%` }}
/>
<input <input
type="range" min={3000} max={25000} step={500} value={budget} type="range" min={3000} max={25000} step={500} value={budget}
onChange={e => setBudget(Number(e.target.value))} onChange={e => setBudget(Number(e.target.value))}
className="w-full accent-accent" className="absolute w-full h-1.5 opacity-0 cursor-pointer z-10"
/> />
<div className="flex justify-between text-xs text-text-muted mt-1"> <div
<span>3 000 zł</span><span>25 000 zł</span> className="absolute size-4 bg-white border-2 border-accent rounded-full shadow-md pointer-events-none"
style={{ left: `calc(${((budget - 3000) / 22000) * 100}% - 8px)` }}
/>
</div>
<div className="flex justify-between text-xs text-text-muted">
<span>3 000 zł</span>
<span>25 000 zł</span>
</div> </div>
</div> </div>
+2 -2
View File
@@ -85,11 +85,11 @@ export function Footer() {
Informacje Informacje
</h3> </h3>
<ul className="flex flex-col gap-3 text-sm text-slate-300"> <ul className="flex flex-col gap-3 text-sm text-slate-300">
<li> {/* <li>
<Link href="/o-nas" className="hover:text-accent transition-colors"> <Link href="/o-nas" className="hover:text-accent transition-colors">
O nas O nas
</Link> </Link>
</li> </li> */}
<li> <li>
<Link href="/realizacje" className="hover:text-accent transition-colors"> <Link href="/realizacje" className="hover:text-accent transition-colors">
Realizacje (Portfolio) Realizacje (Portfolio)
+2 -2
View File
@@ -224,12 +224,12 @@ export function Header() {
> >
Blog Blog
</Link> </Link>
<Link {/* <Link
href="/o-nas" href="/o-nas"
className="text-sm font-medium text-primary hover:text-accent transition-colors" className="text-sm font-medium text-primary hover:text-accent transition-colors"
> >
O nas O nas
</Link> </Link> */}
<Link <Link
href="/kontakt" href="/kontakt"
className="text-sm font-medium text-primary hover:text-accent transition-colors" className="text-sm font-medium text-primary hover:text-accent transition-colors"
+2 -2
View File
@@ -205,7 +205,7 @@ export function MobileMenu({ isOpen, onClose }: MobileMenuProps) {
</div> </div>
{/* 6. O Nas */} {/* 6. O Nas */}
<div className="border-b border-border py-3"> {/* <div className="border-b border-border py-3">
<Link <Link
href="/o-nas" href="/o-nas"
onClick={onClose} onClick={onClose}
@@ -213,7 +213,7 @@ export function MobileMenu({ isOpen, onClose }: MobileMenuProps) {
> >
O nas O nas
</Link> </Link>
</div> </div> */}
{/* 7. Kontakt */} {/* 7. Kontakt */}
<div className="border-b border-border py-3"> <div className="border-b border-border py-3">
-45
View File
@@ -100,29 +100,6 @@ export const posts: Post[] = [
<p>Pamiętaj jednak, że pompa ciepła nie sprawdzi się w nieocieplonych, starych domach z tradycyjnymi grzejnikami. Potrzebujesz rzetelnego audytu? Skontaktuj się z nami, a doradzimy najlepsze rozwiązanie dla Twojego domu!</p> <p>Pamiętaj jednak, że pompa ciepła nie sprawdzi się w nieocieplonych, starych domach z tradycyjnymi grzejnikami. Potrzebujesz rzetelnego audytu? Skontaktuj się z nami, a doradzimy najlepsze rozwiązanie dla Twojego domu!</p>
` `
}, },
{
slug: 'dofinansowanie-czyste-powietrze-2026',
title: 'Czyste Powietrze 2026 — ile możesz dostać na pompę ciepła?',
category: 'pompy-ciepla',
excerpt: 'Program Czyste Powietrze w 2026 roku oferuje rekordowe dopłaty. Dowiedz się, kto może skorzystać, jakie są progi dochodowe i jak złożyć wniosek.',
date: '2026-02-02T10:00:00Z',
author: 'Tomasz Kowalski',
image: '',
featured: true,
content: `
<h2>Rekordowe dopłaty w programie Czyste Powietrze 2026</h2>
<p>Program Czyste Powietrze na rok 2026 przeszedł kilka ważnych modyfikacji, które ułatwiają inwestycję w odnawialne źródła energii. Jeśli planujesz wymianę "kopciucha" na nowoczesną pompę ciepła, to idealny moment na działanie.</p>
<h3>Progi dofinansowania</h3>
<p>Kwota, jaką możesz otrzymać, zależy od Twoich dochodów:</p>
<ul>
<li><strong>Poziom podstawowy:</strong> dla osób z rocznym dochodem do 135 000 zł.</li>
<li><strong>Poziom podwyższony:</strong> wyższa dotacja dla osób z niższym dochodem na członka rodziny.</li>
<li><strong>Poziom najwyższy:</strong> nawet do 100% kosztów (do 135 000 zł) dla najuboższych.</li>
</ul>
<h3>Jak uniknąć błędów?</h3>
<p>Najczęstszym błędem jest zakup urządzenia, które nie figuruje na liście ZUM (Zielonych Urządzeń i Materiałów). Przed opłaceniem faktury zawsze sprawdzaj certyfikaty. Nasi specjaliści pomagają nie tylko w doborze i montażu urządzeń, ale również w kompleksowym przygotowaniu wniosków o dotację. Chcesz wiedzieć więcej? Odezwij się do nas!</p>
`
},
{ {
slug: 'jak-dobrac-moc-klimatyzatora', slug: 'jak-dobrac-moc-klimatyzatora',
title: 'Jak dobrać moc klimatyzatora? Wzory i kalkulator dla każdego pomieszczenia', title: 'Jak dobrać moc klimatyzatora? Wzory i kalkulator dla każdego pomieszczenia',
@@ -382,28 +359,6 @@ export const posts: Post[] = [
<p>Dzięki inwerterom, klimatyzacja jest obecnie bardzo tania w eksploatacji. Sprawdź naszą ofertę inwerterowych systemów z certyfikatem A+++.</p> <p>Dzięki inwerterom, klimatyzacja jest obecnie bardzo tania w eksploatacji. Sprawdź naszą ofertę inwerterowych systemów z certyfikatem A+++.</p>
` `
}, },
{
slug: 'pompy-ciepla-dolny-slask-dofinansowanie',
title: 'Pompy ciepła na Dolnym Śląsku — dofinansowanie i realizacje',
category: 'pompy-ciepla',
excerpt: 'Sprawdź, jak działają pompy ciepła na terenie Dolnego Śląska, jakie dofinansowania lokalne możesz uzyskać i jak dbamy o profesjonalny montaż.',
date: '2026-04-25T10:00:00Z',
author: 'Tomasz Kowalski',
image: '',
content: `
<h2>Dolny Śląsk to idealny region dla pomp ciepła</h2>
<p>Dzięki stosunkowo łagodnym zimom na nizinach Dolnego Śląska (Wrocław, Legnica, Lubin), powietrzne pompy ciepła osiągają u nas fenomenalne wyniki wydajnościowe (SCOP) przez cały sezon grzewczy. Nawet w rejonach przedgórskich (Wałbrzych, Jelenia Góra) nowoczesne instalacje świetnie radzą sobie bez wspomagania grzałkami.</p>
<h3>Wsparcie finansowe dla mieszkańców</h3>
<p>Poza ogólnopolskim programem "Czyste Powietrze", wiele dolnośląskich gmin i miast (m.in. Wrocław z programem KAWKA Plus) oferuje dodatkowe dotacje na likwidację pieców węglowych. Połączenie lokalnych funduszy z rządowymi potrafi sfinansować nawet do 80-90% całej inwestycji.</p>
<h3>Zaufaj lokalnym ekspertom</h3>
<ul>
<li>Znamy specyfikę dolnośląskiego budownictwa.</li>
<li>Zapewniamy szybki serwis gwarancyjny (nasza baza to okolice Wrocławia/Legnicy).</li>
<li>Pomagamy z biurokracją we wrocławskim i lokalnych urzędach.</li>
</ul>
<p>Chcesz dołączyć do grona ekologicznych domów w naszym województwie? Zostaw kontakt, a przeanalizujemy Twoje możliwości dotacyjne!</p>
`
},
{ {
slug: 'klimatyzacja-legnica-montaz', slug: 'klimatyzacja-legnica-montaz',
title: 'Klimatyzacja Legnica — montaż split i multisplit w mieście i okolicach', title: 'Klimatyzacja Legnica — montaż split i multisplit w mieście i okolicach',