fix: linting cleanup, TS errors, mobile cookie banner and hero max-width

This commit is contained in:
MichalC
2026-08-01 12:27:14 +02:00
parent 02cb49e180
commit 88cb4c516d
49 changed files with 167 additions and 125 deletions
+25 -22
View File
@@ -37,13 +37,16 @@ export default function CookieBanner() {
useEffect(() => {
if (isOpen) {
setPrefs({
necessary: true,
analytics: analytics,
marketing: marketing,
advertising: advertising,
});
setShowDetails(false);
const timer = setTimeout(() => {
setPrefs({
necessary: true,
analytics: analytics,
marketing: marketing,
advertising: advertising,
});
setShowDetails(false);
}, 0);
return () => clearTimeout(timer);
}
}, [isOpen, analytics, marketing, advertising]);
@@ -54,17 +57,17 @@ export default function CookieBanner() {
if (!isInitialized || !isOpen) return null;
return (
<div className="fixed bottom-0 left-0 right-0 z-[100] p-4 sm:p-6">
<div className="max-w-4xl mx-auto bg-white rounded-2xl shadow-2xl border border-border overflow-hidden">
<div className="fixed bottom-0 left-0 right-0 z-[100] p-0 sm:p-6">
<div className="max-w-4xl mx-auto bg-white sm:rounded-2xl shadow-[0_-4px_20px_rgba(0,0,0,0.1)] sm:shadow-2xl border-t sm:border border-border overflow-hidden pb-safe sm:pb-0">
{/* Główny pasek */}
<div className="p-5 sm:p-6">
<div className="flex items-start gap-3 mb-4">
<span className="text-2xl shrink-0">🍪</span>
<div className="p-4 sm:p-6">
<div className="flex items-start gap-3 mb-3 sm:mb-4">
<span className="hidden sm:block text-2xl shrink-0">🍪</span>
<div>
<h2 className="text-base font-extrabold text-primary font-heading mb-1">
<h2 className="text-sm sm:text-base font-extrabold text-primary font-heading mb-1">
Ta strona używa plików cookie
</h2>
<p className="text-sm text-text-muted leading-relaxed">
<p className="text-xs sm:text-sm text-text-muted leading-relaxed">
Używamy plików cookie, aby zapewnić prawidłowe działanie strony, analizować ruch
i dostosowywać treści. Możesz wybrać które kategorie akceptujesz.{' '}
<Link href="/polityka-prywatnosci" className="text-accent underline hover:no-underline">
@@ -149,32 +152,32 @@ export default function CookieBanner() {
)}
{/* Przyciski */}
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
<div className="grid grid-cols-2 sm:flex sm:flex-row items-stretch sm:items-center gap-2">
<button
onClick={storeAcceptAll}
className="flex-1 sm:flex-none px-5 py-2.5 bg-accent text-white font-bold rounded-xl text-sm hover:bg-accent/90 transition-colors"
className="col-span-2 sm:col-span-1 flex-1 sm:flex-none px-4 py-2 sm:px-5 sm:py-2.5 bg-accent text-white font-bold rounded-xl text-xs sm:text-sm hover:bg-accent/90 transition-colors min-h-[44px]"
>
Akceptuj wszystkie
</button>
<button
onClick={storeRejectOptional}
className="flex-1 sm:flex-none px-5 py-2.5 bg-slate-100 text-primary font-bold rounded-xl text-sm hover:bg-slate-200 transition-colors"
className="col-span-1 flex-1 sm:flex-none px-4 py-2 sm:px-5 sm:py-2.5 bg-slate-100 text-primary font-bold rounded-xl text-xs sm:text-sm hover:bg-slate-200 transition-colors min-h-[44px]"
>
Odrzuć opcjonalne
Odrzuć
</button>
{showDetails ? (
<button
onClick={saveSelected}
className="flex-1 sm:flex-none px-5 py-2.5 bg-white border border-border text-primary font-bold rounded-xl text-sm hover:border-accent transition-colors"
className="col-span-1 flex-1 sm:flex-none px-4 py-2 sm:px-5 sm:py-2.5 bg-white border border-border text-primary font-bold rounded-xl text-xs sm:text-sm hover:border-accent transition-colors min-h-[44px]"
>
Zapisz wybór
Zapisz
</button>
) : (
<button
onClick={() => setShowDetails(true)}
className="flex-1 sm:flex-none px-5 py-2.5 bg-white border border-border text-primary font-bold rounded-xl text-sm hover:border-accent transition-colors"
className="col-span-1 flex-1 sm:flex-none px-4 py-2 sm:px-5 sm:py-2.5 bg-white border border-border text-primary font-bold rounded-xl text-xs sm:text-sm hover:border-accent transition-colors min-h-[44px]"
>
Dostosuj ustawienia
Dostosuj
</button>
)}
</div>
+2 -2
View File
@@ -1,8 +1,7 @@
'use client';
import { useState } from 'react';
import { Wrench, Zap, Info, Clock, Check, AlertTriangle, Phone, CheckCircle2 } from 'lucide-react';
import Link from 'next/link';
import { Wrench, Zap, Check, AlertTriangle, Phone, CheckCircle2 } from 'lucide-react';
import { companyDetails } from '@/content/company';
import { submitAwariaForm } from '@/app/zgloszenie-awarii/actions';
import { getRecaptchaToken } from '@/lib/getRecaptchaToken';
@@ -127,6 +126,7 @@ export function AwariaClient() {
setIsSubmitting(true);
const recaptchaToken = await getRecaptchaToken('submit');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const formDataObj: any = {
isUrgent,
brand,
+8 -36
View File
@@ -2,8 +2,8 @@
import { useState, useEffect, useMemo } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { ArrowRight, ArrowLeft, Check, Thermometer, Home, Zap, ShoppingCart, Phone, RotateCcw } from 'lucide-react';
import { ArrowRight, ArrowLeft, Check, Home, Zap, ShoppingCart, Phone } from 'lucide-react';
import { Product } from '@/content/products';
import { companyDetails } from '@/content/company';
import { ProductCard } from '@/components/shop/ProductCard';
@@ -20,9 +20,10 @@ function useSessionState<T>(key: string, initialValue: T) {
try {
const item = window.sessionStorage.getItem(key);
if (item) {
setState(JSON.parse(item));
const timer = setTimeout(() => setState(JSON.parse(item)), 0);
return () => clearTimeout(timer);
}
} catch (error) {}
} catch {}
}, [key]);
const setValue = (value: T | ((val: T) => T)) => {
@@ -32,7 +33,7 @@ function useSessionState<T>(key: string, initialValue: T) {
if (typeof window !== 'undefined') {
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {}
} catch {}
};
return [state, setValue] as const;
@@ -203,35 +204,6 @@ export function KalkulatorClient({ products }: Props) {
return pages;
};
const handleFinish = () => {
setStep(4);
setPage(1);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const handleReset = () => {
setStep(0);
setPage(1);
Object.keys(window.sessionStorage).forEach(key => {
if (key.startsWith('calc_')) window.sessionStorage.removeItem(key);
});
setDeviceType('klimatyzacja');
setBuilding('apartment');
setPomieszczenia([{ nazwa: 'Pokój 1', m2: 20 }]);
setFloor('middle');
setSun('south');
setPipes('medium');
setInstallMethod('korytko');
setOutdoorUnitLocation('balcony');
setHasSocket('yes');
setBudget(8000);
setPreferredBrand('');
setEnergyClass('');
setMaxNoise(null);
setMaxNoiseOut(null);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const handleNext = () => {
setStep(s => s + 1);
setPage(1);
@@ -621,7 +593,7 @@ export function KalkulatorClient({ products }: Props) {
{ value: 'facade', label: 'Elewacja / balkon francuski', sub: 'Wymaga użycia podnośnika koszowego (ok. 600 zł/h) i pompki skroplin (+ ok. 400 zł)' },
{ value: 'roof', label: 'Dach budynku', sub: 'Wymaga odpowiedniego stelaża lub konstrukcji dachowej' },
].map(o => (
<button key={o.value} onClick={() => setOutdoorUnitLocation(o.value as any)}
<button key={o.value} onClick={() => setOutdoorUnitLocation(o.value as 'balcony' | 'facade' | 'roof')}
className={`p-4 rounded-xl border-2 text-left transition-all ${outdoorUnitLocation === o.value ? 'border-accent bg-accent/5' : 'border-border hover:border-accent'}`}>
<p className="text-sm font-bold text-primary">{o.label}</p>
<p className="text-xs text-text-muted">{o.sub}</p>
@@ -643,7 +615,7 @@ export function KalkulatorClient({ products }: Props) {
{ value: 'no', label: 'Nie', sub: 'Instalator musi dociągnąć prąd z wewnątrz (+ ok. 200 zł)' },
{ value: 'dont_know', label: 'Nie wiem / Do ustalenia', sub: 'Do ustalenia w trakcie wizji lokalnej' },
].map(o => (
<button key={o.value} onClick={() => setHasSocket(o.value as any)}
<button key={o.value} onClick={() => setHasSocket(o.value as 'yes' | 'no' | 'dont_know')}
className={`p-4 rounded-xl border-2 text-left transition-all ${hasSocket === o.value ? 'border-accent bg-accent/5' : 'border-border hover:border-accent'}`}>
<p className="text-sm font-bold text-primary">{o.label}</p>
<p className="text-xs text-text-muted">{o.sub}</p>
+3 -3
View File
@@ -1,6 +1,6 @@
"use client";
import { useState, useRef, useEffect, useMemo } from "react";
import { useState, useMemo } from "react";
import { Search } from "lucide-react";
import { getProducts } from "@/content/products";
import Link from "next/link";
@@ -85,7 +85,7 @@ export function GlobalSearch() {
>
<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'}
src={product.image || '/images/about/about-story.webp'}
alt={product.name}
fill
className="object-contain p-1 mix-blend-multiply"
@@ -104,7 +104,7 @@ export function GlobalSearch() {
</div>
) : (
<div className="p-8 text-center text-text-muted">
Brak wyników dla <span className="font-semibold">"{query}"</span>
Brak wyników dla <span className="font-semibold">&quot;{query}&quot;</span>
</div>
)}
</div>
+6 -3
View File
@@ -2,7 +2,7 @@
import { useState, useEffect } from "react";
import Link from "next/link";
import { User, ShoppingBag, Menu, Phone, Wrench } from "lucide-react";
import { ShoppingBag, Menu, Phone, Wrench } from "lucide-react";
import { NavDropdown } from "./NavDropdown";
import { GlobalSearch } from "./GlobalSearch";
import { MobileMenu } from "./MobileMenu";
@@ -20,7 +20,7 @@ export function Header() {
const cartItems = useCartStore((state) => state.items);
useEffect(() => {
setMounted(true);
const t = setTimeout(() => setMounted(true), 0);
const handleScroll = () => {
if (window.scrollY > 40) {
setIsScrolled(true);
@@ -30,7 +30,10 @@ export function Header() {
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
clearTimeout(t);
};
}, []);
const cartCount = mounted
+1 -1
View File
@@ -12,7 +12,7 @@ export function AboutStory() {
<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"
src="/images/about/about-story.webp"
alt="Zespół firmy w trakcie pracy"
fill
className="object-cover"
+1 -1
View File
@@ -1,7 +1,7 @@
import React from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { Wind, LayoutGrid, Sparkles, ArrowRight } from 'lucide-react';
import { Wind, LayoutGrid, ArrowRight } from 'lucide-react';
import { cn } from '@/lib/utils';
+1 -1
View File
@@ -1,4 +1,4 @@
import { CheckCircle2, Zap, ShieldCheck, Banknote } from "lucide-react";
import { CheckCircle2, Zap, ShieldCheck } from "lucide-react";
export function AdvantagesBar() {
const advantages = [
+1 -1
View File
@@ -1,6 +1,6 @@
import Link from "next/link";
import Image from "next/image";
import { ArrowRight, Calendar, User } from "lucide-react";
import { ArrowRight, Calendar } from "lucide-react";
import { SectionHeading } from "../common/SectionHeading";
import { getLatestPosts } from "@/content/posts";
+1 -1
View File
@@ -1,5 +1,5 @@
import React from 'react';
import Image from 'next/image';
import Link from 'next/link';
const brands = [
+1 -1
View File
@@ -1,5 +1,5 @@
import React from 'react';
import Image from 'next/image';
import { SectionHeading } from '../common/SectionHeading';
import { companyDetails } from '@/content/company';
+1 -1
View File
@@ -32,7 +32,7 @@ export function FeaturedPost({ post }: FeaturedPostProps) {
{/* 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'}
src={post.image || '/images/hero/hero-section-image.webp'}
alt={post.title}
fill
className="object-cover transition-transform duration-700 group-hover:scale-105"
+1 -1
View File
@@ -68,7 +68,7 @@ export const FundingSection = () => {
<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>Dotacja &quot;Czyste Powietrze&quot;</span>
<span>- 20 500 zł</span>
</div>
<div className="flex justify-between pb-4 border-b border-gray-100 text-green-600 font-medium">
+6 -1
View File
@@ -31,7 +31,7 @@ export function HeroSection({
features
}: HeroSectionProps = {}) {
return (
<section className="relative bg-primary min-h-[480px] md:min-h-[600px] flex items-center overflow-hidden">
<section className="relative bg-primary min-h-[480px] md:min-h-[600px] flex items-center overflow-hidden max-w-[2560px] mx-auto">
{/* TODO ZDJĘCIE HERO: realne zdjęcie firmy/montażu, format poziomy min. 1920×1080, plik public/hero.jpg */}
<Image
src={backgroundImage}
@@ -85,6 +85,11 @@ export function HeroSection({
<ArrowRight className="size-5 group-hover:translate-x-1 transition-transform" />
</Link>
)}
{secondaryCtaText && secondaryCtaLink && (
<Link href={secondaryCtaLink} className="w-fit sm:w-auto btn btn-outline bg-white/10 text-white border-white/20 hover:bg-white hover:text-primary font-bold px-8">
<span>{secondaryCtaText}</span>
</Link>
)}
</div>
+1 -1
View File
@@ -1,5 +1,5 @@
import Link from "next/link";
import { ThermometerSun, Home, ChevronRight, Wrench, ShieldCheck, ArrowRight } from "lucide-react";
import { ThermometerSun, Home, ChevronRight, ArrowRight } from "lucide-react";
export function PillarsSection() {
return (
+1 -1
View File
@@ -88,7 +88,7 @@ export function PostsGrid({ posts, featuredPost }: PostsGridProps) {
>
<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'}
src={post.image || '/images/hero/hero-section-image.webp'}
alt={post.title}
fill
className="object-cover transition-transform duration-700 group-hover:scale-105"
+1 -1
View File
@@ -1,6 +1,6 @@
import React from 'react';
import { pricingData, PricingItem } from '@/content/pricing';
import { Check, Info, ArrowRight, Download } from 'lucide-react';
import { Check, Download } from 'lucide-react';
export const PricingTable = () => {
const splitItems = pricingData.filter((item) => item.category === 'split');
+2 -2
View File
@@ -2,10 +2,10 @@
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';
import { MapPin } from 'lucide-react';
type Category = 'wszystkie' | Project['category'];
+1 -1
View File
@@ -63,7 +63,7 @@ export function Testimonials() {
<Star key={j} className="size-4 fill-yellow-400 text-yellow-400" />
))}
</div>
<p className="text-sm text-text-muted leading-relaxed flex-1">"{t.text}"</p>
<p className="text-sm text-text-muted leading-relaxed flex-1">&quot;{t.text}&quot;</p>
<div className="pt-3 border-t border-border">
<p className="text-sm font-bold text-primary">{t.name}</p>
<p className="text-xs text-text-muted">{t.city}</p>
+1 -1
View File
@@ -1,7 +1,7 @@
'use client';
import { useState } from 'react';
import { Check, Loader2, CheckCircle2 } from 'lucide-react';
import { Loader2, CheckCircle2 } from 'lucide-react';
import { submitMultisplitForm } from '@/app/zapytanie-multisplit/actions';
import { getRecaptchaToken } from '@/lib/getRecaptchaToken';
+1 -1
View File
@@ -2,7 +2,7 @@
import Link from 'next/link';
import Image from 'next/image';
import { ShoppingCart, GitCompare, Phone, Check } from 'lucide-react';
import { ShoppingCart, GitCompare, Phone } from 'lucide-react';
import { Product } from '@/content/products';
import { useCartStore } from '@/lib/cart-store';
import { useCompareStore } from '@/lib/compare-store';
+2 -2
View File
@@ -10,12 +10,12 @@ interface ProductGalleryProps {
}
export function ProductGallery({ image, name, images }: ProductGalleryProps) {
const [mainImage, setMainImage] = useState(image || 'https://images.unsplash.com/photo-1581094794329-c8112a89af12?w=800&q=80');
const [mainImage, setMainImage] = useState(image || '/images/hero/shop-section-image.webp');
// 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'];
: [image, '/images/hero/shop-section-image.webp'];
return (
<div className="flex flex-col gap-4">
+3 -3
View File
@@ -4,8 +4,8 @@ import { useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import {
ArrowLeft, Phone, ShoppingCart, GitCompare, Check,
Thermometer, Volume2, Zap, Wind, Star, Info, Eye, X
Phone, ShoppingCart, GitCompare, Check,
Thermometer, Volume2, Zap, Wind, Star, Info, Eye, X, type LucideIcon
} from 'lucide-react';
import { Product } from '@/content/products';
import { useCartStore } from '@/lib/cart-store';
@@ -114,7 +114,7 @@ export function ProductPageClient({ product, seriesProducts, companyPhone, compa
currentProduct.specs['Hałas wewnątrz (dB)'] && { icon: Volume2, label: 'Hałas wewnątrz', value: `${currentProduct.specs['Hałas wewnątrz (dB)']} dB`, color: 'text-purple-600 bg-purple-50' },
currentProduct.specs['Czynnik chłodniczy'] && { icon: Wind, label: 'Czynnik', value: currentProduct.specs['Czynnik chłodniczy'], color: 'text-teal-600 bg-teal-50' },
currentProduct.specs['Grzanie do'] && { icon: Zap, label: 'Grzanie do', value: `-${currentProduct.specs['Grzanie do']}°C`, color: 'text-accent bg-accent-light' },
].filter(Boolean) as { icon: any; label: string; value: string; color: string }[];
].filter(Boolean) as { icon: LucideIcon; label: string; value: string; color: string }[];
return (
<main className="bg-slate-50 min-h-screen pb-16">
+15 -12
View File
@@ -96,14 +96,17 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
useEffect(() => {
setSearchQuery(urlSearch);
setSelectedBrands(urlBrand ? [urlBrand] : []);
if (urlCategory !== 'all') {
setCategory(urlCategory as 'scienne' | 'multisplit' | 'podlogowe' | 'jednostka-scienna');
} else {
setCategory('all');
}
setPage(1);
const timer = setTimeout(() => {
setSearchQuery(urlSearch);
setSelectedBrands(urlBrand ? [urlBrand] : []);
if (urlCategory !== 'all') {
setCategory(urlCategory as 'scienne' | 'multisplit' | 'podlogowe' | 'jednostka-scienna');
} else {
setCategory('all');
}
setPage(1);
}, 0);
return () => clearTimeout(timer);
}, [urlSearch, urlBrand, urlCategory]);
const toggleBrand = (b: string) => {
@@ -171,7 +174,7 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
return true;
});
}, [products, category, selectedBrands, priceMin, priceMax, selectedRooms, halasMin, halasMax, selectedColors]);
}, [products, category, selectedBrands, priceMin, priceMax, selectedRooms, halasMin, halasMax, selectedColors, searchQuery]);
const totalPages = Math.ceil(filtered.length / PER_PAGE);
const paginated = filtered.slice((page - 1) * PER_PAGE, page * PER_PAGE);
@@ -217,7 +220,7 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
const hasFilters = category !== 'all' || selectedBrands.length > 0 || searchQuery !== '' || priceMin || priceMax || selectedRooms.length > 0 || selectedColors.length > 0 || halasMin > 15 || halasMax < 50;
const Sidebar = () => (
const sidebarContent = (
<aside className="w-full">
{/* Klimatyzatory - typ */}
<Accordion title="Klimatyzatory">
@@ -452,7 +455,7 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
<X className="size-5 text-text-muted" />
</button>
</div>
<Sidebar />
{sidebarContent}
</div>
</div>
)}
@@ -469,7 +472,7 @@ export function ProductsGrid({ products, brands }: ProductsGridProps) {
<X className="size-3" /> Wyczyść filtry
</button>
)}
<Sidebar />
{sidebarContent}
</div>
</div>