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:
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) {
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user