feat: integrate built-in forms, refine footer & parallax, dynamic navigation & branding assets
This commit is contained in:
@@ -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 |
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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.',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -52,5 +52,13 @@ export const ProcessStepsBlock: Block = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'backgroundImage',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'media',
|
||||||
|
admin: {
|
||||||
|
description: 'Opcjonalne tło ze zdjęciem (efekt parallax)',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -49,5 +49,13 @@ export const TechEquipmentBlock: Block = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'backgroundImage',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'media',
|
||||||
|
admin: {
|
||||||
|
description: 'Opcjonalne tło ze zdjęciem (efekt parallax)',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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" />
|
||||||
|
|||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user