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:
@@ -7,3 +7,26 @@ This version has breaking changes — APIs, conventions, and file structure may
|
|||||||
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
|
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
|
||||||
|
|
||||||
<!-- END:nextjs-agent-rules -->
|
<!-- END:nextjs-agent-rules -->
|
||||||
|
|
||||||
|
# POI Clean House - Project Rules & Guidelines
|
||||||
|
|
||||||
|
## 1. ABSOLUTELY NO HARDCODED STRINGS IN TSX/COMPONENT FILES
|
||||||
|
- **Strict Rule:** Never hardcode text strings, button labels, contact details, headings, descriptions, footers, headers, badges, or placeholder texts in `.tsx` components or page files.
|
||||||
|
- **Dynamic Source:** All content MUST come dynamically from Payload CMS (collections, globals like `Company` and `Navigation`, block fields, seed script, or `@intecion/ipal-kit`).
|
||||||
|
- If a prop is optional, render it conditionally only when present (`{prop && <span>{prop}</span>}`), NEVER provide fallback hardcoded Polish or English text inside JSX.
|
||||||
|
|
||||||
|
## 2. BRANDING & UI GUIDELINES (KÄRCHER STYLE)
|
||||||
|
- Colors:
|
||||||
|
- Primary Yellow: `#FFED00` (Hover: `#FAE100`)
|
||||||
|
- Dark Anthracite: `#1F1F1F`
|
||||||
|
- Deep Black: `#141414`
|
||||||
|
- Industrial Light Gray: `#F4F4F4`
|
||||||
|
- Architecture:
|
||||||
|
- Sharp, geometric corners (`rounded-none` / `0px radius`). No bubbly rounded-3xl or generic AI circles.
|
||||||
|
- Checkboxes: Square with 2px black border, yellow `#FFED00` checked background, and thick black checkmark (`stroke-[3]`).
|
||||||
|
- High contrast typography: Uppercase, bold/black weights with letter-spacing.
|
||||||
|
|
||||||
|
## 3. UX & ACCESSIBILITY
|
||||||
|
- Modals & Overlays:
|
||||||
|
- Must lock background scrolling (`document.body.style.overflow = 'hidden'`) when open to prevent background page scroll leakage.
|
||||||
|
- Must support close on button, Escape key, or backdrop click.
|
||||||
|
|||||||
@@ -18,54 +18,12 @@ interface PageProps {
|
|||||||
|
|
||||||
const pageMetadata = createPageMetadata({
|
const pageMetadata = createPageMetadata({
|
||||||
config: i18nConfig,
|
config: i18nConfig,
|
||||||
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000',
|
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,
|
||||||
})
|
})
|
||||||
|
|
||||||
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
|
||||||
const payload = await getCachedPayload()
|
return pageMetadata({ payload: await getCachedPayload(), locale, slug: slug ?? [] })
|
||||||
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) {
|
||||||
|
|||||||
@@ -97,6 +97,11 @@ export default async function LocaleLayout({ children, params }: LayoutProps) {
|
|||||||
href: resolveNavItemHref(item),
|
href: resolveNavItemHref(item),
|
||||||
})) || []
|
})) || []
|
||||||
|
|
||||||
|
const contactHref =
|
||||||
|
navigation?.ctaButton?.url ||
|
||||||
|
(navigation?.ctaButton?.page ? resolveNavItemHref(navigation.ctaButton) : undefined)
|
||||||
|
const contactLabel = navigation?.ctaButton?.label
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale}>
|
<html lang={locale}>
|
||||||
<body className="min-h-screen flex flex-col bg-white text-[#222222] antialiased selection:bg-[#FFED00] selection:text-[#1f1f1f]">
|
<body className="min-h-screen flex flex-col bg-white text-[#222222] antialiased selection:bg-[#FFED00] selection:text-[#1f1f1f]">
|
||||||
@@ -106,10 +111,11 @@ export default async function LocaleLayout({ children, params }: LayoutProps) {
|
|||||||
tagline={navigation?.tagline}
|
tagline={navigation?.tagline}
|
||||||
items={headerItems}
|
items={headerItems}
|
||||||
phone={company?.phone}
|
phone={company?.phone}
|
||||||
|
phoneLabel={(navigation as { phoneLabel?: string })?.phoneLabel}
|
||||||
homeHref={homeHref}
|
homeHref={homeHref}
|
||||||
currentLocale={locale}
|
currentLocale={locale}
|
||||||
contactHref={`/${locale}/kontakt`}
|
contactHref={contactHref}
|
||||||
contactLabel="KONTAKT"
|
contactLabel={contactLabel}
|
||||||
/>
|
/>
|
||||||
<main className="flex-1">{children}</main>
|
<main className="flex-1">{children}</main>
|
||||||
<Footer
|
<Footer
|
||||||
@@ -123,7 +129,11 @@ export default async function LocaleLayout({ children, params }: LayoutProps) {
|
|||||||
email={company?.email}
|
email={company?.email}
|
||||||
items={footerItems}
|
items={footerItems}
|
||||||
privacyHref={privacyHref}
|
privacyHref={privacyHref}
|
||||||
|
privacyLabel={texts.privacyLink?.label}
|
||||||
cookieHref={cookieHref}
|
cookieHref={cookieHref}
|
||||||
|
cookieLabel={cookiePage && typeof cookiePage === 'object' && 'title' in cookiePage ? (cookiePage.title as string) : undefined}
|
||||||
|
navTitle={(navigation as { navTitle?: string })?.navTitle}
|
||||||
|
contactTitle={(navigation as { contactTitle?: string })?.contactTitle}
|
||||||
/>
|
/>
|
||||||
<CustomCookieBanner />
|
<CustomCookieBanner />
|
||||||
<CookieButton className="fixed bottom-4 left-4 z-40 bg-[#1f1f1f] hover:bg-[#FFED00] text-[#FFED00] hover:text-[#1f1f1f] border-2 border-[#FFED00] p-2.5 transition-all shadow-lg cursor-pointer" />
|
<CookieButton className="fixed bottom-4 left-4 z-40 bg-[#1f1f1f] hover:bg-[#FFED00] text-[#FFED00] hover:text-[#1f1f1f] border-2 border-[#FFED00] p-2.5 transition-all shadow-lg cursor-pointer" />
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ interface BlogListBlockProps {
|
|||||||
badge?: string
|
badge?: string
|
||||||
heading: string
|
heading: string
|
||||||
subheading?: string
|
subheading?: string
|
||||||
|
readMoreLabel?: string
|
||||||
articles?: Article[]
|
articles?: Article[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,10 +80,12 @@ export function BlogListBlockComponent(props: BlogListBlockProps) {
|
|||||||
{art.date}
|
{art.date}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{props.readMoreLabel && (
|
||||||
<div className="text-xs font-black uppercase tracking-wider text-[#1f1f1f] flex items-center gap-1 hover:underline cursor-pointer whitespace-nowrap">
|
<div className="text-xs font-black uppercase tracking-wider text-[#1f1f1f] flex items-center gap-1 hover:underline cursor-pointer whitespace-nowrap">
|
||||||
<span>Czytaj poradnik</span>
|
<span>{props.readMoreLabel}</span>
|
||||||
<ArrowRight className="h-3.5 w-3.5 text-[#1f1f1f]" />
|
<ArrowRight className="h-3.5 w-3.5 text-[#1f1f1f]" />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</AnimateIn>
|
</AnimateIn>
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ export const BlogListBlock: Block = {
|
|||||||
defaultValue:
|
defaultValue:
|
||||||
'Praktyczne wskazówki ekspertów: jak usuwać trudne plamy, kiedy impregnować nawierzchnię i dlaczego temperatura wody decyduje o skuteczności czyszczenia.',
|
'Praktyczne wskazówki ekspertów: jak usuwać trudne plamy, kiedy impregnować nawierzchnię i dlaczego temperatura wody decyduje o skuteczności czyszczenia.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'readMoreLabel',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
defaultValue: 'Czytaj poradnik',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'articles',
|
name: 'articles',
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
|||||||
@@ -9,10 +9,20 @@ interface FormBlockProps {
|
|||||||
heading: string
|
heading: string
|
||||||
subheading?: string
|
subheading?: string
|
||||||
phone?: string
|
phone?: string
|
||||||
|
directPhoneLabel?: string
|
||||||
email?: string
|
email?: string
|
||||||
|
emailContactLabel?: string
|
||||||
workingHours?: string
|
workingHours?: string
|
||||||
|
workingHoursLabel?: string
|
||||||
|
nameLabel?: string
|
||||||
|
phoneInputLabel?: string
|
||||||
|
locationLabel?: string
|
||||||
|
areaLabel?: string
|
||||||
|
messageLabel?: string
|
||||||
submitButtonText?: string
|
submitButtonText?: string
|
||||||
consentText?: string
|
consentText?: string
|
||||||
|
successHeading?: string
|
||||||
|
successMessage?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FormBlockComponent(props: FormBlockProps) {
|
export function FormBlockComponent(props: FormBlockProps) {
|
||||||
@@ -21,10 +31,20 @@ export function FormBlockComponent(props: FormBlockProps) {
|
|||||||
heading,
|
heading,
|
||||||
subheading,
|
subheading,
|
||||||
phone,
|
phone,
|
||||||
|
directPhoneLabel,
|
||||||
email,
|
email,
|
||||||
|
emailContactLabel,
|
||||||
workingHours,
|
workingHours,
|
||||||
submitButtonText = 'Wyślij zapytanie o bezpłatną wycenę',
|
workingHoursLabel,
|
||||||
consentText = 'Wyrażam zgodę na przetwarzanie moich danych osobowych w celu kontaktu i przygotowania oferty zgodnie z Polityką Prywatności.',
|
nameLabel,
|
||||||
|
phoneInputLabel,
|
||||||
|
locationLabel,
|
||||||
|
areaLabel,
|
||||||
|
messageLabel,
|
||||||
|
submitButtonText,
|
||||||
|
consentText,
|
||||||
|
successHeading,
|
||||||
|
successMessage,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const [submitted, setSubmitted] = useState(false)
|
const [submitted, setSubmitted] = useState(false)
|
||||||
@@ -73,8 +93,15 @@ export function FormBlockComponent(props: FormBlockProps) {
|
|||||||
<Phone className="h-5 w-5" />
|
<Phone className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[11px] font-black uppercase tracking-wider text-[#666666]">Telefon bezpośredni</div>
|
{directPhoneLabel && (
|
||||||
<a href={`tel:${phone.replace(/\s+/g, '')}`} className="text-base font-black text-[#1f1f1f] font-mono hover:underline">
|
<div className="text-[11px] font-black uppercase tracking-wider text-[#666666]">
|
||||||
|
{directPhoneLabel}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<a
|
||||||
|
href={`tel:${phone.replace(/\s+/g, '')}`}
|
||||||
|
className="text-base font-black text-[#1f1f1f] font-mono hover:underline"
|
||||||
|
>
|
||||||
{phone}
|
{phone}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,7 +116,11 @@ export function FormBlockComponent(props: FormBlockProps) {
|
|||||||
<Mail className="h-5 w-5" />
|
<Mail className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[11px] font-black uppercase tracking-wider text-[#666666]">Kontakt e-mail</div>
|
{emailContactLabel && (
|
||||||
|
<div className="text-[11px] font-black uppercase tracking-wider text-[#666666]">
|
||||||
|
{emailContactLabel}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<a href={`mailto:${email}`} className="text-sm font-bold text-[#1f1f1f] hover:underline">
|
<a href={`mailto:${email}`} className="text-sm font-bold text-[#1f1f1f] hover:underline">
|
||||||
{email}
|
{email}
|
||||||
</a>
|
</a>
|
||||||
@@ -105,7 +136,11 @@ export function FormBlockComponent(props: FormBlockProps) {
|
|||||||
<Clock className="h-5 w-5" />
|
<Clock className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[11px] font-black uppercase tracking-wider text-[#666666]">Godziny pracy</div>
|
{workingHoursLabel && (
|
||||||
|
<div className="text-[11px] font-black uppercase tracking-wider text-[#666666]">
|
||||||
|
{workingHoursLabel}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="text-sm font-bold text-[#1f1f1f]">
|
<div className="text-sm font-bold text-[#1f1f1f]">
|
||||||
{workingHours}
|
{workingHours}
|
||||||
</div>
|
</div>
|
||||||
@@ -125,35 +160,41 @@ export function FormBlockComponent(props: FormBlockProps) {
|
|||||||
<div className="flex h-14 w-14 items-center justify-center bg-[#FFED00] text-[#1f1f1f] border-2 border-[#1f1f1f] mx-auto">
|
<div className="flex h-14 w-14 items-center justify-center bg-[#FFED00] text-[#1f1f1f] border-2 border-[#1f1f1f] mx-auto">
|
||||||
<Check className="h-8 w-8 font-black" />
|
<Check className="h-8 w-8 font-black" />
|
||||||
</div>
|
</div>
|
||||||
|
{successHeading && (
|
||||||
<h3 className="text-xl font-black uppercase text-[#1f1f1f]">
|
<h3 className="text-xl font-black uppercase text-[#1f1f1f]">
|
||||||
Zgłoszenie zostało wysłane
|
{successHeading}
|
||||||
</h3>
|
</h3>
|
||||||
|
)}
|
||||||
|
{successMessage && (
|
||||||
<p className="text-[#666666] text-sm max-w-md mx-auto leading-relaxed">
|
<p className="text-[#666666] text-sm max-w-md mx-auto leading-relaxed">
|
||||||
Nasz doradca techniczny skontaktuje się z Państwem telefonicznie w celu przygotowania dokładnej wyceny.
|
{successMessage}
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<form onSubmit={handleSubmit} className="space-y-5">
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||||
<div>
|
<div>
|
||||||
|
{nameLabel && (
|
||||||
<label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2">
|
<label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2">
|
||||||
Imię i Nazwisko *
|
{nameLabel}
|
||||||
</label>
|
</label>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
placeholder="Jan Kowalski"
|
|
||||||
className="w-full border-2 border-[#1f1f1f] bg-white px-4 py-3.5 text-sm text-[#1f1f1f] focus:bg-[#fffde6] focus:outline-none transition-colors"
|
className="w-full border-2 border-[#1f1f1f] bg-white px-4 py-3.5 text-sm text-[#1f1f1f] focus:bg-[#fffde6] focus:outline-none transition-colors"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
{phoneInputLabel && (
|
||||||
<label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2">
|
<label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2">
|
||||||
Numer Telefonu *
|
{phoneInputLabel}
|
||||||
</label>
|
</label>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
required
|
required
|
||||||
placeholder="+48 600 000 000"
|
|
||||||
className="w-full border-2 border-[#1f1f1f] bg-white px-4 py-3.5 text-sm text-[#1f1f1f] focus:bg-[#fffde6] focus:outline-none transition-colors font-mono"
|
className="w-full border-2 border-[#1f1f1f] bg-white px-4 py-3.5 text-sm text-[#1f1f1f] focus:bg-[#fffde6] focus:outline-none transition-colors font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -161,39 +202,43 @@ export function FormBlockComponent(props: FormBlockProps) {
|
|||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||||
<div>
|
<div>
|
||||||
|
{locationLabel && (
|
||||||
<label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2">
|
<label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2">
|
||||||
Miejscowość
|
{locationLabel}
|
||||||
</label>
|
</label>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="np. Opole, Brzeg, Nysa"
|
|
||||||
className="w-full border-2 border-[#1f1f1f] bg-white px-4 py-3.5 text-sm text-[#1f1f1f] focus:border-[#FFED00] focus:outline-none transition-colors"
|
className="w-full border-2 border-[#1f1f1f] bg-white px-4 py-3.5 text-sm text-[#1f1f1f] focus:border-[#FFED00] focus:outline-none transition-colors"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
{areaLabel && (
|
||||||
<label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2">
|
<label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2">
|
||||||
Szacowany Metraż (m²)
|
{areaLabel}
|
||||||
</label>
|
</label>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="np. 180 m²"
|
|
||||||
className="w-full border-2 border-[#1f1f1f] bg-white px-4 py-3.5 text-sm text-[#1f1f1f] focus:border-[#FFED00] focus:outline-none transition-colors font-mono"
|
className="w-full border-2 border-[#1f1f1f] bg-white px-4 py-3.5 text-sm text-[#1f1f1f] focus:border-[#FFED00] focus:outline-none transition-colors font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
{messageLabel && (
|
||||||
<label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2">
|
<label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2">
|
||||||
Zakres prac / stan nawierzchni
|
{messageLabel}
|
||||||
</label>
|
</label>
|
||||||
|
)}
|
||||||
<textarea
|
<textarea
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="Np. usuwanie mchu z fug, mycie gorącowodne Kärcher/Mosmatic, odplamianie oleju, impregnacja..."
|
|
||||||
className="w-full border-2 border-[#1f1f1f] bg-white px-4 py-3.5 text-sm text-[#1f1f1f] focus:border-[#FFED00] focus:outline-none transition-colors"
|
className="w-full border-2 border-[#1f1f1f] bg-white px-4 py-3.5 text-sm text-[#1f1f1f] focus:border-[#FFED00] focus:outline-none transition-colors"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Custom Styled Kärcher Checkbox */}
|
{/* Custom Styled Kärcher Checkbox */}
|
||||||
|
{consentText && (
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<label htmlFor="consent" className="flex items-start gap-3 cursor-pointer select-none group">
|
<label htmlFor="consent" className="flex items-start gap-3 cursor-pointer select-none group">
|
||||||
<div className="relative flex items-center justify-center mt-0.5 shrink-0">
|
<div className="relative flex items-center justify-center mt-0.5 shrink-0">
|
||||||
@@ -206,12 +251,16 @@ export function FormBlockComponent(props: FormBlockProps) {
|
|||||||
onChange={(e) => setConsentChecked(e.target.checked)}
|
onChange={(e) => setConsentChecked(e.target.checked)}
|
||||||
className="sr-only"
|
className="sr-only"
|
||||||
/>
|
/>
|
||||||
<div className={`w-5 h-5 border-2 border-[#1f1f1f] transition-all flex items-center justify-center ${
|
<div
|
||||||
|
className={`w-5 h-5 border-2 border-[#1f1f1f] transition-all flex items-center justify-center ${
|
||||||
consentChecked ? 'bg-[#FFED00]' : 'bg-white'
|
consentChecked ? 'bg-[#FFED00]' : 'bg-white'
|
||||||
}`}>
|
}`}
|
||||||
<Check className={`w-3.5 h-3.5 text-[#1f1f1f] stroke-[3] transition-opacity ${
|
>
|
||||||
|
<Check
|
||||||
|
className={`w-3.5 h-3.5 text-[#1f1f1f] stroke-[3] transition-opacity ${
|
||||||
consentChecked ? 'opacity-100' : 'opacity-0'
|
consentChecked ? 'opacity-100' : 'opacity-0'
|
||||||
}`} />
|
}`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-[#555555] leading-relaxed group-hover:text-[#1f1f1f] transition-colors">
|
<span className="text-xs text-[#555555] leading-relaxed group-hover:text-[#1f1f1f] transition-colors">
|
||||||
@@ -219,15 +268,18 @@ export function FormBlockComponent(props: FormBlockProps) {
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{submitButtonText && (
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading || !consentChecked}
|
disabled={loading || !consentChecked}
|
||||||
className="w-full inline-flex items-center justify-center gap-3 bg-[#FFED00] hover:bg-[#fae100] border-2 border-[#1f1f1f] py-4 px-8 text-xs font-black uppercase tracking-widest text-[#1f1f1f] transition-all cursor-pointer disabled:opacity-50 shadow-xs hover:-translate-y-0.5"
|
className="w-full inline-flex items-center justify-center gap-3 bg-[#FFED00] hover:bg-[#fae100] border-2 border-[#1f1f1f] py-4 px-8 text-xs font-black uppercase tracking-widest text-[#1f1f1f] transition-all cursor-pointer disabled:opacity-50 shadow-xs hover:-translate-y-0.5"
|
||||||
>
|
>
|
||||||
<Send className="h-4 w-4" />
|
<Send className="h-4 w-4" />
|
||||||
<span>{loading ? 'Wysyłanie...' : submitButtonText}</span>
|
<span>{submitButtonText}</span>
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,16 +28,64 @@ export const FormBlock: Block = {
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
localized: true,
|
localized: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'directPhoneLabel',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
defaultValue: 'Telefon bezpośredni',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'email',
|
name: 'email',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
localized: true,
|
localized: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'emailContactLabel',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
defaultValue: 'Kontakt e-mail',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'workingHours',
|
name: 'workingHours',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
localized: true,
|
localized: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'workingHoursLabel',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
defaultValue: 'Godziny pracy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nameLabel',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
defaultValue: 'Imię i Nazwisko *',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'phoneInputLabel',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
defaultValue: 'Numer Telefonu *',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'locationLabel',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
defaultValue: 'Miejscowość',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'areaLabel',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
defaultValue: 'Szacowany Metraż (m²)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'messageLabel',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
defaultValue: 'Zakres prac / stan nawierzchni',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'submitButtonText',
|
name: 'submitButtonText',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@@ -48,7 +96,21 @@ export const FormBlock: Block = {
|
|||||||
name: 'consentText',
|
name: 'consentText',
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
localized: true,
|
localized: true,
|
||||||
defaultValue: 'Wyrażam zgodę na przetwarzanie moich danych osobowych w celu kontaktu i przygotowania oferty zgodnie z Polityką Prywatności.',
|
defaultValue:
|
||||||
|
'Wyrażam zgodę na przetwarzanie moich danych osobowych w celu kontaktu i przygotowania oferty zgodnie z Polityką Prywatności.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'successHeading',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
defaultValue: 'Zgłoszenie zostało wysłane',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'successMessage',
|
||||||
|
type: 'textarea',
|
||||||
|
localized: true,
|
||||||
|
defaultValue:
|
||||||
|
'Nasz doradca techniczny skontaktuje się z Państwem telefonicznie w celu przygotowania dokładnej wyceny.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,19 +25,18 @@ export function HeroBlockComponent(props: HeroBlockProps) {
|
|||||||
subheading,
|
subheading,
|
||||||
trustBadges,
|
trustBadges,
|
||||||
ctaPhone,
|
ctaPhone,
|
||||||
ctaPhoneLabel = 'Zadzwoń po bezpłatną wycenę',
|
ctaPhoneLabel,
|
||||||
ctaSecondaryLabel = 'Zobacz cennik za m²',
|
ctaSecondaryLabel,
|
||||||
ctaSecondaryUrl = '#cennik',
|
ctaSecondaryUrl,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{/* Hero Banner Section with Optimized Next.js LCP Image */}
|
{/* Hero Banner Section with Next.js Image */}
|
||||||
<section className="relative w-full min-h-[calc(100vh-8rem)] max-h-[780px] flex items-center overflow-hidden">
|
<section className="relative w-full min-h-[calc(100vh-8rem)] max-h-[780px] flex items-center overflow-hidden">
|
||||||
{/* Next.js LCP High Priority Image */}
|
|
||||||
<Image
|
<Image
|
||||||
src="/images/paving-hero.webp"
|
src="/images/paving-hero.webp"
|
||||||
alt="Profesjonalne czyszczenie i renowacja kostki brukowej"
|
alt={heading}
|
||||||
fill
|
fill
|
||||||
priority
|
priority
|
||||||
sizes="100vw"
|
sizes="100vw"
|
||||||
@@ -48,7 +47,7 @@ export function HeroBlockComponent(props: HeroBlockProps) {
|
|||||||
<div className="absolute inset-0 bg-gradient-to-r from-[#141414]/90 via-[#141414]/65 to-black/20 z-[1]" />
|
<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 */}
|
{/* Architectural Content Card */}
|
||||||
<AnimateIn delay={100} direction="right">
|
<AnimateIn delay={100} direction="right">
|
||||||
<div className="bg-white/95 backdrop-blur-md border-2 border-[#1f1f1f] p-6 sm:p-8 lg:p-10 max-w-lg shadow-2xl border-l-8 border-l-[#FFED00]">
|
<div className="bg-white/95 backdrop-blur-md border-2 border-[#1f1f1f] p-6 sm:p-8 lg:p-10 max-w-lg shadow-2xl border-l-8 border-l-[#FFED00]">
|
||||||
{badge && (
|
{badge && (
|
||||||
@@ -68,8 +67,9 @@ export function HeroBlockComponent(props: HeroBlockProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
|
{(ctaPhone || ctaSecondaryLabel) && (
|
||||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3">
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3">
|
||||||
{ctaPhone && (
|
{ctaPhone && ctaPhoneLabel && (
|
||||||
<a
|
<a
|
||||||
href={`tel:${ctaPhone.replace(/\s+/g, '')}`}
|
href={`tel:${ctaPhone.replace(/\s+/g, '')}`}
|
||||||
className="inline-flex items-center justify-center gap-2.5 bg-[#FFED00] hover:bg-[#fae100] text-[#1f1f1f] border-2 border-[#1f1f1f] px-6 py-3.5 text-xs font-black uppercase tracking-widest transition-all shadow-xs hover:-translate-y-0.5"
|
className="inline-flex items-center justify-center gap-2.5 bg-[#FFED00] hover:bg-[#fae100] text-[#1f1f1f] border-2 border-[#1f1f1f] px-6 py-3.5 text-xs font-black uppercase tracking-widest transition-all shadow-xs hover:-translate-y-0.5"
|
||||||
@@ -78,7 +78,7 @@ export function HeroBlockComponent(props: HeroBlockProps) {
|
|||||||
<span>{ctaPhoneLabel}</span>
|
<span>{ctaPhoneLabel}</span>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{ctaSecondaryLabel && (
|
{ctaSecondaryLabel && ctaSecondaryUrl && (
|
||||||
<a
|
<a
|
||||||
href={ctaSecondaryUrl}
|
href={ctaSecondaryUrl}
|
||||||
className="inline-flex items-center justify-center gap-2 bg-[#1f1f1f] hover:bg-[#333333] text-white px-6 py-3.5 text-xs font-black uppercase tracking-widest transition-all hover:-translate-y-0.5"
|
className="inline-flex items-center justify-center gap-2 bg-[#1f1f1f] hover:bg-[#333333] text-white px-6 py-3.5 text-xs font-black uppercase tracking-widest transition-all hover:-translate-y-0.5"
|
||||||
@@ -88,12 +88,13 @@ export function HeroBlockComponent(props: HeroBlockProps) {
|
|||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</AnimateIn>
|
</AnimateIn>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Full-Width Horizontal Feature Strip Directly Under Hero with Stagger */}
|
{/* Full-Width Horizontal Feature Strip Directly Under Hero */}
|
||||||
{trustBadges && trustBadges.length > 0 && (
|
{trustBadges && trustBadges.length > 0 && (
|
||||||
<div className="w-full bg-[#1f1f1f] text-white border-y-2 border-[#FFED00] shadow-md py-4 sm:py-5">
|
<div className="w-full bg-[#1f1f1f] text-white border-y-2 border-[#FFED00] shadow-md py-4 sm:py-5">
|
||||||
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl">
|
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl">
|
||||||
|
|||||||
@@ -80,6 +80,14 @@ export const HeroBlock: Block = {
|
|||||||
description: 'Strona docelowa drugiego przycisku',
|
description: 'Strona docelowa drugiego przycisku',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'ctaSecondaryUrl',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
admin: {
|
||||||
|
description: 'URL lub kotwica drugiego przycisku (np. #cennik)',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'backgroundImage',
|
name: 'backgroundImage',
|
||||||
type: 'upload',
|
type: 'upload',
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ interface Package {
|
|||||||
description?: string
|
description?: string
|
||||||
features?: Feature[]
|
features?: Feature[]
|
||||||
isPopular?: boolean
|
isPopular?: boolean
|
||||||
|
popularBadgeLabel?: string
|
||||||
ctaLabel?: string
|
ctaLabel?: string
|
||||||
ctaUrl?: string
|
ctaUrl?: string
|
||||||
}
|
}
|
||||||
@@ -57,9 +58,9 @@ export function PricingTableBlockComponent(props: PricingTableBlockProps) {
|
|||||||
pkg.isPopular ? 'border-t-8 border-t-[#FFED00] shadow-md' : ''
|
pkg.isPopular ? 'border-t-8 border-t-[#FFED00] shadow-md' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{pkg.isPopular && (
|
{pkg.isPopular && pkg.popularBadgeLabel && (
|
||||||
<div className="absolute top-0 right-0 bg-[#FFED00] text-[#1f1f1f] border-b-2 border-l-2 border-[#1f1f1f] text-[10px] font-black uppercase tracking-widest px-4 py-1">
|
<div className="absolute top-0 right-0 bg-[#FFED00] text-[#1f1f1f] border-b-2 border-l-2 border-[#1f1f1f] text-[10px] font-black uppercase tracking-widest px-4 py-1">
|
||||||
Polecany Pakiet
|
{pkg.popularBadgeLabel}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,12 @@ export const PricingTableBlock: Block = {
|
|||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'popularBadgeLabel',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
defaultValue: 'Polecany Pakiet',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'features',
|
name: 'features',
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
|||||||
@@ -16,6 +16,17 @@ export function CustomCookieBanner() {
|
|||||||
setChoices(state)
|
setChoices(state)
|
||||||
}, [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) {
|
if (decided) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -101,14 +112,21 @@ export function CustomCookieBanner() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 2. Modal Window for Cookie Settings (Centered & Scrollable) */}
|
{/* 2. Modal Window for Cookie Settings (Scroll-locked backdrop & isolated modal scrolling) */}
|
||||||
{showSettings && (
|
{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
|
<div
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label={texts.settingsTitle}
|
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 */}
|
{/* Modal Header */}
|
||||||
<div className="flex items-center justify-between border-b border-[#333333] pb-4 mb-6 shrink-0">
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Scrollable Categories List */}
|
{/* 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) => {
|
{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'
|
||||||
|
|||||||
+25
-13
@@ -17,10 +17,12 @@ interface FooterProps {
|
|||||||
phone?: string | null
|
phone?: string | null
|
||||||
email?: string | null
|
email?: string | null
|
||||||
items?: FooterNavItem[]
|
items?: FooterNavItem[]
|
||||||
privacyHref?: string
|
privacyHref?: string | null
|
||||||
privacyLabel?: string
|
privacyLabel?: string | null
|
||||||
cookieHref?: string
|
cookieHref?: string | null
|
||||||
cookieLabel?: string
|
cookieLabel?: string | null
|
||||||
|
navTitle?: string | null
|
||||||
|
contactTitle?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Footer({
|
export function Footer({
|
||||||
@@ -33,10 +35,12 @@ export function Footer({
|
|||||||
phone,
|
phone,
|
||||||
email,
|
email,
|
||||||
items,
|
items,
|
||||||
privacyHref = '/polityka-prywatnosci',
|
privacyHref,
|
||||||
privacyLabel = 'Polityka Prywatności',
|
privacyLabel,
|
||||||
cookieHref = '/polityka-cookies',
|
cookieHref,
|
||||||
cookieLabel = 'Polityka Cookies',
|
cookieLabel,
|
||||||
|
navTitle,
|
||||||
|
contactTitle,
|
||||||
}: FooterProps) {
|
}: FooterProps) {
|
||||||
return (
|
return (
|
||||||
<footer className="bg-[#1f1f1f] text-[#d0d0d0] py-16 border-t-4 border-[#FFED00]">
|
<footer className="bg-[#1f1f1f] text-[#d0d0d0] py-16 border-t-4 border-[#FFED00]">
|
||||||
@@ -59,18 +63,22 @@ export function Footer({
|
|||||||
{footerTagline}
|
{footerTagline}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{(nip || regon) && (
|
||||||
<div className="text-xs text-[#888888] font-mono space-y-1 pt-2 border-t border-[#333333]">
|
<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>}
|
{nip && <div>NIP: <span className="text-[#e0e0e0] font-semibold">{nip}</span></div>}
|
||||||
{regon && <div>REGON: <span className="text-[#e0e0e0] font-semibold">{regon}</span></div>}
|
{regon && <div>REGON: <span className="text-[#e0e0e0] font-semibold">{regon}</span></div>}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation Links */}
|
{/* Navigation Links */}
|
||||||
{items && items.length > 0 && (
|
{items && items.length > 0 && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{navTitle && (
|
||||||
<div className="text-xs font-black uppercase tracking-widest text-[#FFED00] border-b border-[#333333] pb-2">
|
<div className="text-xs font-black uppercase tracking-widest text-[#FFED00] border-b border-[#333333] pb-2">
|
||||||
Nawigacja
|
{navTitle}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<ul className="space-y-2.5 text-xs font-bold uppercase tracking-wider">
|
<ul className="space-y-2.5 text-xs font-bold uppercase tracking-wider">
|
||||||
{items.map((item, idx) => (
|
{items.map((item, idx) => (
|
||||||
<li key={item.id || idx}>
|
<li key={item.id || idx}>
|
||||||
@@ -84,10 +92,13 @@ export function Footer({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Contact Details */}
|
{/* Contact Details */}
|
||||||
|
{(phone || email || address || city) && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{contactTitle && (
|
||||||
<div className="text-xs font-black uppercase tracking-widest text-[#FFED00] border-b border-[#333333] pb-2">
|
<div className="text-xs font-black uppercase tracking-widest text-[#FFED00] border-b border-[#333333] pb-2">
|
||||||
Kontakt
|
{contactTitle}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<ul className="space-y-3 text-xs">
|
<ul className="space-y-3 text-xs">
|
||||||
{phone && (
|
{phone && (
|
||||||
<li className="flex items-center gap-2.5">
|
<li className="flex items-center gap-2.5">
|
||||||
@@ -117,21 +128,22 @@ export function Footer({
|
|||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom Bar with Privacy & Cookie Links */}
|
{/* 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 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>
|
<div>
|
||||||
© {new Date().getFullYear()} {companyName}. Wszelkie prawa zastrzeżone.
|
© {new Date().getFullYear()} {companyName}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-6 text-xs font-bold uppercase tracking-wider">
|
<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">
|
<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]" />
|
<ShieldCheck className="h-3.5 w-3.5 text-[#FFED00]" />
|
||||||
<span>{privacyLabel}</span>
|
<span>{privacyLabel}</span>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{cookieHref && (
|
{cookieHref && cookieLabel && (
|
||||||
<a href={cookieHref} className="text-[#a0a0a0] hover:text-[#FFED00] transition-colors flex items-center gap-1.5">
|
<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]" />
|
<Cookie className="h-3.5 w-3.5 text-[#FFED00]" />
|
||||||
<span>{cookieLabel}</span>
|
<span>{cookieLabel}</span>
|
||||||
|
|||||||
+26
-14
@@ -17,8 +17,9 @@ interface HeaderProps {
|
|||||||
phone?: string | null
|
phone?: string | null
|
||||||
homeHref?: string
|
homeHref?: string
|
||||||
currentLocale?: string
|
currentLocale?: string
|
||||||
contactHref?: string
|
contactHref?: string | null
|
||||||
contactLabel?: string
|
contactLabel?: string | null
|
||||||
|
phoneLabel?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Header({
|
export function Header({
|
||||||
@@ -28,8 +29,9 @@ export function Header({
|
|||||||
phone,
|
phone,
|
||||||
homeHref = '/',
|
homeHref = '/',
|
||||||
currentLocale = 'pl',
|
currentLocale = 'pl',
|
||||||
contactHref = '/kontakt',
|
contactHref,
|
||||||
contactLabel = 'KONTAKT',
|
contactLabel,
|
||||||
|
phoneLabel,
|
||||||
}: HeaderProps) {
|
}: HeaderProps) {
|
||||||
const pathname = usePathname() || ''
|
const pathname = usePathname() || ''
|
||||||
const [langDropdownOpen, setLangDropdownOpen] = useState(false)
|
const [langDropdownOpen, setLangDropdownOpen] = useState(false)
|
||||||
@@ -61,8 +63,8 @@ export function Header({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const languages = [
|
const languages = [
|
||||||
{ code: 'pl', label: 'PL - Polski' },
|
{ code: 'pl', label: 'PL' },
|
||||||
{ code: 'en', label: 'UK - English' },
|
{ code: 'en', label: 'UK' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const activeLang = languages.find((l) => l.code === currentLocale) || languages[0]
|
const activeLang = languages.find((l) => l.code === currentLocale) || languages[0]
|
||||||
@@ -70,19 +72,24 @@ export function Header({
|
|||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-50 w-full bg-white border-b-2 border-[#1f1f1f] shadow-xs">
|
<header className="sticky top-0 z-50 w-full bg-white border-b-2 border-[#1f1f1f] shadow-xs">
|
||||||
{/* Top Black Utility Bar */}
|
{/* Top Black Utility Bar */}
|
||||||
|
{(tagline || phone) && (
|
||||||
<div className="bg-[#1f1f1f] text-white text-xs py-2 hidden md:block">
|
<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="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">
|
<div className="flex items-center gap-2">
|
||||||
<span className="inline-block w-2.5 h-2.5 bg-[#FFED00]" />
|
<span className="inline-block w-2.5 h-2.5 bg-[#FFED00]" />
|
||||||
<span className="font-bold uppercase tracking-wider text-[11px] text-[#e0e0e0]">
|
<span className="font-bold uppercase tracking-wider text-[11px] text-[#e0e0e0]">
|
||||||
{tagline || 'Profesjonalne Mycie i Renowacja Nawierzchni Brukowych'}
|
{tagline}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{phone && (
|
{phone && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 ml-auto">
|
||||||
|
{phoneLabel && (
|
||||||
<span className="font-bold uppercase tracking-wider text-[11px] text-[#e0e0e0]">
|
<span className="font-bold uppercase tracking-wider text-[11px] text-[#e0e0e0]">
|
||||||
Infolinia / Bezpłatna wycena:
|
{phoneLabel}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
<a
|
<a
|
||||||
href={`tel:${phone.replace(/\s+/g, '')}`}
|
href={`tel:${phone.replace(/\s+/g, '')}`}
|
||||||
className="font-black uppercase tracking-wider text-[11px] text-[#FFED00] hover:underline font-mono"
|
className="font-black uppercase tracking-wider text-[11px] text-[#FFED00] hover:underline font-mono"
|
||||||
@@ -93,6 +100,7 @@ export function Header({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Main Navigation Bar */}
|
{/* 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">
|
<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}
|
{companyName}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{tagline && (
|
||||||
<span className="text-[10px] uppercase tracking-widest text-[#666666] font-bold block mt-1">
|
<span className="text-[10px] uppercase tracking-widest text-[#666666] font-bold block mt-1">
|
||||||
Czyszczenie Kostki Brukowej
|
{tagline}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{/* Navigation Items (Without duplicated Kontakt) */}
|
{/* Navigation Items */}
|
||||||
{filteredNavItems && filteredNavItems.length > 0 && (
|
{filteredNavItems && filteredNavItems.length > 0 && (
|
||||||
<nav className="hidden lg:flex items-center gap-8 text-xs font-black uppercase tracking-wider text-[#1f1f1f]">
|
<nav className="hidden lg:flex items-center gap-8 text-xs font-black uppercase tracking-wider text-[#1f1f1f]">
|
||||||
{filteredNavItems.map((item, idx) => (
|
{filteredNavItems.map((item, idx) => (
|
||||||
@@ -128,7 +138,7 @@ export function Header({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<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}>
|
<div className="relative" ref={dropdownRef}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -141,7 +151,7 @@ export function Header({
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{langDropdownOpen && (
|
{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) => (
|
{languages.map((lang) => (
|
||||||
<a
|
<a
|
||||||
key={lang.code}
|
key={lang.code}
|
||||||
@@ -161,7 +171,8 @@ export function Header({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right CTA Button: KONTAKT */}
|
{/* Right CTA Button */}
|
||||||
|
{contactHref && contactLabel && (
|
||||||
<a
|
<a
|
||||||
href={contactHref}
|
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"
|
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"
|
||||||
@@ -169,6 +180,7 @@ export function Header({
|
|||||||
<span>{contactLabel}</span>
|
<span>{contactLabel}</span>
|
||||||
<ArrowRight className="h-4 w-4" />
|
<ArrowRight className="h-4 w-4" />
|
||||||
</a>
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -11,11 +11,17 @@ export const Navigation: GlobalConfig = {
|
|||||||
name: 'tagline',
|
name: 'tagline',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
localized: true,
|
localized: true,
|
||||||
defaultValue: 'Czyszczenie Kostki Brukowej',
|
defaultValue: 'Profesjonalne Mycie i Renowacja Nawierzchni Brukowych',
|
||||||
admin: {
|
admin: {
|
||||||
description: 'Podtytuł pod logo w nagłówku',
|
description: 'Podtytuł / hasło w pasku górnym i pod logo',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'phoneLabel',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
defaultValue: 'Infolinia / Bezpłatna wycena:',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'headerItems',
|
name: 'headerItems',
|
||||||
type: 'array',
|
type: 'array',
|
||||||
@@ -52,7 +58,7 @@ export const Navigation: GlobalConfig = {
|
|||||||
{
|
{
|
||||||
name: 'label',
|
name: 'label',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
defaultValue: 'Zadzwoń po wycenę',
|
defaultValue: 'KONTAKT',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'page',
|
name: 'page',
|
||||||
@@ -62,6 +68,7 @@ export const Navigation: GlobalConfig = {
|
|||||||
{
|
{
|
||||||
name: 'url',
|
name: 'url',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
defaultValue: '#formularz-kontaktowy',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -75,6 +82,18 @@ export const Navigation: GlobalConfig = {
|
|||||||
description: 'Krótki opis firmy w lewej kolumnie stopki',
|
description: 'Krótki opis firmy w lewej kolumnie stopki',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'navTitle',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
defaultValue: 'Nawigacja',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'contactTitle',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
defaultValue: 'Kontakt',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'footerItems',
|
name: 'footerItems',
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
|||||||
+36
-1
@@ -263,6 +263,10 @@ export interface Page {
|
|||||||
* Strona docelowa drugiego przycisku
|
* Strona docelowa drugiego przycisku
|
||||||
*/
|
*/
|
||||||
ctaSecondaryTarget?: (number | null) | Page;
|
ctaSecondaryTarget?: (number | null) | Page;
|
||||||
|
/**
|
||||||
|
* URL lub kotwica drugiego przycisku (np. #cennik)
|
||||||
|
*/
|
||||||
|
ctaSecondaryUrl?: string | null;
|
||||||
/**
|
/**
|
||||||
* Zdjęcie tła sekcji hero
|
* Zdjęcie tła sekcji hero
|
||||||
*/
|
*/
|
||||||
@@ -354,6 +358,7 @@ export interface Page {
|
|||||||
price: string;
|
price: string;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
isPopular?: boolean | null;
|
isPopular?: boolean | null;
|
||||||
|
popularBadgeLabel?: string | null;
|
||||||
features?:
|
features?:
|
||||||
| {
|
| {
|
||||||
feature: string;
|
feature: string;
|
||||||
@@ -424,10 +429,20 @@ export interface Page {
|
|||||||
heading: string;
|
heading: string;
|
||||||
subheading?: string | null;
|
subheading?: string | null;
|
||||||
phone?: string | null;
|
phone?: string | null;
|
||||||
|
directPhoneLabel?: string | null;
|
||||||
email?: string | null;
|
email?: string | null;
|
||||||
|
emailContactLabel?: string | null;
|
||||||
workingHours?: string | null;
|
workingHours?: string | null;
|
||||||
|
workingHoursLabel?: string | null;
|
||||||
|
nameLabel?: string | null;
|
||||||
|
phoneInputLabel?: string | null;
|
||||||
|
locationLabel?: string | null;
|
||||||
|
areaLabel?: string | null;
|
||||||
|
messageLabel?: string | null;
|
||||||
submitButtonText?: string | null;
|
submitButtonText?: string | null;
|
||||||
consentText?: string | null;
|
consentText?: string | null;
|
||||||
|
successHeading?: string | null;
|
||||||
|
successMessage?: string | null;
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
blockName?: string | null;
|
blockName?: string | null;
|
||||||
blockType: 'formBlock';
|
blockType: 'formBlock';
|
||||||
@@ -444,6 +459,7 @@ export interface Page {
|
|||||||
badge?: string | null;
|
badge?: string | null;
|
||||||
heading: string;
|
heading: string;
|
||||||
subheading?: string | null;
|
subheading?: string | null;
|
||||||
|
readMoreLabel?: string | null;
|
||||||
articles?:
|
articles?:
|
||||||
| {
|
| {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -851,6 +867,7 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
ctaPhoneLabel?: T;
|
ctaPhoneLabel?: T;
|
||||||
ctaSecondaryLabel?: T;
|
ctaSecondaryLabel?: T;
|
||||||
ctaSecondaryTarget?: T;
|
ctaSecondaryTarget?: T;
|
||||||
|
ctaSecondaryUrl?: T;
|
||||||
backgroundImage?: T;
|
backgroundImage?: T;
|
||||||
id?: T;
|
id?: T;
|
||||||
blockName?: T;
|
blockName?: T;
|
||||||
@@ -923,6 +940,7 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
price?: T;
|
price?: T;
|
||||||
description?: T;
|
description?: T;
|
||||||
isPopular?: T;
|
isPopular?: T;
|
||||||
|
popularBadgeLabel?: T;
|
||||||
features?:
|
features?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
@@ -993,10 +1011,20 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
heading?: T;
|
heading?: T;
|
||||||
subheading?: T;
|
subheading?: T;
|
||||||
phone?: T;
|
phone?: T;
|
||||||
|
directPhoneLabel?: T;
|
||||||
email?: T;
|
email?: T;
|
||||||
|
emailContactLabel?: T;
|
||||||
workingHours?: T;
|
workingHours?: T;
|
||||||
|
workingHoursLabel?: T;
|
||||||
|
nameLabel?: T;
|
||||||
|
phoneInputLabel?: T;
|
||||||
|
locationLabel?: T;
|
||||||
|
areaLabel?: T;
|
||||||
|
messageLabel?: T;
|
||||||
submitButtonText?: T;
|
submitButtonText?: T;
|
||||||
consentText?: T;
|
consentText?: T;
|
||||||
|
successHeading?: T;
|
||||||
|
successMessage?: T;
|
||||||
id?: T;
|
id?: T;
|
||||||
blockName?: T;
|
blockName?: T;
|
||||||
};
|
};
|
||||||
@@ -1015,6 +1043,7 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
badge?: T;
|
badge?: T;
|
||||||
heading?: T;
|
heading?: T;
|
||||||
subheading?: T;
|
subheading?: T;
|
||||||
|
readMoreLabel?: T;
|
||||||
articles?:
|
articles?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
@@ -1258,9 +1287,10 @@ export interface Company {
|
|||||||
export interface Navigation {
|
export interface Navigation {
|
||||||
id: number;
|
id: number;
|
||||||
/**
|
/**
|
||||||
* Podtytuł pod logo w nagłówku
|
* Podtytuł / hasło w pasku górnym i pod logo
|
||||||
*/
|
*/
|
||||||
tagline?: string | null;
|
tagline?: string | null;
|
||||||
|
phoneLabel?: string | null;
|
||||||
headerItems?:
|
headerItems?:
|
||||||
| {
|
| {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -1281,6 +1311,8 @@ export interface Navigation {
|
|||||||
* Krótki opis firmy w lewej kolumnie stopki
|
* Krótki opis firmy w lewej kolumnie stopki
|
||||||
*/
|
*/
|
||||||
footerTagline?: string | null;
|
footerTagline?: string | null;
|
||||||
|
navTitle?: string | null;
|
||||||
|
contactTitle?: string | null;
|
||||||
footerItems?:
|
footerItems?:
|
||||||
| {
|
| {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -1502,6 +1534,7 @@ export interface CompanySelect<T extends boolean = true> {
|
|||||||
*/
|
*/
|
||||||
export interface NavigationSelect<T extends boolean = true> {
|
export interface NavigationSelect<T extends boolean = true> {
|
||||||
tagline?: T;
|
tagline?: T;
|
||||||
|
phoneLabel?: T;
|
||||||
headerItems?:
|
headerItems?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
@@ -1518,6 +1551,8 @@ export interface NavigationSelect<T extends boolean = true> {
|
|||||||
url?: T;
|
url?: T;
|
||||||
};
|
};
|
||||||
footerTagline?: T;
|
footerTagline?: T;
|
||||||
|
navTitle?: T;
|
||||||
|
contactTitle?: T;
|
||||||
footerItems?:
|
footerItems?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
|
|||||||
+5
-1
@@ -76,6 +76,7 @@ export async function seed(payload: Payload) {
|
|||||||
ctaPhone: '+48 668 973 737',
|
ctaPhone: '+48 668 973 737',
|
||||||
ctaPhoneLabel: 'Zadzwoń po bezpłatną wycenę',
|
ctaPhoneLabel: 'Zadzwoń po bezpłatną wycenę',
|
||||||
ctaSecondaryLabel: 'Zobacz cennik za m²',
|
ctaSecondaryLabel: 'Zobacz cennik za m²',
|
||||||
|
ctaSecondaryUrl: '#cennik',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
blockType: 'problemSolution',
|
blockType: 'problemSolution',
|
||||||
@@ -578,7 +579,10 @@ W każdej chwili możesz zmienić swoje preferencje dotyczące plików cookies k
|
|||||||
slug: 'navigation',
|
slug: 'navigation',
|
||||||
locale: 'pl',
|
locale: 'pl',
|
||||||
data: {
|
data: {
|
||||||
tagline: 'Czyszczenie Kostki Brukowej',
|
tagline: 'Profesjonalne Mycie i Renowacja Nawierzchni Brukowych',
|
||||||
|
phoneLabel: 'Infolinia / Bezpłatna wycena:',
|
||||||
|
navTitle: 'Nawigacja',
|
||||||
|
contactTitle: 'Kontakt',
|
||||||
headerItems: [
|
headerItems: [
|
||||||
{
|
{
|
||||||
label: 'Cennik',
|
label: 'Cennik',
|
||||||
|
|||||||
Reference in New Issue
Block a user