refactor: eliminate all hardcoded strings in TSX components, enforce project rules in AGENTS.md, lock body scroll in cookie modal
This commit is contained in:
@@ -16,6 +16,17 @@ export function CustomCookieBanner() {
|
||||
setChoices(state)
|
||||
}, [state])
|
||||
|
||||
// Lock background page scroll while settings modal is open
|
||||
useEffect(() => {
|
||||
if (showSettings) {
|
||||
const originalOverflow = document.body.style.overflow
|
||||
document.body.style.overflow = 'hidden'
|
||||
return () => {
|
||||
document.body.style.overflow = originalOverflow
|
||||
}
|
||||
}
|
||||
}, [showSettings])
|
||||
|
||||
if (decided) {
|
||||
return null
|
||||
}
|
||||
@@ -101,14 +112,21 @@ export function CustomCookieBanner() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 2. Modal Window for Cookie Settings (Centered & Scrollable) */}
|
||||
{/* 2. Modal Window for Cookie Settings (Scroll-locked backdrop & isolated modal scrolling) */}
|
||||
{showSettings && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 bg-black/80 backdrop-blur-xs overflow-y-auto">
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 bg-black/80 backdrop-blur-xs overflow-y-auto overscroll-contain"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) setShowSettings(false)
|
||||
}}
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={texts.settingsTitle}
|
||||
className="w-full max-w-2xl bg-[#1f1f1f] text-white p-6 sm:p-8 border-t-4 border-[#FFED00] shadow-2xl my-auto max-h-[90vh] flex flex-col"
|
||||
className="w-full max-w-2xl bg-[#1f1f1f] text-white p-6 sm:p-8 border-t-4 border-[#FFED00] shadow-2xl my-auto max-h-[90vh] flex flex-col overscroll-contain"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Modal Header */}
|
||||
<div className="flex items-center justify-between border-b border-[#333333] pb-4 mb-6 shrink-0">
|
||||
@@ -131,7 +149,7 @@ export function CustomCookieBanner() {
|
||||
</div>
|
||||
|
||||
{/* Scrollable Categories List */}
|
||||
<div className="overflow-y-auto flex-1 pr-1 space-y-3.5">
|
||||
<div className="overflow-y-auto flex-1 pr-1 space-y-3.5 overscroll-contain">
|
||||
{CONSENT_CATEGORIES.map((cat) => {
|
||||
const isChecked = cat === 'necessary' ? true : !!choices[cat]
|
||||
const isDisabled = cat === 'necessary'
|
||||
|
||||
+62
-50
@@ -17,10 +17,12 @@ interface FooterProps {
|
||||
phone?: string | null
|
||||
email?: string | null
|
||||
items?: FooterNavItem[]
|
||||
privacyHref?: string
|
||||
privacyLabel?: string
|
||||
cookieHref?: string
|
||||
cookieLabel?: string
|
||||
privacyHref?: string | null
|
||||
privacyLabel?: string | null
|
||||
cookieHref?: string | null
|
||||
cookieLabel?: string | null
|
||||
navTitle?: string | null
|
||||
contactTitle?: string | null
|
||||
}
|
||||
|
||||
export function Footer({
|
||||
@@ -33,10 +35,12 @@ export function Footer({
|
||||
phone,
|
||||
email,
|
||||
items,
|
||||
privacyHref = '/polityka-prywatnosci',
|
||||
privacyLabel = 'Polityka Prywatności',
|
||||
cookieHref = '/polityka-cookies',
|
||||
cookieLabel = 'Polityka Cookies',
|
||||
privacyHref,
|
||||
privacyLabel,
|
||||
cookieHref,
|
||||
cookieLabel,
|
||||
navTitle,
|
||||
contactTitle,
|
||||
}: FooterProps) {
|
||||
return (
|
||||
<footer className="bg-[#1f1f1f] text-[#d0d0d0] py-16 border-t-4 border-[#FFED00]">
|
||||
@@ -59,18 +63,22 @@ export function Footer({
|
||||
{footerTagline}
|
||||
</p>
|
||||
)}
|
||||
<div className="text-xs text-[#888888] font-mono space-y-1 pt-2 border-t border-[#333333]">
|
||||
{nip && <div>NIP: <span className="text-[#e0e0e0] font-semibold">{nip}</span></div>}
|
||||
{regon && <div>REGON: <span className="text-[#e0e0e0] font-semibold">{regon}</span></div>}
|
||||
</div>
|
||||
{(nip || regon) && (
|
||||
<div className="text-xs text-[#888888] font-mono space-y-1 pt-2 border-t border-[#333333]">
|
||||
{nip && <div>NIP: <span className="text-[#e0e0e0] font-semibold">{nip}</span></div>}
|
||||
{regon && <div>REGON: <span className="text-[#e0e0e0] font-semibold">{regon}</span></div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation Links */}
|
||||
{items && items.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<div className="text-xs font-black uppercase tracking-widest text-[#FFED00] border-b border-[#333333] pb-2">
|
||||
Nawigacja
|
||||
</div>
|
||||
{navTitle && (
|
||||
<div className="text-xs font-black uppercase tracking-widest text-[#FFED00] border-b border-[#333333] pb-2">
|
||||
{navTitle}
|
||||
</div>
|
||||
)}
|
||||
<ul className="space-y-2.5 text-xs font-bold uppercase tracking-wider">
|
||||
{items.map((item, idx) => (
|
||||
<li key={item.id || idx}>
|
||||
@@ -84,54 +92,58 @@ export function Footer({
|
||||
)}
|
||||
|
||||
{/* Contact Details */}
|
||||
<div className="space-y-4">
|
||||
<div className="text-xs font-black uppercase tracking-widest text-[#FFED00] border-b border-[#333333] pb-2">
|
||||
Kontakt
|
||||
{(phone || email || address || city) && (
|
||||
<div className="space-y-4">
|
||||
{contactTitle && (
|
||||
<div className="text-xs font-black uppercase tracking-widest text-[#FFED00] border-b border-[#333333] pb-2">
|
||||
{contactTitle}
|
||||
</div>
|
||||
)}
|
||||
<ul className="space-y-3 text-xs">
|
||||
{phone && (
|
||||
<li className="flex items-center gap-2.5">
|
||||
<Phone className="h-4 w-4 text-[#FFED00] shrink-0" />
|
||||
<a href={`tel:${phone.replace(/\s+/g, '')}`} className="text-white hover:text-[#FFED00] font-mono font-black text-sm">
|
||||
{phone}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{email && (
|
||||
<li className="flex items-center gap-2.5">
|
||||
<Mail className="h-4 w-4 text-[#FFED00] shrink-0" />
|
||||
<a href={`mailto:${email}`} className="text-[#d0d0d0] hover:text-white font-medium">
|
||||
{email}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{(address || city) && (
|
||||
<li className="flex items-start gap-2.5">
|
||||
<MapPin className="h-4 w-4 text-[#FFED00] shrink-0 mt-0.5" />
|
||||
<span className="text-[#a0a0a0]">
|
||||
{address}
|
||||
{address && city ? ', ' : ''}
|
||||
{city}
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<ul className="space-y-3 text-xs">
|
||||
{phone && (
|
||||
<li className="flex items-center gap-2.5">
|
||||
<Phone className="h-4 w-4 text-[#FFED00] shrink-0" />
|
||||
<a href={`tel:${phone.replace(/\s+/g, '')}`} className="text-white hover:text-[#FFED00] font-mono font-black text-sm">
|
||||
{phone}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{email && (
|
||||
<li className="flex items-center gap-2.5">
|
||||
<Mail className="h-4 w-4 text-[#FFED00] shrink-0" />
|
||||
<a href={`mailto:${email}`} className="text-[#d0d0d0] hover:text-white font-medium">
|
||||
{email}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{(address || city) && (
|
||||
<li className="flex items-start gap-2.5">
|
||||
<MapPin className="h-4 w-4 text-[#FFED00] shrink-0 mt-0.5" />
|
||||
<span className="text-[#a0a0a0]">
|
||||
{address}
|
||||
{address && city ? ', ' : ''}
|
||||
{city}
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom Bar with Privacy & Cookie Links */}
|
||||
<div className="mt-12 pt-8 border-t border-[#333333] flex flex-col sm:flex-row items-center justify-between gap-4 text-xs text-[#888888]">
|
||||
<div>
|
||||
© {new Date().getFullYear()} {companyName}. Wszelkie prawa zastrzeżone.
|
||||
© {new Date().getFullYear()} {companyName}
|
||||
</div>
|
||||
<div className="flex items-center gap-6 text-xs font-bold uppercase tracking-wider">
|
||||
{privacyHref && (
|
||||
{privacyHref && privacyLabel && (
|
||||
<a href={privacyHref} className="text-[#a0a0a0] hover:text-[#FFED00] transition-colors flex items-center gap-1.5">
|
||||
<ShieldCheck className="h-3.5 w-3.5 text-[#FFED00]" />
|
||||
<span>{privacyLabel}</span>
|
||||
</a>
|
||||
)}
|
||||
{cookieHref && (
|
||||
{cookieHref && cookieLabel && (
|
||||
<a href={cookieHref} className="text-[#a0a0a0] hover:text-[#FFED00] transition-colors flex items-center gap-1.5">
|
||||
<Cookie className="h-3.5 w-3.5 text-[#FFED00]" />
|
||||
<span>{cookieLabel}</span>
|
||||
|
||||
+53
-41
@@ -17,8 +17,9 @@ interface HeaderProps {
|
||||
phone?: string | null
|
||||
homeHref?: string
|
||||
currentLocale?: string
|
||||
contactHref?: string
|
||||
contactLabel?: string
|
||||
contactHref?: string | null
|
||||
contactLabel?: string | null
|
||||
phoneLabel?: string | null
|
||||
}
|
||||
|
||||
export function Header({
|
||||
@@ -28,8 +29,9 @@ export function Header({
|
||||
phone,
|
||||
homeHref = '/',
|
||||
currentLocale = 'pl',
|
||||
contactHref = '/kontakt',
|
||||
contactLabel = 'KONTAKT',
|
||||
contactHref,
|
||||
contactLabel,
|
||||
phoneLabel,
|
||||
}: HeaderProps) {
|
||||
const pathname = usePathname() || ''
|
||||
const [langDropdownOpen, setLangDropdownOpen] = useState(false)
|
||||
@@ -61,8 +63,8 @@ export function Header({
|
||||
)
|
||||
|
||||
const languages = [
|
||||
{ code: 'pl', label: 'PL - Polski' },
|
||||
{ code: 'en', label: 'UK - English' },
|
||||
{ code: 'pl', label: 'PL' },
|
||||
{ code: 'en', label: 'UK' },
|
||||
]
|
||||
|
||||
const activeLang = languages.find((l) => l.code === currentLocale) || languages[0]
|
||||
@@ -70,29 +72,35 @@ export function Header({
|
||||
return (
|
||||
<header className="sticky top-0 z-50 w-full bg-white border-b-2 border-[#1f1f1f] shadow-xs">
|
||||
{/* Top Black Utility Bar */}
|
||||
<div className="bg-[#1f1f1f] text-white text-xs py-2 hidden md:block">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-block w-2.5 h-2.5 bg-[#FFED00]" />
|
||||
<span className="font-bold uppercase tracking-wider text-[11px] text-[#e0e0e0]">
|
||||
{tagline || 'Profesjonalne Mycie i Renowacja Nawierzchni Brukowych'}
|
||||
</span>
|
||||
{(tagline || phone) && (
|
||||
<div className="bg-[#1f1f1f] text-white text-xs py-2 hidden md:block">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl flex items-center justify-between">
|
||||
{tagline && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-block w-2.5 h-2.5 bg-[#FFED00]" />
|
||||
<span className="font-bold uppercase tracking-wider text-[11px] text-[#e0e0e0]">
|
||||
{tagline}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{phone && (
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
{phoneLabel && (
|
||||
<span className="font-bold uppercase tracking-wider text-[11px] text-[#e0e0e0]">
|
||||
{phoneLabel}
|
||||
</span>
|
||||
)}
|
||||
<a
|
||||
href={`tel:${phone.replace(/\s+/g, '')}`}
|
||||
className="font-black uppercase tracking-wider text-[11px] text-[#FFED00] hover:underline font-mono"
|
||||
>
|
||||
{phone}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{phone && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold uppercase tracking-wider text-[11px] text-[#e0e0e0]">
|
||||
Infolinia / Bezpłatna wycena:
|
||||
</span>
|
||||
<a
|
||||
href={`tel:${phone.replace(/\s+/g, '')}`}
|
||||
className="font-black uppercase tracking-wider text-[11px] text-[#FFED00] hover:underline font-mono"
|
||||
>
|
||||
{phone}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Navigation Bar */}
|
||||
<div className="container mx-auto flex h-20 items-center justify-between px-4 sm:px-6 lg:px-12 max-w-7xl">
|
||||
@@ -106,13 +114,15 @@ export function Header({
|
||||
{companyName}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[10px] uppercase tracking-widest text-[#666666] font-bold block mt-1">
|
||||
Czyszczenie Kostki Brukowej
|
||||
</span>
|
||||
{tagline && (
|
||||
<span className="text-[10px] uppercase tracking-widest text-[#666666] font-bold block mt-1">
|
||||
{tagline}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{/* Navigation Items (Without duplicated Kontakt) */}
|
||||
{/* Navigation Items */}
|
||||
{filteredNavItems && filteredNavItems.length > 0 && (
|
||||
<nav className="hidden lg:flex items-center gap-8 text-xs font-black uppercase tracking-wider text-[#1f1f1f]">
|
||||
{filteredNavItems.map((item, idx) => (
|
||||
@@ -128,7 +138,7 @@ export function Header({
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Wysuwany przełącznik języków (Dropdown Language Switcher) */}
|
||||
{/* Wysuwany przełącznik języków */}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
@@ -141,7 +151,7 @@ export function Header({
|
||||
</button>
|
||||
|
||||
{langDropdownOpen && (
|
||||
<div className="absolute right-0 mt-1 w-36 bg-white border-2 border-[#1f1f1f] shadow-xl z-50 py-1">
|
||||
<div className="absolute right-0 mt-1 w-28 bg-white border-2 border-[#1f1f1f] shadow-xl z-50 py-1">
|
||||
{languages.map((lang) => (
|
||||
<a
|
||||
key={lang.code}
|
||||
@@ -161,14 +171,16 @@ export function Header({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right CTA Button: KONTAKT */}
|
||||
<a
|
||||
href={contactHref}
|
||||
className="inline-flex items-center gap-2 bg-[#FFED00] hover:bg-[#fae100] text-[#1f1f1f] border-2 border-[#1f1f1f] px-6 py-3 text-xs font-black uppercase tracking-widest transition-colors shadow-xs"
|
||||
>
|
||||
<span>{contactLabel}</span>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</a>
|
||||
{/* Right CTA Button */}
|
||||
{contactHref && contactLabel && (
|
||||
<a
|
||||
href={contactHref}
|
||||
className="inline-flex items-center gap-2 bg-[#FFED00] hover:bg-[#fae100] text-[#1f1f1f] border-2 border-[#1f1f1f] px-6 py-3 text-xs font-black uppercase tracking-widest transition-colors shadow-xs"
|
||||
>
|
||||
<span>{contactLabel}</span>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user