feat: integrate built-in forms, refine footer & parallax, dynamic navigation & branding assets

This commit is contained in:
2026-08-26 11:40:04 +02:00
parent cb67badf53
commit 51a3d6fdb8
32 changed files with 929 additions and 651 deletions
+8 -8
View File
@@ -15,15 +15,15 @@ This block is written and re-added by `next dev` — verify at `node_modules/nex
- **Dynamic Source:** All content MUST come dynamically from Payload CMS (collections, globals like `Company` and `Navigation`, block fields, seed script, or `@intecion/ipal-kit`). - **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. - 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) ## 2. BRANDING & UI GUIDELINES
- Colors: - Colors (Managed centrally in `src/app/(frontend)/theme.css`):
- Primary Yellow: `#FFED00` (Hover: `#FAE100`) - Primary Cyan: `#01aeee` (Hover: `#0098d4` / class `bg-primary`, `text-primary`, `border-primary`)
- Dark Anthracite: `#1F1F1F` - Accent Red: `#ee1d23` (Hover: `#d9141a` / class `bg-accent`, `text-accent`, `border-accent`)
- Deep Black: `#141414` - Dark Anthracite/Black: `#231f20` (Hover: `#332d2e` / class `bg-dark`, `text-dark`, `border-dark`)
- Industrial Light Gray: `#F4F4F4` - Industrial Light Gray: `#f4f4f4` (class `bg-surface-gray`)
- Architecture: - Architecture:
- Sharp, geometric corners (`rounded-none` / `0px radius`). No bubbly rounded-3xl or generic AI circles. - Sharp, geometric corners (`rounded-none` / `0px radius`). No bubbly rounded-3xl or generic circles.
- Checkboxes: Square with 2px black border, yellow `#FFED00` checked background, and thick black checkmark (`stroke-[3]`). - Checkboxes: Square with 2px dark border (`border-dark`), primary cyan (`bg-primary`) checked background, and thick dark checkmark (`stroke-[3] text-dark`).
- High contrast typography: Uppercase, bold/black weights with letter-spacing. - High contrast typography: Uppercase, bold/black weights with letter-spacing.
## 3. UX & ACCESSIBILITY ## 3. UX & ACCESSIBILITY
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 988 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

+71 -26
View File
@@ -58,18 +58,6 @@ export default async function LocaleLayout({ children, params }: LayoutProps) {
// fallback if globals not yet created // fallback if globals not yet created
} }
const privacyHref = privacyPage
? getSystemPagePath({ config: i18nConfig, locale, page: privacyPage })
: undefined
const cookieHref = cookiePage
? getSystemPagePath({ config: i18nConfig, locale, page: cookiePage })
: undefined
const homeHref = homePage
? getSystemPagePath({ config: i18nConfig, locale, page: homePage }) || '/'
: '/'
const [texts, analytics] = await Promise.all([ const [texts, analytics] = await Promise.all([
getConsentTexts({ getConsentTexts({
config: i18nConfig, config: i18nConfig,
@@ -83,19 +71,69 @@ export default async function LocaleLayout({ children, params }: LayoutProps) {
getAnalyticsConfig(payload), getAnalyticsConfig(payload),
]) ])
const privacyPath = privacyPage
? getSystemPagePath({ config: i18nConfig, locale, page: privacyPage })
: undefined
const privacyHref = privacyPath || `/${locale}/polityka-prywatnosci`
const privacyLabel =
(typeof privacyPage === 'object' && privacyPage && 'title' in privacyPage && privacyPage.title
? (privacyPage.title as string)
: texts.privacyLink?.label) || (locale === 'pl' ? 'Polityka prywatności' : 'Privacy Policy')
const cookiePath = cookiePage
? getSystemPagePath({ config: i18nConfig, locale, page: cookiePage })
: undefined
const cookieHref = cookiePath || `/${locale}/polityka-cookies`
const cookieLabel =
(typeof cookiePage === 'object' && cookiePage && 'title' in cookiePage && cookiePage.title
? (cookiePage.title as string)
: undefined) || (locale === 'pl' ? 'Polityka cookies' : 'Cookie Policy')
const termsPage = (settings as { termsOfService?: unknown })?.termsOfService
const termsPath = termsPage
? getSystemPagePath({ config: i18nConfig, locale, page: termsPage })
: undefined
const termsHref = termsPath || `/${locale}/regulamin`
const termsLabel =
(typeof termsPage === 'object' && termsPage && 'title' in termsPage && termsPage.title
? (termsPage.title as string)
: undefined) || (locale === 'pl' ? 'Regulamin strony' : 'Terms of Service')
const homeHref = homePage
? getSystemPagePath({ config: i18nConfig, locale, page: homePage }) || '/'
: '/'
const headerItems: NavItem[] = const headerItems: NavItem[] =
navigation?.headerItems?.map((item) => ({ navigation?.headerItems
id: item.id || undefined, ?.filter((item) => {
label: item.label, if (!item.label) return false
href: resolveNavItemHref(item), if (item.url) return true
})) || [] if (item.page && typeof item.page === 'object' && 'slug' in item.page && Boolean(item.page.slug)) {
return true
}
return false
})
.map((item) => ({
id: item.id || undefined,
label: item.label,
href: resolveNavItemHref(item),
})) || []
const footerItems: FooterNavItem[] = const footerItems: FooterNavItem[] =
navigation?.footerItems?.map((item) => ({ navigation?.footerItems
id: item.id || undefined, ?.filter((item) => {
label: item.label, if (!item.label) return false
href: resolveNavItemHref(item), if (item.url) return true
})) || [] if (item.page && typeof item.page === 'object' && 'slug' in item.page && Boolean(item.page.slug)) {
return true
}
return false
})
.map((item) => ({
id: item.id || undefined,
label: item.label,
href: resolveNavItemHref(item),
})) || []
const contactHref = const contactHref =
navigation?.ctaButton?.url || navigation?.ctaButton?.url ||
@@ -104,7 +142,7 @@ export default async function LocaleLayout({ children, params }: LayoutProps) {
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-dark antialiased selection:bg-primary selection:text-dark">
<ConsentProvider texts={texts}> <ConsentProvider texts={texts}>
<Header <Header
companyName={company?.shortName || company?.name} companyName={company?.shortName || company?.name}
@@ -122,21 +160,28 @@ export default async function LocaleLayout({ children, params }: LayoutProps) {
companyName={company?.name} companyName={company?.name}
footerTagline={navigation?.footerTagline} footerTagline={navigation?.footerTagline}
nip={company?.nip} nip={company?.nip}
nipLabel="NIP"
regon={company?.regon} regon={company?.regon}
regonLabel="REGON"
address={company?.address} address={company?.address}
city={company?.city} city={company?.city}
phone={company?.phone} phone={company?.phone}
email={company?.email} email={company?.email}
items={footerItems} items={footerItems}
privacyHref={privacyHref} privacyHref={privacyHref}
privacyLabel={texts.privacyLink?.label} privacyLabel={privacyLabel}
cookieHref={cookieHref} cookieHref={cookieHref}
cookieLabel={cookiePage && typeof cookiePage === 'object' && 'title' in cookiePage ? (cookiePage.title as string) : undefined} cookieLabel={cookieLabel}
termsHref={termsHref}
termsLabel={termsLabel}
navTitle={(navigation as { navTitle?: string })?.navTitle} navTitle={(navigation as { navTitle?: string })?.navTitle}
contactTitle={(navigation as { contactTitle?: string })?.contactTitle} contactTitle={(navigation as { contactTitle?: string })?.contactTitle}
legalTitle={(navigation as { legalTitle?: string })?.legalTitle}
creditLabel="Powered by NetScale.pl"
creditHref="https://netscale.pl"
/> />
<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-3 left-3 z-40 bg-dark hover:bg-primary text-primary hover:text-dark border-2 border-primary p-2 transition-all shadow-lg cursor-pointer" />
<Analytics {...analytics} /> <Analytics {...analytics} />
</ConsentProvider> </ConsentProvider>
</body> </body>
+2 -17
View File
@@ -1,22 +1,7 @@
@import "tailwindcss"; @import "tailwindcss";
@source "../../../node_modules/@intecion/ipal-kit/dist/**/*.js"; @source "../../../node_modules/@intecion/ipal-kit/dist/**/*.js";
@theme { @import "./theme.css";
--color-karcher-yellow: #ffed00;
--color-karcher-yellow-hover: #fae100;
--color-karcher-dark: #1f1f1f;
--color-karcher-black: #141414;
--color-karcher-gray: #f4f4f4;
--color-karcher-border: #e0e0e0;
--color-karcher-text: #222222;
--color-karcher-muted: #666666;
}
:root {
--ipal-primary: #ffed00;
--ipal-primary-hover: #fae100;
--ipal-radius: 0px;
}
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
@@ -26,7 +11,7 @@ html {
body { body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background-color: #ffffff; background-color: #ffffff;
color: #222222; color: var(--color-brand-dark);
} }
h1, h2, h3, h4, .font-karcher { h1, h2, h3, h4, .font-karcher {
+33
View File
@@ -0,0 +1,33 @@
/* Centralized Brand Color Palette & Theme Tokens */
@theme {
/* Core 3 Brand Colors */
--color-brand-cyan: #01aeee;
--color-brand-cyan-hover: #0098d4;
--color-brand-red: #ee1d23;
--color-brand-red-hover: #d9141a;
--color-brand-dark: #231f20;
--color-brand-dark-hover: #332d2e;
/* Semantic Aliases */
--color-primary: #01aeee;
--color-primary-hover: #0098d4;
--color-accent: #ee1d23;
--color-accent-hover: #d9141a;
--color-dark: #231f20;
--color-dark-hover: #332d2e;
/* Neutrals & Industrial Surfaces */
--color-surface-white: #ffffff;
--color-surface-gray: #f4f4f4;
--color-surface-gray-alt: #f8f8f8;
--color-surface-border: #e0e0e0;
--color-muted-text: #666666;
}
:root {
/* IPAL Kit / Framework Variables */
--ipal-primary: #01aeee;
--ipal-primary-hover: #0098d4;
--ipal-secondary: #ee1d23;
--ipal-radius: 0px;
}
+49
View File
@@ -0,0 +1,49 @@
'use server'
import { submitForm } from '@intecion/ipal-kit/server'
import { getCachedPayload } from '@/lib/content'
import { getNotificationTexts, resolveFormMessage } from '@intecion/ipal-kit'
import { headers } from 'next/headers'
export async function submitFormAction({
formId,
data,
locale = 'pl',
}: {
formId: string | number
data: Record<string, unknown>
locale?: string
}) {
try {
const payload = await getCachedPayload()
const headerList = await headers()
const ip = headerList.get('x-forwarded-for')?.split(',')[0]?.trim() || headerList.get('x-real-ip') || '127.0.0.1'
const result = await submitForm({
payload,
formId: String(formId),
data,
ip,
})
if (!result.success) {
const notifications = await getNotificationTexts({ payload, locale })
const errorMessage = resolveFormMessage(result, notifications?.form)
return {
success: false,
message: errorMessage || 'Wystąpił błąd podczas wysyłania formularza. Spróbuj ponownie później.',
}
}
return {
success: true,
submissionId: result.submissionId,
}
} catch (err) {
console.error('Error executing submitFormAction:', err)
return {
success: false,
message: 'Wystąpił nieoczekiwany błąd serwera. Skontaktuj się z nami telefonicznie.',
}
}
}
+11 -11
View File
@@ -20,19 +20,19 @@ export function BeforeAfterGalleryBlockComponent(props: BeforeAfterGalleryBlockP
const { badge, heading, subheading, items } = props const { badge, heading, subheading, items } = props
return ( return (
<section className="py-20 bg-white border-b-2 border-[#1f1f1f]"> <section className="py-20 bg-white border-b-2 border-dark">
<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">
<div className="max-w-3xl mb-14"> <div className="max-w-3xl mb-14">
{badge && ( {badge && (
<div className="inline-block bg-[#FFED00] text-[#1f1f1f] text-xs font-black uppercase tracking-widest px-3 py-1 mb-4 border border-[#1f1f1f]"> <div className="inline-block bg-primary text-dark text-xs font-black uppercase tracking-widest px-3 py-1 mb-4 border border-dark">
{badge} {badge}
</div> </div>
)} )}
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f]"> <h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-dark">
{heading} {heading}
</h2> </h2>
{subheading && ( {subheading && (
<p className="mt-4 text-base text-[#666666] leading-relaxed font-medium"> <p className="mt-4 text-base text-muted-text leading-relaxed font-medium">
{subheading} {subheading}
</p> </p>
)} )}
@@ -43,15 +43,15 @@ export function BeforeAfterGalleryBlockComponent(props: BeforeAfterGalleryBlockP
{items.map((item, idx) => ( {items.map((item, idx) => (
<div <div
key={idx} key={idx}
className="bg-[#f4f4f4] border-2 border-[#1f1f1f] flex flex-col justify-between" className="bg-surface-gray border-2 border-dark flex flex-col justify-between"
> >
<div className="p-8 space-y-4"> <div className="p-8 space-y-4">
{item.category && ( {item.category && (
<span className="inline-block bg-[#1f1f1f] text-[#FFED00] text-[11px] font-black uppercase tracking-wider px-3 py-1"> <span className="inline-block bg-dark text-primary text-[11px] font-black uppercase tracking-wider px-3 py-1">
{item.category} {item.category}
</span> </span>
)} )}
<h3 className="text-lg font-black uppercase text-[#1f1f1f] leading-snug"> <h3 className="text-lg font-black uppercase text-dark leading-snug">
{item.title} {item.title}
</h3> </h3>
{item.description && ( {item.description && (
@@ -61,13 +61,13 @@ export function BeforeAfterGalleryBlockComponent(props: BeforeAfterGalleryBlockP
)} )}
</div> </div>
<div className="p-6 bg-[#ebebeb] border-t-2 border-[#1f1f1f] flex items-center justify-between"> <div className="p-6 bg-[#ebebeb] border-t-2 border-dark flex items-center justify-between">
<div className="text-xs font-black uppercase tracking-wider text-[#1f1f1f]"> <div className="text-xs font-black uppercase tracking-wider text-dark">
Czyszczenie gorącowodne Mosmatic Czyszczenie gorącowodne Mosmatic
</div> </div>
<div className="text-xs font-black uppercase tracking-widest text-[#1f1f1f] flex items-center gap-1.5 hover:underline cursor-pointer"> <div className="text-xs font-black uppercase tracking-widest text-dark flex items-center gap-1.5 hover:underline cursor-pointer">
<span>Szczegóły procedury</span> <span>Szczegóły procedury</span>
<ArrowRight className="h-3.5 w-3.5 text-[#1f1f1f]" /> <ArrowRight className="h-3.5 w-3.5 text-primary" />
</div> </div>
</div> </div>
</div> </div>
+23 -21
View File
@@ -23,20 +23,20 @@ export function BlogListBlockComponent(props: BlogListBlockProps) {
const { badge, heading, subheading, articles } = props const { badge, heading, subheading, articles } = props
return ( return (
<section className="py-24 bg-white border-b-2 border-[#1f1f1f]"> <section className="py-24 bg-white border-b-2 border-dark">
<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">
<AnimateIn direction="up"> <AnimateIn direction="up">
<div className="max-w-3xl mb-14"> <div className="max-w-3xl mb-14">
{badge && ( {badge && (
<div className="inline-block bg-[#FFED00] text-[#1f1f1f] text-xs font-black uppercase tracking-widest px-3 py-1 mb-4 border border-[#1f1f1f]"> <div className="inline-block bg-primary text-dark text-xs font-black uppercase tracking-widest px-3 py-1 mb-4 border border-dark">
{badge} {badge}
</div> </div>
)} )}
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f]"> <h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-dark">
{heading} {heading}
</h2> </h2>
{subheading && ( {subheading && (
<p className="mt-4 text-base text-[#666666] leading-relaxed font-medium"> <p className="mt-4 text-base text-muted-text leading-relaxed font-medium">
{subheading} {subheading}
</p> </p>
)} )}
@@ -48,42 +48,44 @@ export function BlogListBlockComponent(props: BlogListBlockProps) {
{articles.map((art, idx) => ( {articles.map((art, idx) => (
<AnimateIn key={idx} delay={idx * 150} direction="up" className="h-full"> <AnimateIn key={idx} delay={idx * 150} direction="up" className="h-full">
<article <article
className="bg-[#f4f4f4] border-2 border-[#1f1f1f] h-full flex flex-col justify-between hover-lift transition-all shadow-xs hover:border-t-8 hover:border-t-[#FFED00]" className="bg-surface-gray border-2 border-dark h-full flex flex-col justify-between hover-lift transition-all shadow-xs hover:border-t-8 hover:border-t-primary"
> >
<div className="p-8 space-y-4"> <div className="p-8 space-y-3.5">
{/* Top Row: Category Badge & Read Time (No Wrapping / No Overlap) */} {/* Top Row: Category Badge */}
<div className="flex items-center justify-between gap-3"> <div>
<span className="bg-[#1f1f1f] text-[#FFED00] text-[10px] font-black uppercase tracking-wider px-2.5 py-1"> <span className="inline-block bg-dark text-primary text-[10px] font-black uppercase tracking-wider px-2.5 py-1">
{art.category} {art.category}
</span> </span>
</div>
<div>
<h3 className="text-lg font-black uppercase text-dark leading-snug">
{art.title}
</h3>
{art.readTime && ( {art.readTime && (
<span className="flex items-center gap-1.5 text-[11px] text-[#666666] font-bold whitespace-nowrap shrink-0"> <div className="flex items-center gap-1.5 text-[11px] text-muted-text font-bold mt-2">
<Clock className="h-3.5 w-3.5 text-[#1f1f1f]" /> <Clock className="h-3.5 w-3.5 text-dark shrink-0" />
<span>{art.readTime}</span> <span>{art.readTime}</span>
</span> </div>
)} )}
</div> </div>
<h3 className="text-lg font-black uppercase text-[#1f1f1f] leading-snug"> <p className="text-xs sm:text-sm text-[#555555] leading-relaxed line-clamp-4 font-medium pt-1">
{art.title}
</h3>
<p className="text-xs sm:text-sm text-[#555555] leading-relaxed line-clamp-4 font-medium">
{art.excerpt} {art.excerpt}
</p> </p>
</div> </div>
<div className="p-6 bg-[#ebebeb] border-t-2 border-[#1f1f1f] flex items-center justify-between"> <div className="p-6 bg-[#ebebeb] border-t-2 border-dark flex items-center justify-between">
{art.date && ( {art.date && (
<span className="flex items-center gap-1.5 text-xs text-[#777777] font-mono whitespace-nowrap"> <span className="flex items-center gap-1.5 text-xs text-[#777777] font-mono whitespace-nowrap">
<Calendar className="h-3.5 w-3.5 text-[#1f1f1f]" /> <Calendar className="h-3.5 w-3.5 text-dark" />
{art.date} {art.date}
</span> </span>
)} )}
{props.readMoreLabel && ( {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-dark flex items-center gap-1 hover:underline cursor-pointer whitespace-nowrap">
<span>{props.readMoreLabel}</span> <span>{props.readMoreLabel}</span>
<ArrowRight className="h-3.5 w-3.5 text-[#1f1f1f]" /> <ArrowRight className="h-3.5 w-3.5 text-primary" />
</div> </div>
)} )}
</div> </div>
+3 -3
View File
@@ -10,15 +10,15 @@ export function ContentBlockComponent(props: ContentBlockProps) {
const { badge, heading, body } = props const { badge, heading, body } = props
return ( return (
<section className="py-20 bg-white border-b-2 border-[#1f1f1f]"> <section className="py-20 bg-white border-b-2 border-dark">
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-4xl"> <div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-4xl">
{badge && ( {badge && (
<div className="inline-block bg-[#FFED00] text-[#1f1f1f] text-xs font-black uppercase tracking-widest px-3 py-1 mb-4 border border-[#1f1f1f]"> <div className="inline-block bg-primary text-dark text-xs font-black uppercase tracking-widest px-3 py-1 mb-4 border border-dark">
{badge} {badge}
</div> </div>
)} )}
{heading && ( {heading && (
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f] mb-8"> <h1 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-dark mb-8">
{heading} {heading}
</h1> </h1>
)} )}
+7 -7
View File
@@ -25,20 +25,20 @@ export function FaqAccordionBlockComponent(props: FaqAccordionBlockProps) {
} }
return ( return (
<section className="py-24 bg-[#f4f4f4] border-b-2 border-[#1f1f1f]"> <section className="py-24 bg-surface-gray border-b-2 border-dark">
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-4xl"> <div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-4xl">
<AnimateIn direction="up"> <AnimateIn direction="up">
<div className="max-w-3xl mb-14 text-center mx-auto"> <div className="max-w-3xl mb-14 text-center mx-auto">
{badge && ( {badge && (
<div className="inline-block bg-[#1f1f1f] text-[#FFED00] text-xs font-black uppercase tracking-widest px-3 py-1 mb-4"> <div className="inline-block bg-dark text-primary text-xs font-black uppercase tracking-widest px-3 py-1 mb-4">
{badge} {badge}
</div> </div>
)} )}
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f]"> <h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-dark">
{heading} {heading}
</h2> </h2>
{subheading && ( {subheading && (
<p className="mt-4 text-base text-[#666666] leading-relaxed font-medium"> <p className="mt-4 text-base text-muted-text leading-relaxed font-medium">
{subheading} {subheading}
</p> </p>
)} )}
@@ -52,17 +52,17 @@ export function FaqAccordionBlockComponent(props: FaqAccordionBlockProps) {
return ( return (
<AnimateIn key={idx} delay={idx * 100} direction="up"> <AnimateIn key={idx} delay={idx * 100} direction="up">
<div <div
className="border-2 border-[#1f1f1f] bg-white transition-all hover-lift" className="border-2 border-dark bg-white transition-all hover-lift"
> >
<button <button
type="button" type="button"
onClick={() => toggle(idx)} onClick={() => toggle(idx)}
className="w-full flex items-center justify-between p-6 text-left hover:bg-[#f8f8f8] transition-colors" className="w-full flex items-center justify-between p-6 text-left hover:bg-[#f8f8f8] transition-colors"
> >
<span className="text-sm sm:text-base font-black uppercase text-[#1f1f1f] pr-4"> <span className="text-sm sm:text-base font-black uppercase text-dark pr-4">
{item.question} {item.question}
</span> </span>
<div className="h-7 w-7 flex items-center justify-center bg-[#FFED00] text-[#1f1f1f] border border-[#1f1f1f] shrink-0 font-bold"> <div className="h-7 w-7 flex items-center justify-center bg-primary text-dark border border-dark shrink-0 font-bold">
{isOpen ? <Minus className="h-4 w-4" /> : <Plus className="h-4 w-4" />} {isOpen ? <Minus className="h-4 w-4" /> : <Plus className="h-4 w-4" />}
</div> </div>
</button> </button>
+291 -228
View File
@@ -1,32 +1,49 @@
'use client' 'use client'
import React, { useState } from 'react' import React, { useState } from 'react'
import { Phone, Mail, Clock, Send, Check } from 'lucide-react' import { Phone, Mail, Clock, MapPin, CheckCircle, AlertCircle, ArrowRight, Loader2 } from 'lucide-react'
import { AnimateIn } from '@/components/AnimateIn' import { AnimateIn } from '@/components/AnimateIn'
import { submitFormAction } from '@/app/actions/submitFormAction'
interface FormField {
id?: string
blockType: string
name?: string
label?: string
required?: boolean
width?: number
defaultValue?: string
options?: { label: string; value: string; id?: string }[]
message?: unknown
}
interface FormDoc {
id: string | number
title?: string
fields?: FormField[]
submitButtonLabel?: string
confirmationType?: 'message' | 'redirect'
confirmationMessage?: unknown
}
interface FormBlockProps { interface FormBlockProps {
badge?: string form?: FormDoc | string | number | null
heading: string badge?: string | null
subheading?: string heading?: string | null
phone?: string subheading?: string | null
directPhoneLabel?: string phone?: string | null
email?: string directPhoneLabel?: string | null
emailContactLabel?: string email?: string | null
workingHours?: string emailContactLabel?: string | null
workingHoursLabel?: string workingHours?: string | null
nameLabel?: string workingHoursLabel?: string | null
phoneInputLabel?: string address?: string | null
locationLabel?: string addressLabel?: string | null
areaLabel?: string
messageLabel?: string
submitButtonText?: string
consentText?: string
successHeading?: string
successMessage?: string
} }
export function FormBlockComponent(props: FormBlockProps) { export function FormBlockComponent(props: FormBlockProps) {
const { const {
form,
badge, badge,
heading, heading,
subheading, subheading,
@@ -36,280 +53,326 @@ export function FormBlockComponent(props: FormBlockProps) {
emailContactLabel, emailContactLabel,
workingHours, workingHours,
workingHoursLabel, workingHoursLabel,
nameLabel, address,
phoneInputLabel, addressLabel,
locationLabel,
areaLabel,
messageLabel,
submitButtonText,
consentText,
successHeading,
successMessage,
} = props } = props
const [submitted, setSubmitted] = useState(false) const formDoc = typeof form === 'object' && form !== null ? form : null
const [loading, setLoading] = useState(false) const formId = formDoc?.id || (typeof form === 'string' || typeof form === 'number' ? form : null)
const [consentChecked, setConsentChecked] = useState(true) const fields = formDoc?.fields || []
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { const [formData, setFormData] = useState<Record<string, unknown>>({})
const [isSubmitting, setIsSubmitting] = useState(false)
const [isSubmitted, setIsSubmitted] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const handleInputChange = (name: string, value: unknown) => {
setFormData((prev) => ({
...prev,
[name]: value,
}))
if (errorMessage) setErrorMessage(null)
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
if (!consentChecked) return if (!formId) return
setLoading(true)
setTimeout(() => { setIsSubmitting(true)
setLoading(false) setErrorMessage(null)
setSubmitted(true)
}, 600) try {
const res = await submitFormAction({
formId,
data: formData,
})
if (res.success) {
setIsSubmitted(true)
} else {
setErrorMessage(res.message || 'Wystąpił błąd podczas wysyłania.')
}
} catch {
setErrorMessage('Nie udało się wysłać formularza. Spróbuj ponownie później.')
} finally {
setIsSubmitting(false)
}
} }
return ( return (
<section id="formularz-kontaktowy" className="py-24 bg-[#f4f4f4] border-t-2 border-[#1f1f1f]"> <section id="formularz-kontaktowy" className="py-24 bg-white border-b-2 border-dark">
<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">
<div className="grid grid-cols-1 lg:grid-cols-5 gap-12 items-start"> <div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-start">
{/* Left Info Column */} {/* Left Column: Industrial Contact Info */}
<div className="lg:col-span-2 space-y-8"> <div className="lg:col-span-5 space-y-8">
<AnimateIn direction="right"> <AnimateIn direction="right">
<div> <div>
{badge && ( {badge && (
<div className="inline-block bg-[#1f1f1f] text-[#FFED00] text-xs font-black uppercase tracking-widest px-3 py-1 mb-4"> <div className="inline-block bg-primary text-dark text-xs font-black uppercase tracking-widest px-3 py-1 mb-4 border border-dark">
{badge} {badge}
</div> </div>
)} )}
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f]"> {heading && (
{heading} <h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-dark leading-tight">
</h2> {heading}
</h2>
)}
{subheading && ( {subheading && (
<p className="mt-4 text-base text-[#666666] leading-relaxed font-medium"> <p className="mt-4 text-sm text-[#555555] leading-relaxed font-medium">
{subheading} {subheading}
</p> </p>
)} )}
</div> </div>
</AnimateIn> </AnimateIn>
<div className="space-y-4"> <AnimateIn delay={150} direction="right">
{phone && ( <div className="space-y-4">
<AnimateIn delay={100} direction="right"> {/* Phone Card */}
<div className="flex items-center gap-4 bg-white p-5 border-2 border-[#1f1f1f] hover-lift"> {phone && (
<div className="flex h-11 w-11 shrink-0 items-center justify-center bg-[#FFED00] text-[#1f1f1f] border border-[#1f1f1f]"> <a
href={`tel:${phone.replace(/\s+/g, '')}`}
className="flex items-start gap-4 p-5 bg-surface-gray border-2 border-dark hover:border-primary transition-all group block shadow-xs"
>
<div className="flex h-10 w-10 shrink-0 items-center justify-center bg-primary text-dark font-black">
<Phone className="h-5 w-5" /> <Phone className="h-5 w-5" />
</div> </div>
<div> <div>
{directPhoneLabel && ( {directPhoneLabel && (
<div className="text-[11px] font-black uppercase tracking-wider text-[#666666]"> <span className="text-[11px] font-black uppercase tracking-wider text-muted-text block">
{directPhoneLabel} {directPhoneLabel}
</div> </span>
)} )}
<a <span className="text-base sm:text-lg font-black uppercase text-dark font-mono group-hover:text-primary transition-colors">
href={`tel:${phone.replace(/\s+/g, '')}`}
className="text-base font-black text-[#1f1f1f] font-mono hover:underline"
>
{phone} {phone}
</a> </span>
</div> </div>
</div> </a>
</AnimateIn> )}
)}
{email && ( {/* Email Card */}
<AnimateIn delay={200} direction="right"> {email && (
<div className="flex items-center gap-4 bg-white p-5 border-2 border-[#1f1f1f] hover-lift"> <a
<div className="flex h-11 w-11 shrink-0 items-center justify-center bg-[#FFED00] text-[#1f1f1f] border border-[#1f1f1f]"> href={`mailto:${email}`}
className="flex items-start gap-4 p-5 bg-surface-gray border-2 border-dark hover:border-primary transition-all group block shadow-xs"
>
<div className="flex h-10 w-10 shrink-0 items-center justify-center bg-primary text-dark font-black">
<Mail className="h-5 w-5" /> <Mail className="h-5 w-5" />
</div> </div>
<div> <div>
{emailContactLabel && ( {emailContactLabel && (
<div className="text-[11px] font-black uppercase tracking-wider text-[#666666]"> <span className="text-[11px] font-black uppercase tracking-wider text-muted-text block">
{emailContactLabel} {emailContactLabel}
</div> </span>
)} )}
<a href={`mailto:${email}`} className="text-sm font-bold text-[#1f1f1f] hover:underline"> <span className="text-sm sm:text-base font-bold text-dark group-hover:text-primary transition-colors">
{email} {email}
</a> </span>
</div>
</a>
)}
{/* Address Card */}
{address && (
<div className="flex items-start gap-4 p-5 bg-surface-gray border-2 border-dark shadow-xs">
<div className="flex h-10 w-10 shrink-0 items-center justify-center bg-primary text-dark font-black">
<MapPin className="h-5 w-5" />
</div>
<div>
{addressLabel && (
<span className="text-[11px] font-black uppercase tracking-wider text-muted-text block">
{addressLabel}
</span>
)}
<span className="text-sm sm:text-base font-bold text-dark">
{address}
</span>
</div> </div>
</div> </div>
</AnimateIn> )}
)}
{workingHours && ( {/* Working Hours Card */}
<AnimateIn delay={300} direction="right"> {workingHours && (
<div className="flex items-center gap-4 bg-white p-5 border-2 border-[#1f1f1f] hover-lift"> <div className="flex items-start gap-4 p-5 bg-surface-gray border-2 border-dark shadow-xs">
<div className="flex h-11 w-11 shrink-0 items-center justify-center bg-[#FFED00] text-[#1f1f1f] border border-[#1f1f1f]"> <div className="flex h-10 w-10 shrink-0 items-center justify-center bg-primary text-dark font-black">
<Clock className="h-5 w-5" /> <Clock className="h-5 w-5" />
</div> </div>
<div> <div>
{workingHoursLabel && ( {workingHoursLabel && (
<div className="text-[11px] font-black uppercase tracking-wider text-[#666666]"> <span className="text-[11px] font-black uppercase tracking-wider text-muted-text block">
{workingHoursLabel} {workingHoursLabel}
</div> </span>
)} )}
<div className="text-sm font-bold text-[#1f1f1f]"> <span className="text-sm sm:text-base font-bold text-dark">
{workingHours} {workingHours}
</div> </span>
</div> </div>
</div> </div>
</AnimateIn> )}
)} </div>
</div> </AnimateIn>
</div> </div>
{/* Right Form Box */} {/* Right Column: Built-in Form Engine Form */}
<div className="lg:col-span-3"> <div className="lg:col-span-7">
<AnimateIn delay={200} direction="left"> <AnimateIn delay={200} direction="left">
<div className="bg-white p-8 sm:p-12 border-2 border-[#1f1f1f] shadow-md border-t-8 border-t-[#FFED00]"> <div className="bg-white border-2 border-dark p-8 sm:p-10 shadow-2xl relative">
{submitted ? ( {isSubmitted ? (
<div className="text-center py-12 space-y-4"> <div className="py-12 text-center space-y-4">
<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-16 w-16 mx-auto items-center justify-center bg-primary text-dark border-2 border-dark shadow-md">
<Check className="h-8 w-8 font-black" /> <CheckCircle className="h-8 w-8 text-dark stroke-[2.5]" />
</div> </div>
{successHeading && ( <h3 className="text-xl sm:text-2xl font-black uppercase tracking-tight text-dark">
<h3 className="text-xl font-black uppercase text-[#1f1f1f]"> Dziękujemy za zgłoszenie!
{successHeading} </h3>
</h3> <p className="text-sm text-[#555555] max-w-md mx-auto font-medium leading-relaxed">
)} Twoja wiadomość została zapisana w systemie. Nasz doradca techniczny skontaktuje się z Tobą w najbliższym czasie.
{successMessage && ( </p>
<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-6">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5"> {/* Error Banner */}
<div> {errorMessage && (
{nameLabel && ( <div className="p-4 bg-accent/10 border-2 border-accent text-accent flex items-start gap-3 text-xs sm:text-sm font-bold">
<label <AlertCircle className="h-5 w-5 shrink-0 mt-0.5" />
htmlFor="form-name" <span>{errorMessage}</span>
className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2"
>
{nameLabel}
</label>
)}
<input
id="form-name"
name="name"
type="text"
required
aria-label={nameLabel || 'Imię i Nazwisko'}
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> )}
{phoneInputLabel && (
<label
htmlFor="form-phone"
className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2"
>
{phoneInputLabel}
</label>
)}
<input
id="form-phone"
name="phone"
type="tel"
required
aria-label={phoneInputLabel || 'Numer Telefonu'}
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>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5"> {/* Dynamic Fields Grid */}
<div> <div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
{locationLabel && ( {fields.map((field, idx) => {
<label const fieldName = field.name || `field_${idx}`
htmlFor="form-location" const isFullWidth =
className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2" field.blockType === 'textarea' ||
> field.blockType === 'checkbox' ||
{locationLabel} field.blockType === 'message' ||
</label> (field.width && field.width >= 100)
)}
<input
id="form-location"
name="location"
type="text"
aria-label={locationLabel || 'Miejscowość'}
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>
{areaLabel && (
<label
htmlFor="form-area"
className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2"
>
{areaLabel}
</label>
)}
<input
id="form-area"
name="area"
type="text"
aria-label={areaLabel || 'Szacowany Metraż'}
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> if (field.blockType === 'textarea') {
{messageLabel && ( return (
<label <div key={field.id || idx} className="sm:col-span-2">
htmlFor="form-message" {field.label && (
className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2" <label
> htmlFor={fieldName}
{messageLabel} className="block text-xs font-black uppercase tracking-wider text-dark mb-2"
</label> >
)} {field.label} {field.required && <span className="text-accent">*</span>}
<textarea </label>
id="form-message" )}
name="message" <textarea
rows={3} id={fieldName}
aria-label={messageLabel || 'Zakres prac'} rows={4}
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" required={field.required}
/> value={(formData[fieldName] as string) || ''}
</div> onChange={(e) => handleInputChange(fieldName, e.target.value)}
className="w-full bg-white border-2 border-dark p-3.5 text-sm font-medium text-dark focus:border-primary focus:outline-none transition-colors resize-y"
{/* Custom Styled Kärcher Checkbox */}
{consentText && (
<div className="pt-2">
<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">
<input
id="consent"
name="consent"
type="checkbox"
required
checked={consentChecked}
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'
}`}
>
<Check
className={`w-3.5 h-3.5 text-[#1f1f1f] stroke-[3] transition-opacity ${
consentChecked ? 'opacity-100' : 'opacity-0'
}`}
/> />
</div> </div>
</div> )
<span className="text-xs text-[#555555] leading-relaxed group-hover:text-[#1f1f1f] transition-colors"> }
{consentText}
</span>
</label>
</div>
)}
{submitButtonText && ( if (field.blockType === 'checkbox') {
return (
<div key={field.id || idx} className="sm:col-span-2 pt-2">
<label className="flex items-start gap-3 cursor-pointer select-none">
<input
type="checkbox"
id={fieldName}
required={field.required}
checked={Boolean(formData[fieldName])}
onChange={(e) => handleInputChange(fieldName, e.target.checked)}
className="h-5 w-5 shrink-0 border-2 border-dark checked:bg-primary checked:border-dark appearance-none transition-all cursor-pointer relative checked:after:content-['✓'] checked:after:text-dark checked:after:font-black checked:after:text-xs checked:after:absolute checked:after:inset-0 checked:after:flex checked:after:items-center checked:after:justify-center"
/>
{field.label && (
<span className="text-xs text-[#555555] leading-relaxed font-medium">
{field.label} {field.required && <span className="text-accent">*</span>}
</span>
)}
</label>
</div>
)
}
if (field.blockType === 'select') {
return (
<div key={field.id || idx} className={isFullWidth ? 'sm:col-span-2' : 'sm:col-span-1'}>
{field.label && (
<label
htmlFor={fieldName}
className="block text-xs font-black uppercase tracking-wider text-dark mb-2"
>
{field.label} {field.required && <span className="text-accent">*</span>}
</label>
)}
<select
id={fieldName}
required={field.required}
value={(formData[fieldName] as string) || ''}
onChange={(e) => handleInputChange(fieldName, e.target.value)}
className="w-full bg-white border-2 border-dark p-3.5 text-sm font-medium text-dark focus:border-primary focus:outline-none transition-colors"
>
<option value="">Wybierz opcję...</option>
{field.options?.map((opt) => (
<option key={opt.id || opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
)
}
// Default: Text, Email, Number
const inputType =
field.blockType === 'email'
? 'email'
: field.blockType === 'number'
? 'number'
: 'text'
return (
<div key={field.id || idx} className={isFullWidth ? 'sm:col-span-2' : 'sm:col-span-1'}>
{field.label && (
<label
htmlFor={fieldName}
className="block text-xs font-black uppercase tracking-wider text-dark mb-2"
>
{field.label} {field.required && <span className="text-accent">*</span>}
</label>
)}
<input
type={inputType}
id={fieldName}
required={field.required}
value={(formData[fieldName] as string) || ''}
onChange={(e) => handleInputChange(fieldName, e.target.value)}
className="w-full bg-white border-2 border-dark p-3.5 text-sm font-medium text-dark focus:border-primary focus:outline-none transition-colors"
/>
</div>
)
})}
</div>
{/* Submit Button */}
<div className="pt-2">
<button <button
type="submit" type="submit"
disabled={loading || !consentChecked} disabled={isSubmitting}
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 flex items-center justify-center gap-3 bg-accent hover:bg-accent-hover text-white border-2 border-dark py-4 px-8 text-xs sm:text-sm font-black uppercase tracking-widest transition-all shadow-md hover:-translate-y-0.5 cursor-pointer disabled:opacity-60 disabled:cursor-not-allowed"
> >
<Send className="h-4 w-4" /> {isSubmitting ? (
<span>{submitButtonText}</span> <>
<Loader2 className="h-4 w-4 animate-spin" />
<span>Wysyłanie...</span>
</>
) : (
<>
<span>{formDoc?.submitButtonLabel || 'Wyślij zapytanie'}</span>
<ArrowRight className="h-4 w-4" />
</>
)}
</button> </button>
)} </div>
</form> </form>
)} )}
</div> </div>
+15 -54
View File
@@ -7,6 +7,15 @@ export const FormBlock: Block = {
plural: 'Sekcje Formularza i Kontaktu', plural: 'Sekcje Formularza i Kontaktu',
}, },
fields: [ fields: [
{
name: 'form',
type: 'relationship',
relationTo: 'forms',
required: true,
admin: {
description: 'Formularz z wbudowanego modułu Forms (zarządzanie polami, walidacją i powiadomieniami)',
},
},
{ {
name: 'badge', name: 'badge',
type: 'text', type: 'text',
@@ -26,7 +35,6 @@ export const FormBlock: Block = {
{ {
name: 'phone', name: 'phone',
type: 'text', type: 'text',
localized: true,
}, },
{ {
name: 'directPhoneLabel', name: 'directPhoneLabel',
@@ -37,7 +45,6 @@ export const FormBlock: Block = {
{ {
name: 'email', name: 'email',
type: 'text', type: 'text',
localized: true,
}, },
{ {
name: 'emailContactLabel', name: 'emailContactLabel',
@@ -57,60 +64,14 @@ export const FormBlock: Block = {
defaultValue: 'Godziny pracy', defaultValue: 'Godziny pracy',
}, },
{ {
name: 'nameLabel', name: 'address',
type: 'text',
},
{
name: 'addressLabel',
type: 'text', type: 'text',
localized: true, localized: true,
defaultValue: 'Imię i Nazwisko *', defaultValue: 'Adres biura',
},
{
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',
type: 'text',
localized: true,
defaultValue: 'Wyślij zapytanie o bezpłatną wycenę',
},
{
name: 'consentText',
type: 'textarea',
localized: true,
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.',
}, },
], ],
} }
+18 -11
View File
@@ -16,6 +16,7 @@ interface HeroBlockProps {
ctaPhoneLabel?: string ctaPhoneLabel?: string
ctaSecondaryLabel?: string ctaSecondaryLabel?: string
ctaSecondaryUrl?: string ctaSecondaryUrl?: string
backgroundImage?: string | { url?: string } | null
} }
export function HeroBlockComponent(props: HeroBlockProps) { export function HeroBlockComponent(props: HeroBlockProps) {
@@ -28,14 +29,20 @@ export function HeroBlockComponent(props: HeroBlockProps) {
ctaPhoneLabel, ctaPhoneLabel,
ctaSecondaryLabel, ctaSecondaryLabel,
ctaSecondaryUrl, ctaSecondaryUrl,
backgroundImage,
} = props } = props
const heroImgSrc =
(typeof backgroundImage === 'object' && backgroundImage?.url) ||
(typeof backgroundImage === 'string' && backgroundImage) ||
'/images/paving-hero.webp'
return ( return (
<div className="w-full"> <div className="w-full">
{/* Hero Banner Section with Next.js 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">
<Image <Image
src="/images/paving-hero.webp" src={heroImgSrc}
alt={heading} alt={heading}
fill fill
priority priority
@@ -44,19 +51,19 @@ export function HeroBlockComponent(props: HeroBlockProps) {
/> />
{/* Dark gradient overlay for text readability */} {/* Dark gradient overlay for text readability */}
<div className="absolute inset-0 bg-gradient-to-r from-[#141414]/90 via-[#141414]/65 to-black/20 z-[1]" /> <div className="absolute inset-0 bg-gradient-to-r from-dark/95 via-dark/70 to-black/30 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">
{/* 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-dark p-6 sm:p-8 lg:p-10 max-w-lg shadow-2xl border-l-8 border-l-primary">
{badge && ( {badge && (
<div className="inline-block bg-[#1f1f1f] text-[#FFED00] text-[11px] font-black uppercase tracking-widest px-3 py-1 mb-4"> <div className="inline-block bg-dark text-primary text-[11px] font-black uppercase tracking-widest px-3 py-1 mb-4">
{badge} {badge}
</div> </div>
)} )}
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f] leading-snug mb-4"> <h1 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-dark leading-snug mb-4">
{heading} {heading}
</h1> </h1>
@@ -72,7 +79,7 @@ export function HeroBlockComponent(props: HeroBlockProps) {
{ctaPhone && ctaPhoneLabel && ( {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-accent hover:bg-accent-hover text-white border-2 border-dark 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" /> <Phone className="h-4 w-4" />
<span>{ctaPhoneLabel}</span> <span>{ctaPhoneLabel}</span>
@@ -81,10 +88,10 @@ export function HeroBlockComponent(props: HeroBlockProps) {
{ctaSecondaryLabel && ctaSecondaryUrl && ( {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-dark hover:bg-dark-hover text-white px-6 py-3.5 text-xs font-black uppercase tracking-widest transition-all hover:-translate-y-0.5"
> >
<span>{ctaSecondaryLabel}</span> <span>{ctaSecondaryLabel}</span>
<ArrowRight className="h-3.5 w-3.5 text-[#FFED00]" /> <ArrowRight className="h-3.5 w-3.5 text-primary" />
</a> </a>
)} )}
</div> </div>
@@ -96,14 +103,14 @@ export function HeroBlockComponent(props: HeroBlockProps) {
{/* Full-Width Horizontal Feature Strip Directly Under Hero */} {/* 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-dark text-white border-y-2 border-primary 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">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6">
{trustBadges.slice(0, 4).map((badgeItem, idx) => ( {trustBadges.slice(0, 4).map((badgeItem, idx) => (
<AnimateIn key={idx} delay={idx * 100} direction="up"> <AnimateIn key={idx} delay={idx * 100} direction="up">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="flex h-6 w-6 shrink-0 items-center justify-center bg-[#FFED00] text-[#1f1f1f] border border-[#1f1f1f]"> <span className="flex h-6 w-6 shrink-0 items-center justify-center bg-primary text-dark border border-dark">
<Check className="h-4 w-4 text-[#1f1f1f] stroke-[3]" /> <Check className="h-4 w-4 text-dark stroke-[3]" />
</span> </span>
<span className="text-xs font-black uppercase tracking-tight text-[#f4f4f4] leading-snug"> <span className="text-xs font-black uppercase tracking-tight text-[#f4f4f4] leading-snug">
{badgeItem.text} {badgeItem.text}
+15 -15
View File
@@ -29,20 +29,20 @@ export function PricingTableBlockComponent(props: PricingTableBlockProps) {
const { badge, heading, subheading, disclaimer, packages } = props const { badge, heading, subheading, disclaimer, packages } = props
return ( return (
<section id="cennik" className="py-24 bg-[#f4f4f4] border-b-2 border-[#1f1f1f] scroll-mt-20"> <section id="cennik" className="py-24 bg-surface-gray border-b-2 border-dark scroll-mt-20">
<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">
<AnimateIn direction="up"> <AnimateIn direction="up">
<div className="max-w-3xl mb-14"> <div className="max-w-3xl mb-14">
{badge && ( {badge && (
<div className="inline-block bg-[#1f1f1f] text-[#FFED00] text-xs font-black uppercase tracking-widest px-3 py-1 mb-4"> <div className="inline-block bg-dark text-primary text-xs font-black uppercase tracking-widest px-3 py-1 mb-4">
{badge} {badge}
</div> </div>
)} )}
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f]"> <h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-dark">
{heading} {heading}
</h2> </h2>
{subheading && ( {subheading && (
<p className="mt-4 text-base text-[#666666] leading-relaxed font-medium"> <p className="mt-4 text-base text-muted-text leading-relaxed font-medium">
{subheading} {subheading}
</p> </p>
)} )}
@@ -54,28 +54,28 @@ export function PricingTableBlockComponent(props: PricingTableBlockProps) {
{packages.map((pkg, idx) => ( {packages.map((pkg, idx) => (
<AnimateIn key={idx} delay={idx * 150} direction="up" className="h-full"> <AnimateIn key={idx} delay={idx * 150} direction="up" className="h-full">
<div <div
className={`relative p-8 h-full flex flex-col justify-between bg-white border-2 border-[#1f1f1f] hover-lift transition-all ${ className={`relative p-8 h-full flex flex-col justify-between bg-white border-2 border-dark hover-lift transition-all ${
pkg.isPopular ? 'border-t-8 border-t-[#FFED00] shadow-md' : '' pkg.isPopular ? 'border-t-8 border-t-accent shadow-md' : ''
}`} }`}
> >
{pkg.isPopular && pkg.popularBadgeLabel && ( {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-accent text-white border-b-2 border-l-2 border-dark text-[10px] font-black uppercase tracking-widest px-4 py-1">
{pkg.popularBadgeLabel} {pkg.popularBadgeLabel}
</div> </div>
)} )}
<div> <div>
<h3 className="text-lg font-black uppercase text-[#1f1f1f]"> <h3 className="text-lg font-black uppercase text-dark">
{pkg.name} {pkg.name}
</h3> </h3>
{pkg.description && ( {pkg.description && (
<p className="mt-2 text-xs text-[#666666] leading-relaxed min-h-[36px]"> <p className="mt-2 text-xs text-muted-text leading-relaxed min-h-[36px]">
{pkg.description} {pkg.description}
</p> </p>
)} )}
<div className="mt-6 mb-8 pb-6 border-b-2 border-[#f0f0f0] flex items-baseline gap-2"> <div className="mt-6 mb-8 pb-6 border-b-2 border-[#f0f0f0] flex items-baseline gap-2">
<span className="text-3xl sm:text-4xl font-black text-[#1f1f1f] tracking-tight font-mono"> <span className="text-3xl sm:text-4xl font-black text-dark tracking-tight font-mono">
{pkg.price} {pkg.price}
</span> </span>
</div> </div>
@@ -83,8 +83,8 @@ export function PricingTableBlockComponent(props: PricingTableBlockProps) {
{pkg.features && pkg.features.length > 0 && ( {pkg.features && pkg.features.length > 0 && (
<ul className="space-y-3 text-xs sm:text-sm"> <ul className="space-y-3 text-xs sm:text-sm">
{pkg.features.map((feat, fIdx) => ( {pkg.features.map((feat, fIdx) => (
<li key={fIdx} className="flex items-start gap-3 text-[#222222]"> <li key={fIdx} className="flex items-start gap-3 text-dark">
<Check className="h-4 w-4 text-[#1f1f1f] bg-[#FFED00] p-0.5 shrink-0 mt-0.5 font-bold" /> <Check className="h-4 w-4 text-dark bg-primary p-0.5 shrink-0 mt-0.5 font-bold" />
<span className="font-semibold">{feat.feature}</span> <span className="font-semibold">{feat.feature}</span>
</li> </li>
))} ))}
@@ -97,8 +97,8 @@ export function PricingTableBlockComponent(props: PricingTableBlockProps) {
href={pkg.ctaUrl || '#formularz-kontaktowy'} href={pkg.ctaUrl || '#formularz-kontaktowy'}
className={`w-full inline-flex items-center justify-center py-4 px-6 text-xs font-black uppercase tracking-widest transition-all ${ className={`w-full inline-flex items-center justify-center py-4 px-6 text-xs font-black uppercase tracking-widest transition-all ${
pkg.isPopular pkg.isPopular
? 'bg-[#FFED00] hover:bg-[#fae100] text-[#1f1f1f] border-2 border-[#1f1f1f] shadow-xs' ? 'bg-accent hover:bg-accent-hover text-white border-2 border-dark shadow-xs'
: 'bg-[#1f1f1f] text-white hover:bg-[#333333]' : 'bg-dark text-white hover:bg-dark-hover'
}`} }`}
> >
{pkg.ctaLabel || 'Wybierz pakiet'} {pkg.ctaLabel || 'Wybierz pakiet'}
@@ -112,7 +112,7 @@ export function PricingTableBlockComponent(props: PricingTableBlockProps) {
{disclaimer && ( {disclaimer && (
<AnimateIn delay={400} direction="none"> <AnimateIn delay={400} direction="none">
<p className="mt-10 text-xs text-[#666666] font-medium"> <p className="mt-10 text-xs text-muted-text font-medium">
{disclaimer} {disclaimer}
</p> </p>
</AnimateIn> </AnimateIn>
+10 -10
View File
@@ -20,20 +20,20 @@ export function ProblemSolutionBlockComponent(props: ProblemSolutionBlockProps)
const { badge, heading, subheading, items } = props const { badge, heading, subheading, items } = props
return ( return (
<section className="py-24 bg-white border-b-2 border-[#1f1f1f]"> <section className="py-24 bg-white border-b-2 border-dark">
<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">
<AnimateIn direction="up"> <AnimateIn direction="up">
<div className="max-w-3xl mb-14"> <div className="max-w-3xl mb-14">
{badge && ( {badge && (
<div className="inline-block bg-[#FFED00] text-[#1f1f1f] text-xs font-black uppercase tracking-widest px-3 py-1 mb-4 border border-[#1f1f1f]"> <div className="inline-block bg-primary text-dark text-xs font-black uppercase tracking-widest px-3 py-1 mb-4 border border-dark">
{badge} {badge}
</div> </div>
)} )}
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f]"> <h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-dark">
{heading} {heading}
</h2> </h2>
{subheading && ( {subheading && (
<p className="mt-4 text-base text-[#666666] leading-relaxed font-medium"> <p className="mt-4 text-base text-muted-text leading-relaxed font-medium">
{subheading} {subheading}
</p> </p>
)} )}
@@ -45,19 +45,19 @@ export function ProblemSolutionBlockComponent(props: ProblemSolutionBlockProps)
{items.map((item, idx) => ( {items.map((item, idx) => (
<AnimateIn key={idx} delay={idx * 150} direction="up" className="h-full"> <AnimateIn key={idx} delay={idx * 150} direction="up" className="h-full">
<div <div
className="bg-[#f8f8f8] border-2 border-[#1f1f1f] p-8 space-y-6 shadow-sm hover-lift h-full flex flex-col justify-between" className="bg-[#f8f8f8] border-2 border-dark p-8 space-y-6 shadow-sm hover-lift h-full flex flex-col justify-between"
> >
{/* Problem */} {/* Problem */}
<div className="flex items-start gap-4 pb-6 border-b border-[#e5e5e5]"> <div className="flex items-start gap-4 pb-6 border-b border-[#e5e5e5]">
<div className="flex h-8 w-8 shrink-0 items-center justify-center bg-rose-600 text-white font-black"> <div className="flex h-8 w-8 shrink-0 items-center justify-center bg-accent text-white font-black">
<X className="h-5 w-5" /> <X className="h-5 w-5" />
</div> </div>
<div> <div>
<h4 className="text-sm font-black uppercase tracking-wide text-rose-950"> <h4 className="text-sm font-black uppercase tracking-wide text-accent">
{item.problemTitle} {item.problemTitle}
</h4> </h4>
{item.problemDescription && ( {item.problemDescription && (
<p className="mt-1 text-xs sm:text-sm text-[#666666] leading-relaxed font-medium"> <p className="mt-1 text-xs sm:text-sm text-muted-text leading-relaxed font-medium">
{item.problemDescription} {item.problemDescription}
</p> </p>
)} )}
@@ -66,11 +66,11 @@ export function ProblemSolutionBlockComponent(props: ProblemSolutionBlockProps)
{/* Solution */} {/* Solution */}
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="flex h-8 w-8 shrink-0 items-center justify-center bg-[#FFED00] text-[#1f1f1f] border border-[#1f1f1f] font-black"> <div className="flex h-8 w-8 shrink-0 items-center justify-center bg-primary text-dark border border-dark font-black">
<Check className="h-5 w-5" /> <Check className="h-5 w-5" />
</div> </div>
<div> <div>
<h4 className="text-sm font-black uppercase tracking-wide text-[#1f1f1f]"> <h4 className="text-sm font-black uppercase tracking-wide text-dark">
{item.solutionTitle} {item.solutionTitle}
</h4> </h4>
{item.solutionDescription && ( {item.solutionDescription && (
+20 -9
View File
@@ -12,26 +12,37 @@ interface ProcessStepsBlockProps {
heading: string heading: string
subheading?: string subheading?: string
steps?: Step[] steps?: Step[]
backgroundImage?: string | { url?: string } | null
} }
export function ProcessStepsBlockComponent(props: ProcessStepsBlockProps) { export function ProcessStepsBlockComponent(props: ProcessStepsBlockProps) {
const { badge, heading, subheading, steps } = props const { badge, heading, subheading, steps, backgroundImage } = props
const bgUrl =
(typeof backgroundImage === 'object' && backgroundImage?.url) ||
(typeof backgroundImage === 'string' && backgroundImage) ||
'/images/process-parallax.webp'
return ( return (
<section className="py-24 bg-[#f4f4f4] border-b-2 border-[#1f1f1f]"> <section
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl"> className="relative py-28 border-b-2 border-dark bg-fixed bg-center bg-cover overflow-hidden"
style={{ backgroundImage: `url('${bgUrl}')` }}
>
{/* Dark overlay for contrast */}
<div className="absolute inset-0 bg-dark/85 backdrop-blur-[1px]" />
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl relative z-10">
<AnimateIn direction="up"> <AnimateIn direction="up">
<div className="max-w-3xl mb-14"> <div className="max-w-3xl mb-14">
{badge && ( {badge && (
<div className="inline-block bg-[#1f1f1f] text-[#FFED00] text-xs font-black uppercase tracking-widest px-3 py-1 mb-4"> <div className="inline-block bg-primary text-dark text-xs font-black uppercase tracking-widest px-3 py-1 mb-4 border border-dark">
{badge} {badge}
</div> </div>
)} )}
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f]"> <h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-white">
{heading} {heading}
</h2> </h2>
{subheading && ( {subheading && (
<p className="mt-4 text-base text-[#666666] leading-relaxed font-medium"> <p className="mt-4 text-base text-[#e0e0e0] leading-relaxed font-medium">
{subheading} {subheading}
</p> </p>
)} )}
@@ -43,13 +54,13 @@ export function ProcessStepsBlockComponent(props: ProcessStepsBlockProps) {
{steps.map((step, idx) => ( {steps.map((step, idx) => (
<AnimateIn key={idx} delay={idx * 100} direction="up" className="h-full"> <AnimateIn key={idx} delay={idx * 100} direction="up" className="h-full">
<div <div
className="bg-white border-2 border-[#1f1f1f] p-6 h-full flex flex-col justify-between border-t-8 border-t-[#FFED00] hover-lift transition-all" className="bg-white/95 backdrop-blur-md border-2 border-dark p-6 h-full flex flex-col justify-between border-t-8 border-t-primary hover-lift transition-all shadow-xl"
> >
<div> <div>
<div className="text-2xl font-black text-[#1f1f1f] bg-[#FFED00] border border-[#1f1f1f] w-10 h-10 flex items-center justify-center mb-5 font-mono"> <div className="text-2xl font-black text-dark bg-primary border border-dark w-10 h-10 flex items-center justify-center mb-5 font-mono">
{step.stepNumber} {step.stepNumber}
</div> </div>
<h4 className="text-sm font-black uppercase tracking-tight text-[#1f1f1f] mb-3"> <h4 className="text-sm font-black uppercase tracking-tight text-dark mb-3">
{step.title} {step.title}
</h4> </h4>
{step.description && ( {step.description && (
+8
View File
@@ -52,5 +52,13 @@ export const ProcessStepsBlock: Block = {
}, },
], ],
}, },
{
name: 'backgroundImage',
type: 'upload',
relationTo: 'media',
admin: {
description: 'Opcjonalne tło ze zdjęciem (efekt parallax)',
},
},
], ],
} }
+9 -9
View File
@@ -20,20 +20,20 @@ export function ReviewsBlockComponent(props: ReviewsBlockProps) {
const { badge, heading, subheading, items } = props const { badge, heading, subheading, items } = props
return ( return (
<section className="py-24 bg-white border-b-2 border-[#1f1f1f]"> <section className="py-24 bg-white border-b-2 border-dark">
<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">
<AnimateIn direction="up"> <AnimateIn direction="up">
<div className="max-w-3xl mb-14"> <div className="max-w-3xl mb-14">
{badge && ( {badge && (
<div className="inline-block bg-[#FFED00] text-[#1f1f1f] text-xs font-black uppercase tracking-widest px-3 py-1 mb-4 border border-[#1f1f1f]"> <div className="inline-block bg-primary text-dark text-xs font-black uppercase tracking-widest px-3 py-1 mb-4 border border-dark">
{badge} {badge}
</div> </div>
)} )}
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f]"> <h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-dark">
{heading} {heading}
</h2> </h2>
{subheading && ( {subheading && (
<p className="mt-4 text-base text-[#666666] leading-relaxed font-medium"> <p className="mt-4 text-base text-muted-text leading-relaxed font-medium">
{subheading} {subheading}
</p> </p>
)} )}
@@ -45,12 +45,12 @@ export function ReviewsBlockComponent(props: ReviewsBlockProps) {
{items.map((item, idx) => ( {items.map((item, idx) => (
<AnimateIn key={idx} delay={idx * 150} direction="up" className="h-full"> <AnimateIn key={idx} delay={idx * 150} direction="up" className="h-full">
<div <div
className="bg-[#f4f4f4] border-2 border-[#1f1f1f] p-8 h-full flex flex-col justify-between hover-lift transition-all" className="bg-surface-gray border-2 border-dark p-8 h-full flex flex-col justify-between hover-lift transition-all"
> >
<div> <div>
<div className="flex items-center gap-1 text-[#1f1f1f] mb-6"> <div className="flex items-center gap-1 text-dark mb-6">
{Array.from({ length: item.rating || 5 }).map((_, sIdx) => ( {Array.from({ length: item.rating || 5 }).map((_, sIdx) => (
<Star key={sIdx} className="h-5 w-5 fill-[#FFED00] stroke-[#1f1f1f]" /> <Star key={sIdx} className="h-5 w-5 fill-primary stroke-dark" />
))} ))}
</div> </div>
<p className="text-sm text-[#333333] leading-relaxed font-medium"> <p className="text-sm text-[#333333] leading-relaxed font-medium">
@@ -59,11 +59,11 @@ export function ReviewsBlockComponent(props: ReviewsBlockProps) {
</div> </div>
<div className="mt-8 pt-6 border-t-2 border-[#e0e0e0]"> <div className="mt-8 pt-6 border-t-2 border-[#e0e0e0]">
<div className="font-black uppercase text-[#1f1f1f] text-xs tracking-wider"> <div className="font-black uppercase text-dark text-xs tracking-wider">
{item.author} {item.author}
</div> </div>
{item.location && ( {item.location && (
<div className="text-xs text-[#666666] mt-0.5 font-medium"> <div className="text-xs text-muted-text mt-0.5 font-medium">
{item.location} {item.location}
</div> </div>
)} )}
+20 -9
View File
@@ -12,26 +12,37 @@ interface TechEquipmentBlockProps {
heading: string heading: string
subheading?: string subheading?: string
items?: TechItem[] items?: TechItem[]
backgroundImage?: string | { url?: string } | null
} }
export function TechEquipmentBlockComponent(props: TechEquipmentBlockProps) { export function TechEquipmentBlockComponent(props: TechEquipmentBlockProps) {
const { badge, heading, subheading, items } = props const { badge, heading, subheading, items, backgroundImage } = props
const bgUrl =
(typeof backgroundImage === 'object' && backgroundImage?.url) ||
(typeof backgroundImage === 'string' && backgroundImage) ||
'/images/paving-parallax.webp'
return ( return (
<section className="py-24 bg-white border-b-2 border-[#1f1f1f]"> <section
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl"> className="relative py-28 border-b-2 border-dark bg-fixed bg-center bg-cover overflow-hidden"
style={{ backgroundImage: `url('${bgUrl}')` }}
>
{/* Dark overlay for contrast */}
<div className="absolute inset-0 bg-dark/85 backdrop-blur-[1px]" />
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl relative z-10">
<AnimateIn direction="up"> <AnimateIn direction="up">
<div className="max-w-3xl mb-14"> <div className="max-w-3xl mb-14">
{badge && ( {badge && (
<div className="inline-block bg-[#FFED00] text-[#1f1f1f] text-xs font-black uppercase tracking-widest px-3 py-1 mb-4 border border-[#1f1f1f]"> <div className="inline-block bg-primary text-dark text-xs font-black uppercase tracking-widest px-3 py-1 mb-4 border border-dark">
{badge} {badge}
</div> </div>
)} )}
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f]"> <h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-white">
{heading} {heading}
</h2> </h2>
{subheading && ( {subheading && (
<p className="mt-4 text-base text-[#666666] leading-relaxed font-medium"> <p className="mt-4 text-base text-[#e0e0e0] leading-relaxed font-medium">
{subheading} {subheading}
</p> </p>
)} )}
@@ -43,13 +54,13 @@ export function TechEquipmentBlockComponent(props: TechEquipmentBlockProps) {
{items.map((item, idx) => ( {items.map((item, idx) => (
<AnimateIn key={idx} delay={idx * 150} direction="up" className="h-full"> <AnimateIn key={idx} delay={idx * 150} direction="up" className="h-full">
<div <div
className="bg-[#f4f4f4] border-2 border-[#1f1f1f] p-8 h-full flex flex-col justify-between hover-lift transition-all" className="bg-white/95 backdrop-blur-md border-2 border-dark p-8 h-full flex flex-col justify-between hover-lift transition-all shadow-xl"
> >
<div> <div>
<div className="flex h-10 w-10 items-center justify-center bg-[#FFED00] text-[#1f1f1f] border border-[#1f1f1f] mb-6"> <div className="flex h-10 w-10 items-center justify-center bg-primary text-dark border border-dark mb-6">
<Check className="h-5 w-5 font-black" /> <Check className="h-5 w-5 font-black" />
</div> </div>
<h3 className="text-base font-black uppercase text-[#1f1f1f] mb-3"> <h3 className="text-base font-black uppercase text-dark mb-3">
{item.title} {item.title}
</h3> </h3>
{item.description && ( {item.description && (
+8
View File
@@ -49,5 +49,13 @@ export const TechEquipmentBlock: Block = {
}, },
], ],
}, },
{
name: 'backgroundImage',
type: 'upload',
relationTo: 'media',
admin: {
description: 'Opcjonalne tło ze zdjęciem (efekt parallax)',
},
},
], ],
} }
+20 -20
View File
@@ -61,12 +61,12 @@ export function CustomCookieBanner() {
<div <div
role="dialog" role="dialog"
aria-label="Cookie consent" aria-label="Cookie consent"
className="fixed bottom-0 inset-x-0 z-50 bg-[#1f1f1f] text-white p-4 sm:p-6 border-t-4 border-[#FFED00] shadow-2xl" className="fixed bottom-0 inset-x-0 z-50 bg-dark text-white p-4 sm:p-6 border-t-4 border-primary shadow-2xl"
> >
<div className="container mx-auto max-w-5xl"> <div className="container mx-auto max-w-5xl">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-5"> <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-5">
<div className="flex items-start gap-3.5"> <div className="flex items-start gap-3.5">
<div className="p-2 bg-[#FFED00] text-[#1f1f1f] shrink-0 border border-[#1f1f1f]"> <div className="p-2 bg-primary text-dark shrink-0 border border-dark">
<Cookie className="h-5 w-5" /> <Cookie className="h-5 w-5" />
</div> </div>
<div className="text-xs sm:text-sm text-[#e0e0e0] leading-relaxed"> <div className="text-xs sm:text-sm text-[#e0e0e0] leading-relaxed">
@@ -75,7 +75,7 @@ export function CustomCookieBanner() {
{texts.privacyLink && ( {texts.privacyLink && (
<a <a
href={texts.privacyLink.href} href={texts.privacyLink.href}
className="text-[#FFED00] underline hover:no-underline font-bold" className="text-primary underline hover:no-underline font-bold"
> >
{texts.privacyLink.label} {texts.privacyLink.label}
</a> </a>
@@ -88,21 +88,21 @@ export function CustomCookieBanner() {
<button <button
type="button" type="button"
onClick={() => setShowSettings(true)} onClick={() => setShowSettings(true)}
className="bg-[#2b2b2b] hover:bg-[#383838] text-white border border-[#444444] px-4 py-2.5 text-xs font-bold uppercase tracking-wider transition-colors cursor-pointer" className="bg-[#332d2e] hover:bg-[#423b3c] text-white border border-[#4a4244] px-4 py-2.5 text-xs font-bold uppercase tracking-wider transition-colors cursor-pointer"
> >
{texts.buttons.settings} {texts.buttons.settings}
</button> </button>
<button <button
type="button" type="button"
onClick={handleRejectAll} onClick={handleRejectAll}
className="bg-[#2b2b2b] hover:bg-[#383838] text-white border border-[#444444] px-4 py-2.5 text-xs font-bold uppercase tracking-wider transition-colors cursor-pointer" className="bg-[#332d2e] hover:bg-[#423b3c] text-white border border-[#4a4244] px-4 py-2.5 text-xs font-bold uppercase tracking-wider transition-colors cursor-pointer"
> >
{texts.buttons.reject} {texts.buttons.reject}
</button> </button>
<button <button
type="button" type="button"
onClick={handleAcceptAll} onClick={handleAcceptAll}
className="bg-[#FFED00] hover:bg-[#fae100] text-[#1f1f1f] border-2 border-[#1f1f1f] px-6 py-2.5 text-xs font-black uppercase tracking-widest transition-all cursor-pointer shadow-xs" className="bg-primary hover:bg-primary-hover text-dark border-2 border-dark px-6 py-2.5 text-xs font-black uppercase tracking-widest transition-all cursor-pointer shadow-xs"
> >
{texts.buttons.acceptAll} {texts.buttons.acceptAll}
</button> </button>
@@ -112,7 +112,7 @@ export function CustomCookieBanner() {
</div> </div>
)} )}
{/* 2. Modal Window for Cookie Settings (Scroll-locked backdrop & isolated modal scrolling) */} {/* 2. Modal Window for Cookie Settings */}
{showSettings && ( {showSettings && (
<div <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" 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"
@@ -125,13 +125,13 @@ export function CustomCookieBanner() {
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 overscroll-contain" className="w-full max-w-2xl bg-dark text-white p-6 sm:p-8 border-t-4 border-primary shadow-2xl my-auto max-h-[90vh] flex flex-col overscroll-contain"
onClick={(e) => e.stopPropagation()} 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-[#363032] pb-4 mb-6 shrink-0">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-1.5 bg-[#FFED00] text-[#1f1f1f]"> <div className="p-1.5 bg-primary text-dark">
<Cookie className="h-5 w-5" /> <Cookie className="h-5 w-5" />
</div> </div>
<h2 className="text-base sm:text-lg font-black uppercase tracking-wider text-white"> <h2 className="text-base sm:text-lg font-black uppercase tracking-wider text-white">
@@ -161,12 +161,12 @@ export function CustomCookieBanner() {
<div <div
key={cat} key={cat}
onClick={() => !isDisabled && toggle(cat)} onClick={() => !isDisabled && toggle(cat)}
className={`flex items-start justify-between gap-4 p-4 border border-[#333333] bg-[#262626] ${ className={`flex items-start justify-between gap-4 p-4 border border-[#363032] bg-[#2d2728] ${
!isDisabled ? 'cursor-pointer hover:border-[#FFED00]' : 'opacity-90' !isDisabled ? 'cursor-pointer hover:border-primary' : 'opacity-90'
} transition-colors`} } transition-colors`}
> >
<div className="pr-4 select-none"> <div className="pr-4 select-none">
<div className="text-xs font-black uppercase tracking-wider text-[#FFED00]"> <div className="text-xs font-black uppercase tracking-wider text-primary">
{catData.title} {catData.title}
</div> </div>
<p className="text-xs text-[#a0a0a0] mt-1 leading-relaxed"> <p className="text-xs text-[#a0a0a0] mt-1 leading-relaxed">
@@ -174,15 +174,15 @@ export function CustomCookieBanner() {
</p> </p>
</div> </div>
{/* Custom Kärcher Checkbox */} {/* Custom Checkbox */}
<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">
<div <div
className={`w-5 h-5 border-2 border-[#1f1f1f] transition-all flex items-center justify-center ${ className={`w-5 h-5 border-2 border-dark transition-all flex items-center justify-center ${
isChecked ? 'bg-[#FFED00]' : 'bg-white' isChecked ? 'bg-primary' : 'bg-white'
}`} }`}
> >
<Check <Check
className={`w-3.5 h-3.5 text-[#1f1f1f] stroke-[3] transition-opacity ${ className={`w-3.5 h-3.5 text-dark stroke-[3] transition-opacity ${
isChecked ? 'opacity-100' : 'opacity-0' isChecked ? 'opacity-100' : 'opacity-0'
}`} }`}
/> />
@@ -194,18 +194,18 @@ export function CustomCookieBanner() {
</div> </div>
{/* Modal Footer Buttons */} {/* Modal Footer Buttons */}
<div className="flex flex-wrap items-center justify-end gap-3 pt-6 mt-6 border-t border-[#333333] shrink-0"> <div className="flex flex-wrap items-center justify-end gap-3 pt-6 mt-6 border-t border-[#363032] shrink-0">
<button <button
type="button" type="button"
onClick={() => setShowSettings(false)} onClick={() => setShowSettings(false)}
className="bg-[#2b2b2b] hover:bg-[#383838] text-white border border-[#444444] px-5 py-2.5 text-xs font-bold uppercase tracking-wider transition-colors cursor-pointer" className="bg-[#332d2e] hover:bg-[#423b3c] text-white border border-[#4a4244] px-5 py-2.5 text-xs font-bold uppercase tracking-wider transition-colors cursor-pointer"
> >
{texts.buttons.back} {texts.buttons.back}
</button> </button>
<button <button
type="button" type="button"
onClick={handleSave} onClick={handleSave}
className="bg-[#FFED00] hover:bg-[#fae100] text-[#1f1f1f] border-2 border-[#1f1f1f] px-6 py-2.5 text-xs font-black uppercase tracking-widest transition-all cursor-pointer shadow-xs" className="bg-primary hover:bg-primary-hover text-dark border-2 border-dark px-6 py-2.5 text-xs font-black uppercase tracking-widest transition-all cursor-pointer shadow-xs"
> >
{texts.buttons.save} {texts.buttons.save}
</button> </button>
+114 -55
View File
@@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import { MapPin, Phone, Mail, ShieldCheck, Cookie } from 'lucide-react' import { Phone, Mail, ShieldCheck, Cookie, FileText } from 'lucide-react'
import { Logo } from '@/components/Logo'
export interface FooterNavItem { export interface FooterNavItem {
id?: string id?: string
@@ -11,7 +12,9 @@ interface FooterProps {
companyName?: string | null companyName?: string | null
footerTagline?: string | null footerTagline?: string | null
nip?: string | null nip?: string | null
nipLabel?: string | null
regon?: string | null regon?: string | null
regonLabel?: string | null
address?: string | null address?: string | null
city?: string | null city?: string | null
phone?: string | null phone?: string | null
@@ -21,15 +24,22 @@ interface FooterProps {
privacyLabel?: string | null privacyLabel?: string | null
cookieHref?: string | null cookieHref?: string | null
cookieLabel?: string | null cookieLabel?: string | null
termsHref?: string | null
termsLabel?: string | null
navTitle?: string | null navTitle?: string | null
contactTitle?: string | null contactTitle?: string | null
legalTitle?: string | null
creditLabel?: string | null
creditHref?: string | null
} }
export function Footer({ export function Footer({
companyName, companyName,
footerTagline, footerTagline,
nip, nip,
nipLabel,
regon, regon,
regonLabel,
address, address,
city, city,
phone, phone,
@@ -39,50 +49,72 @@ export function Footer({
privacyLabel, privacyLabel,
cookieHref, cookieHref,
cookieLabel, cookieLabel,
termsHref,
termsLabel,
navTitle, navTitle,
contactTitle, contactTitle,
legalTitle,
creditLabel,
creditHref,
}: FooterProps) { }: FooterProps) {
return ( return (
<footer className="bg-[#1f1f1f] text-[#d0d0d0] py-16 border-t-4 border-[#FFED00]"> <footer className="bg-dark text-[#d0d0d0] pt-14 pb-8 border-t-4 border-primary">
<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">
<div className="grid grid-cols-1 md:grid-cols-4 gap-12"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-10 lg:gap-12">
{/* Company Info */} {/* 1. Company Info */}
<div className="md:col-span-2 space-y-4"> <div className="space-y-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center bg-[#FFED00] text-[#1f1f1f] font-black text-xl"> <Logo variant="dark" className="h-12 w-auto" alt={companyName || undefined} />
POI
</div>
{companyName && (
<span className="text-xl font-black uppercase tracking-tight text-white">
{companyName}
</span>
)}
</div> </div>
{footerTagline && ( {footerTagline && (
<p className="text-xs sm:text-sm text-[#a0a0a0] leading-relaxed max-w-md"> <p className="text-xs sm:text-sm text-[#a0a0a0] leading-relaxed">
{footerTagline} {footerTagline}
</p> </p>
)} )}
{(nip || regon) && ( {(companyName || address || city || nip || regon) && (
<div className="text-xs text-[#888888] font-mono space-y-1 pt-2 border-t border-[#333333]"> <div className="text-xs text-[#a0a0a0] space-y-1.5 pt-3 border-t border-[#363032]">
{nip && <div>NIP: <span className="text-[#e0e0e0] font-semibold">{nip}</span></div>} {companyName && (
{regon && <div>REGON: <span className="text-[#e0e0e0] font-semibold">{regon}</span></div>} <div className="font-bold text-white uppercase tracking-wide">
{companyName}
</div>
)}
{(address || city) && (
<div className="text-[#a0a0a0]">
{address}
{address && city ? ', ' : ''}
{city}
</div>
)}
<div className="text-xs text-[#888888] font-mono space-y-1 pt-1">
{nip && (
<div>
{nipLabel ? `${nipLabel}: ` : ''}
<span className="text-[#e0e0e0] font-semibold">{nip}</span>
</div>
)}
{regon && (
<div>
{regonLabel ? `${regonLabel}: ` : ''}
<span className="text-[#e0e0e0] font-semibold">{regon}</span>
</div>
)}
</div>
</div> </div>
)} )}
</div> </div>
{/* Navigation Links */} {/* 2. Navigation Links */}
{items && items.length > 0 && ( {items && items.length > 0 && (
<div className="space-y-4"> <div className="space-y-4">
{navTitle && ( {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-primary border-b border-[#363032] pb-2">
{navTitle} {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}>
<a href={item.href} className="text-[#d0d0d0] hover:text-[#FFED00] transition-colors block py-0.5"> <a href={item.href} className="text-[#d0d0d0] hover:text-primary transition-colors block py-0.5">
{item.label} {item.label}
</a> </a>
</li> </li>
@@ -91,65 +123,92 @@ export function Footer({
</div> </div>
)} )}
{/* Contact Details */} {/* 3. Legal & Policies Column */}
{(phone || email || address || city) && ( {(privacyHref || cookieHref || termsHref) && (
<div className="space-y-4">
{legalTitle && (
<div className="text-xs font-black uppercase tracking-widest text-primary border-b border-[#363032] pb-2">
{legalTitle}
</div>
)}
<ul className="space-y-2.5 text-xs font-bold uppercase tracking-wider">
{privacyHref && privacyLabel && (
<li>
<a href={privacyHref} className="text-[#d0d0d0] hover:text-primary transition-colors flex items-center gap-2 py-0.5">
<ShieldCheck className="h-3.5 w-3.5 text-primary shrink-0" />
<span>{privacyLabel}</span>
</a>
</li>
)}
{cookieHref && cookieLabel && (
<li>
<a href={cookieHref} className="text-[#d0d0d0] hover:text-primary transition-colors flex items-center gap-2 py-0.5">
<Cookie className="h-3.5 w-3.5 text-primary shrink-0" />
<span>{cookieLabel}</span>
</a>
</li>
)}
{termsHref && termsLabel && (
<li>
<a href={termsHref} className="text-[#d0d0d0] hover:text-primary transition-colors flex items-center gap-2 py-0.5">
<FileText className="h-3.5 w-3.5 text-primary shrink-0" />
<span>{termsLabel}</span>
</a>
</li>
)}
</ul>
</div>
)}
{/* 4. Contact Details */}
{(phone || email) && (
<div className="space-y-4"> <div className="space-y-4">
{contactTitle && ( {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-primary border-b border-[#363032] pb-2">
{contactTitle} {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">
<Phone className="h-4 w-4 text-[#FFED00] shrink-0" /> <Phone className="h-4 w-4 text-primary shrink-0" />
<a href={`tel:${phone.replace(/\s+/g, '')}`} className="text-white hover:text-[#FFED00] font-mono font-black text-sm"> <a href={`tel:${phone.replace(/\s+/g, '')}`} className="text-white hover:text-primary font-mono font-black text-sm">
{phone} {phone}
</a> </a>
</li> </li>
)} )}
{email && ( {email && (
<li className="flex items-center gap-2.5"> <li className="flex items-center gap-2.5">
<Mail className="h-4 w-4 text-[#FFED00] shrink-0" /> <Mail className="h-4 w-4 text-primary shrink-0" />
<a href={`mailto:${email}`} className="text-[#d0d0d0] hover:text-white font-medium"> <a href={`mailto:${email}`} className="text-[#d0d0d0] hover:text-white font-medium">
{email} {email}
</a> </a>
</li> </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> </ul>
</div> </div>
)} )}
</div> </div>
{/* Bottom Bar with Privacy & Cookie Links */} {/* Bottom Bar - Evenly balanced spacing */}
<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-8 pt-5 border-t border-[#363032] flex flex-col sm:flex-row items-center justify-between gap-3 text-xs text-[#888888]">
<div> {companyName && (
© {new Date().getFullYear()} {companyName} <div>
</div> © {new Date().getFullYear()} {companyName}
<div className="flex items-center gap-6 text-xs font-bold uppercase tracking-wider"> </div>
{privacyHref && privacyLabel && ( )}
<a href={privacyHref} className="text-[#a0a0a0] hover:text-[#FFED00] transition-colors flex items-center gap-1.5"> {creditHref && creditLabel && (
<ShieldCheck className="h-3.5 w-3.5 text-[#FFED00]" /> <div>
<span>{privacyLabel}</span> <a
href={creditHref}
target="_blank"
rel="noopener noreferrer"
className="font-bold text-white hover:text-primary transition-colors underline decoration-primary decoration-2 underline-offset-4"
>
{creditLabel}
</a> </a>
)} </div>
{cookieHref && cookieLabel && ( )}
<a href={cookieHref} className="text-[#a0a0a0] hover:text-[#FFED00] transition-colors flex items-center gap-1.5">
<Cookie className="h-3.5 w-3.5 text-[#FFED00]" />
<span>{cookieLabel}</span>
</a>
)}
</div>
</div> </div>
</div> </div>
</footer> </footer>
+23 -36
View File
@@ -3,6 +3,7 @@
import React, { useState, useRef, useEffect } from 'react' import React, { useState, useRef, useEffect } from 'react'
import { ArrowRight, Globe, ChevronDown } from 'lucide-react' import { ArrowRight, Globe, ChevronDown } from 'lucide-react'
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
import { Logo } from '@/components/Logo'
export interface NavItem { export interface NavItem {
id?: string id?: string
@@ -57,27 +58,27 @@ export function Header({
return `/${targetLocale}${pathname}` return `/${targetLocale}${pathname}`
} }
// Filter out duplicated "Kontakt" from menu items since it has a dedicated right CTA button // Filter out duplicated CTA item from menu items if it matches contactLabel
const filteredNavItems = items?.filter( const filteredNavItems = items?.filter(
(item) => !item.label.toLowerCase().includes('kontakt') (item) => !contactLabel || item.label.trim().toLowerCase() !== contactLabel.trim().toLowerCase()
) )
const languages = [ const languages = ['pl', 'en'].map((code) => ({
{ code: 'pl', label: 'PL' }, code,
{ code: 'en', label: 'UK' }, label: code.toUpperCase(),
] }))
const activeLang = languages.find((l) => l.code === currentLocale) || languages[0] const activeLang = languages.find((l) => l.code === currentLocale) || languages[0]
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-dark shadow-xs">
{/* Top Black Utility Bar */} {/* Top Utility Bar */}
{(tagline || phone) && ( {(tagline || phone) && (
<div className="bg-[#1f1f1f] text-white text-xs py-2 hidden md:block"> <div className="bg-dark 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 && ( {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-primary" />
<span className="font-bold uppercase tracking-wider text-[11px] text-[#e0e0e0]"> <span className="font-bold uppercase tracking-wider text-[11px] text-[#e0e0e0]">
{tagline} {tagline}
</span> </span>
@@ -92,7 +93,7 @@ export function Header({
)} )}
<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-primary hover:text-accent hover:underline font-mono transition-colors"
> >
{phone} {phone}
</a> </a>
@@ -104,32 +105,18 @@ export function Header({
{/* 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">
<a href={homeHref} className="flex items-center gap-3.5"> <a href={homeHref} className="flex items-center gap-3">
<div className="flex h-11 w-11 items-center justify-center bg-[#FFED00] text-[#1f1f1f] font-black text-xl border-2 border-[#1f1f1f] shadow-xs"> <Logo className="h-12 w-auto" alt={companyName || undefined} />
POI
</div>
<div>
{companyName && (
<span className="text-xl font-black uppercase tracking-tight text-[#1f1f1f] block leading-none">
{companyName}
</span>
)}
{tagline && (
<span className="text-[10px] uppercase tracking-widest text-[#666666] font-bold block mt-1">
{tagline}
</span>
)}
</div>
</a> </a>
{/* Navigation Items */} {/* 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-dark">
{filteredNavItems.map((item, idx) => ( {filteredNavItems.map((item, idx) => (
<a <a
key={item.id || idx} key={item.id || idx}
href={item.href} href={item.href}
className="hover:text-[#1f1f1f] hover:border-b-2 hover:border-[#FFED00] transition-all py-2" className="hover:text-dark hover:border-b-2 hover:border-primary transition-all py-2"
> >
{item.label} {item.label}
</a> </a>
@@ -143,15 +130,15 @@ export function Header({
<button <button
type="button" type="button"
onClick={() => setLangDropdownOpen(!langDropdownOpen)} onClick={() => setLangDropdownOpen(!langDropdownOpen)}
className="flex items-center gap-2 border-2 border-[#1f1f1f] bg-white px-3 py-2 text-xs font-black text-[#1f1f1f] hover:bg-[#f4f4f4] transition-colors cursor-pointer" className="flex items-center gap-2 border-2 border-dark bg-white px-3 py-2 text-xs font-black text-dark hover:bg-surface-gray transition-colors cursor-pointer"
> >
<Globe className="h-3.5 w-3.5 text-[#1f1f1f]" /> <Globe className="h-3.5 w-3.5 text-dark" />
<span className="uppercase">{activeLang.code.toUpperCase()}</span> <span className="uppercase">{activeLang.code.toUpperCase()}</span>
<ChevronDown className={`h-3.5 w-3.5 transition-transform duration-200 ${langDropdownOpen ? 'rotate-180' : ''}`} /> <ChevronDown className={`h-3.5 w-3.5 transition-transform duration-200 ${langDropdownOpen ? 'rotate-180' : ''}`} />
</button> </button>
{langDropdownOpen && ( {langDropdownOpen && (
<div className="absolute right-0 mt-1 w-28 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-dark shadow-xl z-50 py-1">
{languages.map((lang) => ( {languages.map((lang) => (
<a <a
key={lang.code} key={lang.code}
@@ -159,12 +146,12 @@ export function Header({
onClick={() => setLangDropdownOpen(false)} onClick={() => setLangDropdownOpen(false)}
className={`flex items-center justify-between px-3 py-2 text-xs font-bold transition-colors ${ className={`flex items-center justify-between px-3 py-2 text-xs font-bold transition-colors ${
currentLocale === lang.code currentLocale === lang.code
? 'bg-[#1f1f1f] text-[#FFED00]' ? 'bg-dark text-primary'
: 'text-[#1f1f1f] hover:bg-[#FFED00]/20' : 'text-dark hover:bg-primary/20'
}`} }`}
> >
<span>{lang.label}</span> <span>{lang.label}</span>
{currentLocale === lang.code && <span className="w-1.5 h-1.5 bg-[#FFED00]" />} {currentLocale === lang.code && <span className="w-1.5 h-1.5 bg-primary" />}
</a> </a>
))} ))}
</div> </div>
@@ -175,7 +162,7 @@ export function Header({
{contactHref && contactLabel && ( {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-accent hover:bg-accent-hover text-white border-2 border-dark px-6 py-3 text-xs font-black uppercase tracking-widest transition-colors shadow-xs"
> >
<span>{contactLabel}</span> <span>{contactLabel}</span>
<ArrowRight className="h-4 w-4" /> <ArrowRight className="h-4 w-4" />
+47
View File
@@ -0,0 +1,47 @@
import React from 'react'
import Image from 'next/image'
interface LogoProps {
className?: string
variant?: 'light' | 'dark' // light = dark text on white bg, dark = white text on dark bg
height?: number
width?: number
priority?: boolean
alt?: string
}
export function LogoIcon({ className = 'h-11 w-auto', alt = '', ...props }: { className?: string; alt?: string }) {
return (
<Image
src="/images/logo-icon.webp"
alt={alt}
width={140}
height={140}
priority
className={`object-contain ${className}`}
{...props}
/>
)
}
export function Logo({
className = 'h-12 w-auto',
variant = 'light',
priority = true,
alt = '',
...props
}: LogoProps) {
const src = variant === 'dark' ? '/images/logo-white.webp' : '/images/logo.webp'
return (
<Image
src={src}
alt={alt}
width={240}
height={84}
priority={priority}
className={`object-contain select-none ${className}`}
{...props}
/>
)
}
+9 -3
View File
@@ -25,7 +25,6 @@ export const Navigation: GlobalConfig = {
{ {
name: 'headerItems', name: 'headerItems',
type: 'array', type: 'array',
localized: true,
labels: { labels: {
singular: 'Pozycja menu głównego', singular: 'Pozycja menu głównego',
plural: 'Pozycje menu głównego', plural: 'Pozycje menu głównego',
@@ -35,6 +34,7 @@ export const Navigation: GlobalConfig = {
name: 'label', name: 'label',
type: 'text', type: 'text',
required: true, required: true,
localized: true,
}, },
{ {
name: 'page', name: 'page',
@@ -53,11 +53,11 @@ export const Navigation: GlobalConfig = {
{ {
name: 'ctaButton', name: 'ctaButton',
type: 'group', type: 'group',
localized: true,
fields: [ fields: [
{ {
name: 'label', name: 'label',
type: 'text', type: 'text',
localized: true,
defaultValue: 'KONTAKT', defaultValue: 'KONTAKT',
}, },
{ {
@@ -94,10 +94,15 @@ export const Navigation: GlobalConfig = {
localized: true, localized: true,
defaultValue: 'Kontakt', defaultValue: 'Kontakt',
}, },
{
name: 'legalTitle',
type: 'text',
localized: true,
defaultValue: 'Polityki i regulamin',
},
{ {
name: 'footerItems', name: 'footerItems',
type: 'array', type: 'array',
localized: true,
labels: { labels: {
singular: 'Pozycja menu stopki', singular: 'Pozycja menu stopki',
plural: 'Pozycje menu stopki', plural: 'Pozycje menu stopki',
@@ -107,6 +112,7 @@ export const Navigation: GlobalConfig = {
name: 'label', name: 'label',
type: 'text', type: 'text',
required: true, required: true,
localized: true,
}, },
{ {
name: 'page', name: 'page',
+21 -18
View File
@@ -307,6 +307,10 @@ export interface Page {
id?: string | null; id?: string | null;
}[] }[]
| null; | null;
/**
* Opcjonalne tło ze zdjęciem (efekt parallax)
*/
backgroundImage?: (number | null) | Media;
id?: string | null; id?: string | null;
blockName?: string | null; blockName?: string | null;
blockType: 'processSteps'; blockType: 'processSteps';
@@ -385,6 +389,10 @@ export interface Page {
id?: string | null; id?: string | null;
}[] }[]
| null; | null;
/**
* Opcjonalne tło ze zdjęciem (efekt parallax)
*/
backgroundImage?: (number | null) | Media;
id?: string | null; id?: string | null;
blockName?: string | null; blockName?: string | null;
blockType: 'techEquipment'; blockType: 'techEquipment';
@@ -425,6 +433,10 @@ export interface Page {
blockType: 'reviews'; blockType: 'reviews';
} }
| { | {
/**
* Formularz z wbudowanego modułu Forms (zarządzanie polami, walidacją i powiadomieniami)
*/
form: number | Form;
badge?: string | null; badge?: string | null;
heading: string; heading: string;
subheading?: string | null; subheading?: string | null;
@@ -434,15 +446,8 @@ export interface Page {
emailContactLabel?: string | null; emailContactLabel?: string | null;
workingHours?: string | null; workingHours?: string | null;
workingHoursLabel?: string | null; workingHoursLabel?: string | null;
nameLabel?: string | null; address?: string | null;
phoneInputLabel?: string | null; addressLabel?: string | null;
locationLabel?: string | null;
areaLabel?: string | null;
messageLabel?: string | null;
submitButtonText?: 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';
@@ -904,6 +909,7 @@ export interface PagesSelect<T extends boolean = true> {
description?: T; description?: T;
id?: T; id?: T;
}; };
backgroundImage?: T;
id?: T; id?: T;
blockName?: T; blockName?: T;
}; };
@@ -967,6 +973,7 @@ export interface PagesSelect<T extends boolean = true> {
image?: T; image?: T;
id?: T; id?: T;
}; };
backgroundImage?: T;
id?: T; id?: T;
blockName?: T; blockName?: T;
}; };
@@ -1007,6 +1014,7 @@ export interface PagesSelect<T extends boolean = true> {
formBlock?: formBlock?:
| T | T
| { | {
form?: T;
badge?: T; badge?: T;
heading?: T; heading?: T;
subheading?: T; subheading?: T;
@@ -1016,15 +1024,8 @@ export interface PagesSelect<T extends boolean = true> {
emailContactLabel?: T; emailContactLabel?: T;
workingHours?: T; workingHours?: T;
workingHoursLabel?: T; workingHoursLabel?: T;
nameLabel?: T; address?: T;
phoneInputLabel?: T; addressLabel?: T;
locationLabel?: T;
areaLabel?: T;
messageLabel?: T;
submitButtonText?: T;
consentText?: T;
successHeading?: T;
successMessage?: T;
id?: T; id?: T;
blockName?: T; blockName?: T;
}; };
@@ -1313,6 +1314,7 @@ export interface Navigation {
footerTagline?: string | null; footerTagline?: string | null;
navTitle?: string | null; navTitle?: string | null;
contactTitle?: string | null; contactTitle?: string | null;
legalTitle?: string | null;
footerItems?: footerItems?:
| { | {
label: string; label: string;
@@ -1553,6 +1555,7 @@ export interface NavigationSelect<T extends boolean = true> {
footerTagline?: T; footerTagline?: T;
navTitle?: T; navTitle?: T;
contactTitle?: T; contactTitle?: T;
legalTitle?: T;
footerItems?: footerItems?:
| T | T
| { | {
+74 -81
View File
@@ -40,7 +40,75 @@ export async function seed(payload: Payload) {
}, },
}) })
// 3. Sprawdź czy strony już istnieją // 3. Utworzenie formularza kontaktowego w kolekcji Forms
let contactForm: { id: number } | null = null
const existingForms = await payload.find({
collection: 'forms',
limit: 1,
})
if (existingForms.totalDocs === 0) {
payload.logger.info('Tworzę formularz wyceny w kolekcji Forms...')
const createdForm = await payload.create({
collection: 'forms',
locale: 'pl',
data: {
title: 'Formularz Wyceny i Kontaktu',
submitButtonLabel: 'Wyślij zapytanie o bezpłatną wycenę',
confirmationType: 'message',
fields: [
{
blockType: 'text',
name: 'name',
label: 'Imię i Nazwisko',
required: true,
width: 50,
},
{
blockType: 'text',
name: 'phone',
label: 'Numer Telefonu',
required: true,
width: 50,
},
{
blockType: 'text',
name: 'location',
label: 'Miejscowość',
required: false,
width: 50,
},
{
blockType: 'text',
name: 'area',
label: 'Szacowany Metraż (m²)',
required: false,
width: 50,
},
{
blockType: 'textarea',
name: 'message',
label: 'Zakres prac / stan nawierzchni',
required: false,
width: 100,
},
{
blockType: 'checkbox',
name: 'consent',
label:
'Wyrażam zgodę na przetwarzanie moich danych osobowych w celu kontaktu i przygotowania oferty zgodnie z Polityką Prywatności.',
required: true,
width: 100,
},
],
} as never,
})
contactForm = { id: Number(createdForm.id) }
} else {
contactForm = { id: Number(existingForms.docs[0].id) }
}
// 4. Sprawdź czy strony już istnieją
const existingPages = await payload.find({ const existingPages = await payload.find({
collection: 'pages', collection: 'pages',
limit: 1, limit: 1,
@@ -284,6 +352,7 @@ export async function seed(payload: Payload) {
}, },
{ {
blockType: 'formBlock', blockType: 'formBlock',
form: contactForm.id,
badge: 'Kontakt i Wycena', badge: 'Kontakt i Wycena',
heading: 'Skontaktuj się i otrzymaj bezpłatną wycenę', heading: 'Skontaktuj się i otrzymaj bezpłatną wycenę',
subheading: subheading:
@@ -291,9 +360,6 @@ export async function seed(payload: Payload) {
phone: '+48 668 973 737', phone: '+48 668 973 737',
email: '[email protected]', email: '[email protected]',
workingHours: 'Pon - Sob: 7:00 - 20:00', workingHours: 'Pon - Sob: 7:00 - 20:00',
submitButtonText: 'Wyślij zapytanie o bezpłatną wycenę',
consentText:
'Wyrażam zgodę na przetwarzanie moich danych osobowych w celu kontaktu i przygotowania oferty zgodnie z Polityką Prywatności.',
}, },
], ],
}, },
@@ -329,6 +395,7 @@ export async function seed(payload: Payload) {
}, },
{ {
blockType: 'formBlock', blockType: 'formBlock',
form: contactForm.id,
badge: 'Wycena', badge: 'Wycena',
heading: 'Chcesz, aby Twój podjazd wyglądał jak nowy?', heading: 'Chcesz, aby Twój podjazd wyglądał jak nowy?',
subheading: 'Skontaktuj się z nami już dziś i umów dogodny termin realizacji.', subheading: 'Skontaktuj się z nami już dziś i umów dogodny termin realizacji.',
@@ -340,75 +407,7 @@ export async function seed(payload: Payload) {
}, },
}) })
// D. Strona O nas / Technologia // D. Strona Kontakt
const oNasPage = await payload.create({
collection: 'pages',
locale: 'pl',
data: {
title: 'Technologia i Sprzęt',
slug: 'o-nas',
layout: [
{
blockType: 'techEquipment',
badge: 'O nas & Sprzęt',
heading: 'Technologia czyszczenia nawierzchni brukowych',
subheading:
'Jesteśmy wyspecjalizowaną firmą skupioną w 100% na nawierzchniach brukowych. Nie myjemy dywanów ani elewacji – całą naszą wiedzę i sprzęt inwestujemy w perfekcyjne odnawianie kostki.',
items: [
{
title: 'Przemysłowe myjki spalinowe z podgrzewaniem wody',
description:
'Niezależne od prądu urządzenia generujące ciśnienie do 250 barów i temperaturę wody do 100°C. Rozpuszczają brud bez niszczenia struktury materiału.',
},
{
title: 'Talerze powierzchniowe Mosmatic Surface Cleaner',
description:
'Opatentowana szwajcarska technologia bezrozbryzgowa. Cały brud i woda zostają pod kopułą talerza, chroniąc fasady budynków, cokoły i roślinność ogrodową.',
},
{
title: 'Certyfikowane impregnaty hydrofobowe',
description:
'Głęboko penetrujące impregnaty silikonowo-siloksanowe oraz polimerowe, zabezpieczające kostkę przed erozją mrozową, solą i zanieczyszczeniami.',
},
],
},
{
blockType: 'processSteps',
badge: 'Standard Jakości',
heading: 'Precyzja na każdym etapie prac',
subheading: 'Dbamy o każdy metr kwadratowy Twojej posesji.',
steps: [
{
stepNumber: '01',
title: 'Darmowa inspekcja',
description: 'Weryfikacja podłoża i dopasowanie odpowiednich preparatów chemicznych.',
},
{
stepNumber: '02',
title: 'Bezpieczne wykonanie',
description: 'Mycie z osłoną otoczenia i z zachowaniem bezpieczeństwa roślin ogrodowych.',
},
{
stepNumber: '03',
title: 'Zabezpieczenie na lata',
description: 'Uzupełnienie fug piaskiem kwarcowym oraz hydrofobizacja nawierzchni.',
},
],
},
{
blockType: 'formBlock',
badge: 'Zamówienie',
heading: 'Skontaktuj się ze specjalistami',
subheading: 'Zadzwoń lub napisz – chętnie odpowiemy na wszystkie pytania techniczne.',
phone: '+48 668 973 737',
email: '[email protected]',
workingHours: 'Pon - Sob: 7:00 - 20:00',
},
],
},
})
// E. Strona Kontakt
const kontaktPage = await payload.create({ const kontaktPage = await payload.create({
collection: 'pages', collection: 'pages',
locale: 'pl', locale: 'pl',
@@ -418,6 +417,7 @@ export async function seed(payload: Payload) {
layout: [ layout: [
{ {
blockType: 'formBlock', blockType: 'formBlock',
form: contactForm.id,
badge: 'Kontakt', badge: 'Kontakt',
heading: 'Skontaktuj się z POI Clean House', heading: 'Skontaktuj się z POI Clean House',
subheading: subheading:
@@ -544,6 +544,7 @@ W każdej chwili możesz zmienić swoje preferencje dotyczące plików cookies k
}, },
{ {
blockType: 'formBlock', blockType: 'formBlock',
form: contactForm.id,
badge: 'Wycena', badge: 'Wycena',
heading: 'Potrzebujesz profesjonalnego czyszczenia?', heading: 'Potrzebujesz profesjonalnego czyszczenia?',
subheading: 'Skontaktuj się z naszym zespołem i otrzymaj darmową wycenę w ciągu 2 godzin.', subheading: 'Skontaktuj się z naszym zespołem i otrzymaj darmową wycenę w ciągu 2 godzin.',
@@ -592,10 +593,6 @@ W każdej chwili możesz zmienić swoje preferencje dotyczące plików cookies k
label: 'Realizacje', label: 'Realizacje',
page: realizacjePage.id, page: realizacjePage.id,
}, },
{
label: 'Technologia',
page: oNasPage.id,
},
{ {
label: 'Blog', label: 'Blog',
page: blogPage.id, page: blogPage.id,
@@ -620,10 +617,6 @@ W każdej chwili możesz zmienić swoje preferencje dotyczące plików cookies k
label: 'Galeria realizacji', label: 'Galeria realizacji',
page: realizacjePage.id, page: realizacjePage.id,
}, },
{
label: 'Technologia i sprzęt',
page: oNasPage.id,
},
{ {
label: 'Poradnik i Blog', label: 'Poradnik i Blog',
page: blogPage.id, page: blogPage.id,