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:
2026-08-26 03:09:15 +02:00
parent 8c6f3d2633
commit 434237e916
17 changed files with 475 additions and 245 deletions
+23
View File
@@ -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) {
+12 -2
View File
@@ -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" />
+7 -4
View File
@@ -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>
)} )}
<div className="text-xs font-black uppercase tracking-wider text-[#1f1f1f] flex items-center gap-1 hover:underline cursor-pointer whitespace-nowrap"> {props.readMoreLabel && (
<span>Czytaj poradnik</span> <div className="text-xs font-black uppercase tracking-wider text-[#1f1f1f] flex items-center gap-1 hover:underline cursor-pointer whitespace-nowrap">
<ArrowRight className="h-3.5 w-3.5 text-[#1f1f1f]" /> <span>{props.readMoreLabel}</span>
</div> <ArrowRight className="h-3.5 w-3.5 text-[#1f1f1f]" />
</div>
)}
</div> </div>
</article> </article>
</AnimateIn> </AnimateIn>
+6
View File
@@ -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',
+116 -64
View File
@@ -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>
<h3 className="text-xl font-black uppercase text-[#1f1f1f]"> {successHeading && (
Zgłoszenie zostało wysłane <h3 className="text-xl font-black uppercase text-[#1f1f1f]">
</h3> {successHeading}
<p className="text-[#666666] text-sm max-w-md mx-auto leading-relaxed"> </h3>
Nasz doradca techniczny skontaktuje się z Państwem telefonicznie w celu przygotowania dokładnej wyceny. )}
</p> {successMessage && (
<p className="text-[#666666] text-sm max-w-md mx-auto leading-relaxed">
{successMessage}
</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>
<label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2"> {nameLabel && (
Imię i Nazwisko * <label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2">
</label> {nameLabel}
</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>
<label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2"> {phoneInputLabel && (
Numer Telefonu * <label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2">
</label> {phoneInputLabel}
</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,73 +202,84 @@ 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>
<label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2"> {locationLabel && (
Miejscowość <label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2">
</label> {locationLabel}
</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>
<label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2"> {areaLabel && (
Szacowany Metraż (m²) <label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2">
</label> {areaLabel}
</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>
<label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2"> {messageLabel && (
Zakres prac / stan nawierzchni <label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2">
</label> {messageLabel}
</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 */}
<div className="pt-2"> {consentText && (
<label htmlFor="consent" className="flex items-start gap-3 cursor-pointer select-none group"> <div className="pt-2">
<div className="relative flex items-center justify-center mt-0.5 shrink-0"> <label htmlFor="consent" className="flex items-start gap-3 cursor-pointer select-none group">
<input <div className="relative flex items-center justify-center mt-0.5 shrink-0">
id="consent" <input
name="consent" id="consent"
type="checkbox" name="consent"
required type="checkbox"
checked={consentChecked} required
onChange={(e) => setConsentChecked(e.target.checked)} checked={consentChecked}
className="sr-only" onChange={(e) => setConsentChecked(e.target.checked)}
/> className="sr-only"
<div className={`w-5 h-5 border-2 border-[#1f1f1f] transition-all flex items-center justify-center ${ />
consentChecked ? 'bg-[#FFED00]' : 'bg-white' <div
}`}> className={`w-5 h-5 border-2 border-[#1f1f1f] transition-all flex items-center justify-center ${
<Check className={`w-3.5 h-3.5 text-[#1f1f1f] stroke-[3] transition-opacity ${ consentChecked ? 'bg-[#FFED00]' : 'bg-white'
consentChecked ? 'opacity-100' : 'opacity-0' }`}
}`} /> >
<Check
className={`w-3.5 h-3.5 text-[#1f1f1f] stroke-[3] transition-opacity ${
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"> {consentText}
{consentText} </span>
</span> </label>
</label> </div>
</div> )}
<button {submitButtonText && (
type="submit" <button
disabled={loading || !consentChecked} type="submit"
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" 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"
<Send className="h-4 w-4" /> >
<span>{loading ? 'Wysyłanie...' : submitButtonText}</span> <Send className="h-4 w-4" />
</button> <span>{submitButtonText}</span>
</button>
)}
</form> </form>
)} )}
</div> </div>
+63 -1
View File
@@ -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.',
}, },
], ],
} }
+29 -28
View File
@@ -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,32 +67,34 @@ export function HeroBlockComponent(props: HeroBlockProps) {
)} )}
{/* Action Buttons */} {/* Action Buttons */}
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3"> {(ctaPhone || ctaSecondaryLabel) && (
{ctaPhone && ( <div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3">
<a {ctaPhone && ctaPhoneLabel && (
href={`tel:${ctaPhone.replace(/\s+/g, '')}`} <a
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" 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"
<Phone className="h-4 w-4" /> >
<span>{ctaPhoneLabel}</span> <Phone className="h-4 w-4" />
</a> <span>{ctaPhoneLabel}</span>
)} </a>
{ctaSecondaryLabel && ( )}
<a {ctaSecondaryLabel && ctaSecondaryUrl && (
href={ctaSecondaryUrl} <a
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" 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"
<span>{ctaSecondaryLabel}</span> >
<ArrowRight className="h-3.5 w-3.5 text-[#FFED00]" /> <span>{ctaSecondaryLabel}</span>
</a> <ArrowRight className="h-3.5 w-3.5 text-[#FFED00]" />
)} </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">
+8
View File
@@ -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',
+3 -2
View File
@@ -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>
)} )}
+6
View File
@@ -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',
+22 -4
View File
@@ -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'
+62 -50
View File
@@ -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>
)} )}
<div className="text-xs text-[#888888] font-mono space-y-1 pt-2 border-t border-[#333333]"> {(nip || regon) && (
{nip && <div>NIP: <span className="text-[#e0e0e0] font-semibold">{nip}</span></div>} <div className="text-xs text-[#888888] font-mono space-y-1 pt-2 border-t border-[#333333]">
{regon && <div>REGON: <span className="text-[#e0e0e0] font-semibold">{regon}</span></div>} {nip && <div>NIP: <span className="text-[#e0e0e0] font-semibold">{nip}</span></div>}
</div> {regon && <div>REGON: <span className="text-[#e0e0e0] font-semibold">{regon}</span></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">
<div className="text-xs font-black uppercase tracking-widest text-[#FFED00] border-b border-[#333333] pb-2"> {navTitle && (
Nawigacja <div className="text-xs font-black uppercase tracking-widest text-[#FFED00] border-b border-[#333333] pb-2">
</div> {navTitle}
</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,54 +92,58 @@ export function Footer({
)} )}
{/* Contact Details */} {/* Contact Details */}
<div className="space-y-4"> {(phone || email || address || city) && (
<div className="text-xs font-black uppercase tracking-widest text-[#FFED00] border-b border-[#333333] pb-2"> <div className="space-y-4">
Kontakt {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> </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> </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>
+53 -41
View File
@@ -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,29 +72,35 @@ 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 */}
<div className="bg-[#1f1f1f] text-white text-xs py-2 hidden md:block"> {(tagline || phone) && (
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl flex items-center justify-between"> <div className="bg-[#1f1f1f] text-white text-xs py-2 hidden md:block">
<div className="flex items-center gap-2"> <div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl flex items-center justify-between">
<span className="inline-block w-2.5 h-2.5 bg-[#FFED00]" /> {tagline && (
<span className="font-bold uppercase tracking-wider text-[11px] text-[#e0e0e0]"> <div className="flex items-center gap-2">
{tagline || 'Profesjonalne Mycie i Renowacja Nawierzchni Brukowych'} <span className="inline-block w-2.5 h-2.5 bg-[#FFED00]" />
</span> <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> </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>
</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>
)} )}
<span className="text-[10px] uppercase tracking-widest text-[#666666] font-bold block mt-1"> {tagline && (
Czyszczenie Kostki Brukowej <span className="text-[10px] uppercase tracking-widest text-[#666666] font-bold block mt-1">
</span> {tagline}
</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,14 +171,16 @@ export function Header({
)} )}
</div> </div>
{/* Right CTA Button: KONTAKT */} {/* Right CTA Button */}
<a {contactHref && contactLabel && (
href={contactHref} <a
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" 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" /> <span>{contactLabel}</span>
</a> <ArrowRight className="h-4 w-4" />
</a>
)}
</div> </div>
</div> </div>
</header> </header>
+22 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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',