fix(cookies & seo): refactor cookie settings to centered scrollable modal, add OpenGraph share image SEO metadata, and optimize hero LCP image

This commit is contained in:
2026-08-26 03:05:00 +02:00
parent 3353cee64c
commit 8c6f3d2633
5 changed files with 175 additions and 88 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

@@ -23,7 +23,49 @@ const pageMetadata = createPageMetadata({
export async function generateMetadata({ params }: PageProps): Promise<Metadata> { export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { locale, slug } = await params const { locale, slug } = await params
return pageMetadata({ payload: await getCachedPayload(), locale, slug: slug ?? [] }) const payload = await getCachedPayload()
const baseMeta = await pageMetadata({ payload, locale, slug: slug ?? [] })
const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000'
const fallbackOgImage = `${baseUrl}/images/paving-hero.webp`
const ogImages =
baseMeta.openGraph?.images && (baseMeta.openGraph.images as unknown[]).length > 0
? baseMeta.openGraph.images
: [
{
url: fallbackOgImage,
width: 1920,
height: 1080,
alt: 'POI Clean House - Czyszczenie Kostki Brukowej',
},
]
return {
...baseMeta,
title: baseMeta.title || 'POI Clean House | Profesjonalne Czyszczenie Kostki Brukowej',
description:
baseMeta.description ||
'Specjalistyczne mycie gorącowodne do 100°C, odplamianie oleju, piaskowanie i impregnacja hydrofobowa kostki brukowej. Sprawdź bezpłatną wycenę!',
openGraph: {
...baseMeta.openGraph,
title: (baseMeta.title as string) || 'POI Clean House | Profesjonalne Czyszczenie Kostki Brukowej',
description:
baseMeta.description ||
'Specjalistyczne mycie gorącowodne do 100°C, odplamianie oleju, piaskowanie i impregnacja hydrofobowa kostki brukowej. Sprawdź bezpłatną wycenę!',
images: ogImages,
type: 'website',
siteName: 'POI Clean House',
},
twitter: {
card: 'summary_large_image',
title: (baseMeta.title as string) || 'POI Clean House | Profesjonalne Czyszczenie Kostki Brukowej',
description:
baseMeta.description ||
'Specjalistyczne mycie gorącowodne do 100°C, odplamianie oleju, piaskowanie i impregnacja hydrofobowa kostki brukowej.',
images: [fallbackOgImage],
},
}
} }
export default async function Page({ params, searchParams }: PageProps) { export default async function Page({ params, searchParams }: PageProps) {
+14 -8
View File
@@ -1,4 +1,5 @@
import React from 'react' import React from 'react'
import Image from 'next/image'
import { Phone, ArrowRight, Check } from 'lucide-react' import { Phone, ArrowRight, Check } from 'lucide-react'
import { AnimateIn } from '@/components/AnimateIn' import { AnimateIn } from '@/components/AnimateIn'
@@ -31,15 +32,20 @@ export function HeroBlockComponent(props: HeroBlockProps) {
return ( return (
<div className="w-full"> <div className="w-full">
{/* Hero Banner Section with Background Photo */} {/* Hero Banner Section with Optimized Next.js LCP Image */}
<section <section className="relative w-full min-h-[calc(100vh-8rem)] max-h-[780px] flex items-center overflow-hidden">
className="relative w-full min-h-[calc(100vh-8rem)] max-h-[780px] flex items-center bg-cover bg-center overflow-hidden" {/* Next.js LCP High Priority Image */}
style={{ <Image
backgroundImage: "url('/images/paving-hero.jpg')", src="/images/paving-hero.webp"
}} alt="Profesjonalne czyszczenie i renowacja kostki brukowej"
> fill
priority
sizes="100vw"
className="object-cover object-center z-0"
/>
{/* Dark gradient overlay for text readability */} {/* Dark gradient overlay for text readability */}
<div className="absolute inset-0 bg-gradient-to-r from-[#141414]/90 via-[#141414]/65 to-black/20" /> <div className="absolute inset-0 bg-gradient-to-r from-[#141414]/90 via-[#141414]/65 to-black/20 z-[1]" />
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl relative z-10 py-10 lg:py-14"> <div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl relative z-10 py-10 lg:py-14">
{/* Compact Architectural Content Card */} {/* Compact Architectural Content Card */}
+20 -12
View File
@@ -15,22 +15,30 @@ export function AnimateIn({
delay = 0, delay = 0,
direction = 'up', direction = 'up',
className = '', className = '',
threshold = 0.15, threshold = 0,
}: AnimateInProps) { }: AnimateInProps) {
const [isVisible, setIsVisible] = useState(false) const [isVisible, setIsVisible] = useState(false)
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
useEffect(() => { useEffect(() => {
// If IntersectionObserver is not supported, show immediately
if (typeof window === 'undefined' || !('IntersectionObserver' in window)) {
setIsVisible(true)
return
}
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
([entry]) => { ([entry]) => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
setIsVisible(true) setIsVisible(true)
observer.unobserve(entry.target) if (ref.current) {
observer.unobserve(ref.current)
}
} }
}, },
{ {
threshold, threshold,
rootMargin: '0px 0px -40px 0px', rootMargin: '0px 0px 150px 0px',
} }
) )
@@ -47,20 +55,20 @@ export function AnimateIn({
}, [threshold]) }, [threshold])
const getTransform = () => { const getTransform = () => {
if (isVisible) return 'translate3d(0, 0, 0) scale(1)' if (isVisible) return 'translate3d(0, 0, 0)'
switch (direction) { switch (direction) {
case 'up': case 'up':
return 'translate3d(0, 32px, 0)' return 'translate3d(0, 24px, 0)'
case 'down': case 'down':
return 'translate3d(0, -32px, 0)' return 'translate3d(0, -24px, 0)'
case 'left': case 'left':
return 'translate3d(32px, 0, 0)' return 'translate3d(24px, 0, 0)'
case 'right': case 'right':
return 'translate3d(-32px, 0, 0)' return 'translate3d(-24px, 0, 0)'
case 'none': case 'none':
return 'scale(0.96)' return 'scale(0.98)'
default: default:
return 'translate3d(0, 32px, 0)' return 'translate3d(0, 24px, 0)'
} }
} }
@@ -68,11 +76,11 @@ export function AnimateIn({
<div <div
ref={ref} ref={ref}
style={{ style={{
transitionDuration: '700ms', transitionDuration: '600ms',
transitionTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)', transitionTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
transitionDelay: `${delay}ms`, transitionDelay: `${delay}ms`,
transform: getTransform(), transform: getTransform(),
opacity: isVisible ? 1 : 0, opacity: isVisible ? 1 : 0.05,
}} }}
className={`transition-all ${className}`} className={`transition-all ${className}`}
> >
+98 -67
View File
@@ -28,80 +28,110 @@ export function CustomCookieBanner() {
})) }))
} }
return ( const handleSave = () => {
<div savePreferences(choices)
role="dialog" setShowSettings(false)
aria-label="Cookie consent" }
className="fixed bottom-0 inset-x-0 z-50 bg-[#1f1f1f] text-white p-5 sm:p-6 border-t-4 border-[#FFED00] shadow-2xl max-h-[85vh] overflow-y-auto"
>
<div className="container mx-auto max-w-4xl">
{!showSettings ? (
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-5">
<div className="flex items-start gap-3.5">
<div className="p-2 bg-[#FFED00] text-[#1f1f1f] shrink-0 border border-[#1f1f1f]">
<Cookie className="h-5 w-5" />
</div>
<div className="text-xs sm:text-sm text-[#e0e0e0] leading-relaxed">
<p>
{texts.message}{' '}
{texts.privacyLink && (
<a
href={texts.privacyLink.href}
className="text-[#FFED00] underline hover:no-underline font-bold"
>
{texts.privacyLink.label}
</a>
)}
</p>
</div>
</div>
<div className="flex flex-wrap items-center gap-2.5 shrink-0"> const handleAcceptAll = () => {
<button acceptAll()
type="button" setShowSettings(false)
onClick={() => setShowSettings(true)} }
className="bg-[#2b2b2b] hover:bg-[#383838] text-white border border-[#444444] px-4 py-2.5 text-xs font-bold uppercase tracking-wider transition-colors cursor-pointer"
> const handleRejectAll = () => {
{texts.buttons.settings} rejectAll()
</button> setShowSettings(false)
<button }
type="button"
onClick={rejectAll} return (
className="bg-[#2b2b2b] hover:bg-[#383838] text-white border border-[#444444] px-4 py-2.5 text-xs font-bold uppercase tracking-wider transition-colors cursor-pointer" <>
> {/* 1. Compact Bottom Banner */}
{texts.buttons.reject} {!showSettings && (
</button> <div
<button role="dialog"
type="button" aria-label="Cookie consent"
onClick={acceptAll} className="fixed bottom-0 inset-x-0 z-50 bg-[#1f1f1f] text-white p-4 sm:p-6 border-t-4 border-[#FFED00] shadow-2xl"
className="bg-[#FFED00] hover:bg-[#fae100] text-[#1f1f1f] border-2 border-[#1f1f1f] px-6 py-2.5 text-xs font-black uppercase tracking-widest transition-all cursor-pointer shadow-xs" >
> <div className="container mx-auto max-w-5xl">
{texts.buttons.acceptAll} <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-5">
</button> <div className="flex items-start gap-3.5">
<div className="p-2 bg-[#FFED00] text-[#1f1f1f] shrink-0 border border-[#1f1f1f]">
<Cookie className="h-5 w-5" />
</div>
<div className="text-xs sm:text-sm text-[#e0e0e0] leading-relaxed">
<p>
{texts.message}{' '}
{texts.privacyLink && (
<a
href={texts.privacyLink.href}
className="text-[#FFED00] underline hover:no-underline font-bold"
>
{texts.privacyLink.label}
</a>
)}
</p>
</div>
</div>
<div className="flex flex-wrap items-center gap-2.5 shrink-0">
<button
type="button"
onClick={() => setShowSettings(true)}
className="bg-[#2b2b2b] hover:bg-[#383838] text-white border border-[#444444] px-4 py-2.5 text-xs font-bold uppercase tracking-wider transition-colors cursor-pointer"
>
{texts.buttons.settings}
</button>
<button
type="button"
onClick={handleRejectAll}
className="bg-[#2b2b2b] hover:bg-[#383838] text-white border border-[#444444] px-4 py-2.5 text-xs font-bold uppercase tracking-wider transition-colors cursor-pointer"
>
{texts.buttons.reject}
</button>
<button
type="button"
onClick={handleAcceptAll}
className="bg-[#FFED00] hover:bg-[#fae100] text-[#1f1f1f] border-2 border-[#1f1f1f] px-6 py-2.5 text-xs font-black uppercase tracking-widest transition-all cursor-pointer shadow-xs"
>
{texts.buttons.acceptAll}
</button>
</div>
</div> </div>
</div> </div>
) : ( </div>
<div className="space-y-5"> )}
<div className="flex items-center justify-between border-b border-[#333333] pb-3">
{/* 2. Modal Window for Cookie Settings (Centered & Scrollable) */}
{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
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"
>
{/* Modal Header */}
<div className="flex items-center justify-between border-b border-[#333333] pb-4 mb-6 shrink-0">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-1.5 bg-[#FFED00] text-[#1f1f1f]"> <div className="p-1.5 bg-[#FFED00] text-[#1f1f1f]">
<Cookie className="h-5 w-5" /> <Cookie className="h-5 w-5" />
</div> </div>
<h2 className="text-sm sm:text-base font-black uppercase tracking-wider text-white"> <h2 className="text-base sm:text-lg font-black uppercase tracking-wider text-white">
{texts.settingsTitle} {texts.settingsTitle}
</h2> </h2>
</div> </div>
<button <button
type="button" type="button"
onClick={() => setShowSettings(false)} onClick={() => setShowSettings(false)}
className="text-[#a0a0a0] hover:text-white p-1" className="text-[#a0a0a0] hover:text-white p-1.5 cursor-pointer transition-colors"
aria-label="Zamknij ustawienia" aria-label="Zamknij"
> >
<X className="h-5 w-5" /> <X className="h-5 w-5" />
</button> </button>
</div> </div>
<ul className="space-y-3"> {/* Scrollable Categories List */}
<div className="overflow-y-auto flex-1 pr-1 space-y-3.5">
{CONSENT_CATEGORIES.map((cat) => { {CONSENT_CATEGORIES.map((cat) => {
const isChecked = cat === 'necessary' ? true : !!choices[cat] const isChecked = cat === 'necessary' ? true : !!choices[cat]
const isDisabled = cat === 'necessary' const isDisabled = cat === 'necessary'
@@ -110,18 +140,18 @@ export function CustomCookieBanner() {
if (!catData) return null if (!catData) return null
return ( return (
<li <div
key={cat} key={cat}
onClick={() => !isDisabled && toggle(cat)} onClick={() => !isDisabled && toggle(cat)}
className={`flex items-start justify-between gap-4 p-3.5 border border-[#333333] bg-[#262626] ${ className={`flex items-start justify-between gap-4 p-4 border border-[#333333] bg-[#262626] ${
!isDisabled ? 'cursor-pointer hover:border-[#FFED00]' : 'opacity-90' !isDisabled ? 'cursor-pointer hover:border-[#FFED00]' : 'opacity-90'
} transition-colors`} } transition-colors`}
> >
<div className="pr-4"> <div className="pr-4 select-none">
<div className="text-xs font-black uppercase tracking-wider text-[#FFED00]"> <div className="text-xs font-black uppercase tracking-wider text-[#FFED00]">
{catData.title} {catData.title}
</div> </div>
<p className="text-xs text-[#a0a0a0] mt-0.5 leading-relaxed"> <p className="text-xs text-[#a0a0a0] mt-1 leading-relaxed">
{catData.description} {catData.description}
</p> </p>
</div> </div>
@@ -140,12 +170,13 @@ export function CustomCookieBanner() {
/> />
</div> </div>
</div> </div>
</li> </div>
) )
})} })}
</ul> </div>
<div className="flex justify-end gap-3 pt-2"> {/* Modal Footer Buttons */}
<div className="flex flex-wrap items-center justify-end gap-3 pt-6 mt-6 border-t border-[#333333] shrink-0">
<button <button
type="button" type="button"
onClick={() => setShowSettings(false)} onClick={() => setShowSettings(false)}
@@ -155,15 +186,15 @@ export function CustomCookieBanner() {
</button> </button>
<button <button
type="button" type="button"
onClick={() => savePreferences(choices)} onClick={handleSave}
className="bg-[#FFED00] hover:bg-[#fae100] text-[#1f1f1f] border-2 border-[#1f1f1f] px-6 py-2.5 text-xs font-black uppercase tracking-widest transition-all cursor-pointer shadow-xs" className="bg-[#FFED00] hover:bg-[#fae100] text-[#1f1f1f] border-2 border-[#1f1f1f] px-6 py-2.5 text-xs font-black uppercase tracking-widest transition-all cursor-pointer shadow-xs"
> >
{texts.buttons.save} {texts.buttons.save}
</button> </button>
</div> </div>
</div> </div>
)} </div>
</div> )}
</div> </>
) )
} }