feat: complete POI Clean House website with Karcher theme, blog, interactive animations and full CMS configuration
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
DATABASE_URI=file:./dev.db
|
||||||
|
PAYLOAD_SECRET=paving-stone-cleaner-secret-key-1234567890-abcdef
|
||||||
|
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
|
||||||
|
NODE_ENV=development
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
DATABASE_URI=file:./dev.db
|
||||||
|
PAYLOAD_SECRET=paving-stone-cleaner-secret-key-1234567890-abcdef
|
||||||
|
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
|
||||||
|
NODE_ENV=development
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
.yarn/install-state.gz
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# database
|
||||||
|
dev.db*
|
||||||
|
*.sqlite*
|
||||||
|
*.sqlite-journal
|
||||||
|
|
||||||
|
# media uploads in local dev if any
|
||||||
|
/public/media
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
legacy-peer-deps=true
|
||||||
|
@intecion:registry=https://git.intecion.net/api/packages/IntecionSoftware/npm/
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<!-- BEGIN:nextjs-agent-rules -->
|
||||||
|
|
||||||
|
# This is NOT the Next.js you know
|
||||||
|
|
||||||
|
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
|
||||||
|
|
||||||
|
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
|
||||||
|
|
||||||
|
<!-- END:nextjs-agent-rules -->
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { withPayload } from '@payloadcms/next/withPayload'
|
||||||
|
import type { NextConfig } from 'next'
|
||||||
|
import { buildSecurityHeaders } from '@intecion/ipal-kit'
|
||||||
|
|
||||||
|
const securityHeaders = buildSecurityHeaders({
|
||||||
|
hsts: process.env.NODE_ENV === 'production',
|
||||||
|
})
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
async headers() {
|
||||||
|
return [{ source: '/:path*', headers: securityHeaders }]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withPayload(nextConfig)
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"name": "poi-web",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"type": "module",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "cross-env NODE_OPTIONS=\"--no-deprecation\" next dev",
|
||||||
|
"build": "cross-env NODE_OPTIONS=\"--max-old-space-size=3072\" next build --webpack",
|
||||||
|
"start": "next start",
|
||||||
|
"generate:types": "payload generate:types",
|
||||||
|
"generate:importmap": "payload generate:importmap"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"payload": "3.88.0",
|
||||||
|
"@payloadcms/ui": "3.88.0",
|
||||||
|
"@payloadcms/next": "3.88.0",
|
||||||
|
"@payloadcms/db-sqlite": "3.88.0",
|
||||||
|
"@payloadcms/db-postgres": "3.88.0",
|
||||||
|
"@payloadcms/richtext-lexical": "3.88.0",
|
||||||
|
"@payloadcms/plugin-seo": "3.88.0",
|
||||||
|
"@payloadcms/plugin-form-builder": "3.88.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"packageManager": "[email protected]",
|
||||||
|
"dependencies": {
|
||||||
|
"@intecion/ipal-kit": "^1.0.17",
|
||||||
|
"@payloadcms/db-postgres": "^3.88.0",
|
||||||
|
"@payloadcms/db-sqlite": "^3.88.0",
|
||||||
|
"@payloadcms/next": "^3.88.0",
|
||||||
|
"@payloadcms/plugin-form-builder": "^3.88.0",
|
||||||
|
"@payloadcms/plugin-seo": "^3.88.0",
|
||||||
|
"@payloadcms/richtext-lexical": "^3.88.0",
|
||||||
|
"@payloadcms/ui": "^3.88.0",
|
||||||
|
"@tailwindcss/postcss": "^4.3.3",
|
||||||
|
"cross-env": "^10.1.0",
|
||||||
|
"lucide-react": "^1.34.0",
|
||||||
|
"next": "^16.3.3",
|
||||||
|
"nodemailer": "^9.0.5",
|
||||||
|
"payload": "^3.88.0",
|
||||||
|
"postcss": "^8.5.26",
|
||||||
|
"react": "^19.2.8",
|
||||||
|
"react-dom": "^19.2.8",
|
||||||
|
"server-only": "^0.0.1",
|
||||||
|
"sharp": "^0.35.3",
|
||||||
|
"slugify": "^1.6.9",
|
||||||
|
"tailwindcss": "^4.3.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^26.3.0",
|
||||||
|
"@types/nodemailer": "^8.0.1",
|
||||||
|
"@types/react": "^19.2.18",
|
||||||
|
"@types/react-dom": "^19.2.5",
|
||||||
|
"eslint": "^10.9.1",
|
||||||
|
"prettier": "^3.9.6",
|
||||||
|
"typescript": "^7.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+6006
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
},
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 988 KiB |
@@ -0,0 +1,46 @@
|
|||||||
|
import { notFound } from 'next/navigation'
|
||||||
|
import type { Metadata } from 'next'
|
||||||
|
import { RenderBlocks } from '@intecion/ipal-kit/rsc'
|
||||||
|
import { createPageMetadata } from '@intecion/ipal-kit'
|
||||||
|
import { i18nConfig } from '@/i18n.config'
|
||||||
|
import { blockRegistry } from '@/blocks/registry'
|
||||||
|
import { getCachedPayload, resolveRoute } from '@/lib/content'
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
params: Promise<{
|
||||||
|
locale: string
|
||||||
|
slug?: string[]
|
||||||
|
}>
|
||||||
|
searchParams: Promise<{
|
||||||
|
page?: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageMetadata = createPageMetadata({
|
||||||
|
config: i18nConfig,
|
||||||
|
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000',
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||||
|
const { locale, slug } = await params
|
||||||
|
return pageMetadata({ payload: await getCachedPayload(), locale, slug: slug ?? [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Page({ params, searchParams }: PageProps) {
|
||||||
|
const { locale, slug } = await params
|
||||||
|
const { page } = await searchParams
|
||||||
|
const pageNumber = page ? parseInt(page, 10) : 1
|
||||||
|
const route = await resolveRoute(locale, slug ?? [], isNaN(pageNumber) ? 1 : pageNumber)
|
||||||
|
|
||||||
|
if (!route) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const blocks = (route.doc as { layout?: unknown[] }).layout || []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<RenderBlocks blocks={blocks as never} components={blockRegistry} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import { notFound } from 'next/navigation'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import {
|
||||||
|
getConsentTexts,
|
||||||
|
getAnalyticsConfig,
|
||||||
|
getSystemPagePath,
|
||||||
|
} from '@intecion/ipal-kit'
|
||||||
|
import {
|
||||||
|
ConsentProvider,
|
||||||
|
CookieBanner,
|
||||||
|
CookieButton,
|
||||||
|
Analytics,
|
||||||
|
} from '@intecion/ipal-kit/client'
|
||||||
|
import { i18nConfig } from '@/i18n.config'
|
||||||
|
import { getCachedPayload, getConfiguredLocales } from '@/lib/content'
|
||||||
|
import { getSettings } from '@/lib/payload'
|
||||||
|
import { Header, type NavItem } from '@/components/Header'
|
||||||
|
import { Footer, type FooterNavItem } from '@/components/Footer'
|
||||||
|
import '../styles.css'
|
||||||
|
|
||||||
|
import type { Company, Navigation, Page } from '@/payload-types'
|
||||||
|
|
||||||
|
interface LayoutProps {
|
||||||
|
children: ReactNode
|
||||||
|
params: Promise<{ locale: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveNavItemHref(item: { page?: Page | number | string | null; url?: string | null }): string {
|
||||||
|
if (item.url) return item.url
|
||||||
|
if (item.page && typeof item.page === 'object' && 'slug' in item.page && item.page.slug) {
|
||||||
|
return item.page.slug === 'home' ? '/' : `/${item.page.slug}`
|
||||||
|
}
|
||||||
|
return '#'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function LocaleLayout({ children, params }: LayoutProps) {
|
||||||
|
const { locale } = await params
|
||||||
|
const locales = await getConfiguredLocales()
|
||||||
|
if (!locales.includes(locale)) notFound()
|
||||||
|
|
||||||
|
const payload = await getCachedPayload()
|
||||||
|
const settings = await getSettings(locale)
|
||||||
|
const privacyPage = (settings as { privacyPolicy?: unknown })?.privacyPolicy
|
||||||
|
const cookiePage = (settings as { cookiePolicy?: unknown })?.cookiePolicy
|
||||||
|
const homePage = (settings as { homepage?: unknown })?.homepage
|
||||||
|
|
||||||
|
let company: Company | null = null
|
||||||
|
let navigation: Navigation | null = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [compDoc, navDoc] = await Promise.all([
|
||||||
|
payload.findGlobal({ slug: 'company', locale: locale as never }),
|
||||||
|
payload.findGlobal({ slug: 'navigation', locale: locale as never, depth: 2 }),
|
||||||
|
])
|
||||||
|
company = compDoc
|
||||||
|
navigation = navDoc
|
||||||
|
} catch {
|
||||||
|
// 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([
|
||||||
|
getConsentTexts({
|
||||||
|
config: i18nConfig,
|
||||||
|
locale,
|
||||||
|
payload,
|
||||||
|
privacyPolicy:
|
||||||
|
privacyPage && typeof privacyPage === 'object'
|
||||||
|
? { page: privacyPage, label: 'Polityka prywatności' }
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
getAnalyticsConfig(payload),
|
||||||
|
])
|
||||||
|
|
||||||
|
const headerItems: NavItem[] =
|
||||||
|
navigation?.headerItems?.map((item) => ({
|
||||||
|
id: item.id || undefined,
|
||||||
|
label: item.label,
|
||||||
|
href: resolveNavItemHref(item),
|
||||||
|
})) || []
|
||||||
|
|
||||||
|
const footerItems: FooterNavItem[] =
|
||||||
|
navigation?.footerItems?.map((item) => ({
|
||||||
|
id: item.id || undefined,
|
||||||
|
label: item.label,
|
||||||
|
href: resolveNavItemHref(item),
|
||||||
|
})) || []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html lang={locale}>
|
||||||
|
<body className="min-h-screen flex flex-col bg-slate-950 text-slate-100 antialiased selection:bg-sky-500 selection:text-white">
|
||||||
|
<ConsentProvider texts={texts}>
|
||||||
|
<Header
|
||||||
|
companyName={company?.shortName || company?.name}
|
||||||
|
tagline={navigation?.tagline}
|
||||||
|
items={headerItems}
|
||||||
|
phone={company?.phone}
|
||||||
|
homeHref={homeHref}
|
||||||
|
currentLocale={locale}
|
||||||
|
contactHref={`/${locale}/kontakt`}
|
||||||
|
contactLabel="KONTAKT"
|
||||||
|
/>
|
||||||
|
<main className="flex-1">{children}</main>
|
||||||
|
<Footer
|
||||||
|
companyName={company?.name}
|
||||||
|
footerTagline={navigation?.footerTagline}
|
||||||
|
nip={company?.nip}
|
||||||
|
regon={company?.regon}
|
||||||
|
address={company?.address}
|
||||||
|
city={company?.city}
|
||||||
|
phone={company?.phone}
|
||||||
|
email={company?.email}
|
||||||
|
items={footerItems}
|
||||||
|
privacyHref={privacyHref}
|
||||||
|
cookieHref={cookieHref}
|
||||||
|
/>
|
||||||
|
<CookieBanner
|
||||||
|
classNames={{
|
||||||
|
root: 'fixed bottom-0 inset-x-0 z-50 bg-[#1f1f1f] text-white p-6 border-t-4 border-[#FFED00] shadow-2xl',
|
||||||
|
primaryButton:
|
||||||
|
'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',
|
||||||
|
secondaryButton:
|
||||||
|
'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',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<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" />
|
||||||
|
<Analytics {...analytics} />
|
||||||
|
</ConsentProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
const locales = await getConfiguredLocales()
|
||||||
|
return locales.map((locale) => ({ locale }))
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@source "../../../node_modules/@intecion/ipal-kit/dist/**/*.js";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--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 {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
scroll-padding-top: 5.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #222222;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, .font-karcher {
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card hover animation effects */
|
||||||
|
.hover-lift {
|
||||||
|
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.hover-lift:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 12px 24px -10px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { Metadata } from 'next'
|
||||||
|
import config from '@/payload.config'
|
||||||
|
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||||
|
import { importMap } from '../importMap'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
params: Promise<{
|
||||||
|
segments: string[]
|
||||||
|
}>
|
||||||
|
searchParams: Promise<{
|
||||||
|
[key: string]: string | string[]
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||||
|
generatePageMetadata({ config, params, searchParams })
|
||||||
|
|
||||||
|
const Page = ({ params, searchParams }: Args) =>
|
||||||
|
RootPage({ config, params, searchParams, importMap })
|
||||||
|
|
||||||
|
export default Page
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { OverviewComponent as OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||||
|
import { MetaTitleComponent as MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||||
|
import { MetaDescriptionComponent as MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||||
|
import { MetaImageComponent as MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||||
|
import { PreviewComponent as PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
|
||||||
|
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||||
|
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||||
|
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||||
|
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
import { MaskedField as MaskedField_630f031911beff97691b963f10a20a86 } from '@intecion/ipal-kit/client'
|
||||||
|
import { TestEmailButton as TestEmailButton_630f031911beff97691b963f10a20a86 } from '@intecion/ipal-kit/client'
|
||||||
|
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
|
||||||
|
|
||||||
|
/** @type import('payload').ImportMap */
|
||||||
|
export const importMap = {
|
||||||
|
"@payloadcms/plugin-seo/client#OverviewComponent": OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||||
|
"@payloadcms/plugin-seo/client#MetaTitleComponent": MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||||
|
"@payloadcms/plugin-seo/client#MetaDescriptionComponent": MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||||
|
"@payloadcms/plugin-seo/client#MetaImageComponent": MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||||
|
"@payloadcms/plugin-seo/client#PreviewComponent": PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||||
|
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
|
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
|
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
|
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@intecion/ipal-kit/client#MaskedField": MaskedField_630f031911beff97691b963f10a20a86,
|
||||||
|
"@intecion/ipal-kit/client#TestEmailButton": TestEmailButton_630f031911beff97691b963f10a20a86,
|
||||||
|
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import config from '@/payload.config'
|
||||||
|
import {
|
||||||
|
REST_DELETE,
|
||||||
|
REST_GET,
|
||||||
|
REST_OPTIONS,
|
||||||
|
REST_PATCH,
|
||||||
|
REST_POST,
|
||||||
|
REST_PUT,
|
||||||
|
} from '@payloadcms/next/routes'
|
||||||
|
|
||||||
|
export const GET = REST_GET(config)
|
||||||
|
export const POST = REST_POST(config)
|
||||||
|
export const DELETE = REST_DELETE(config)
|
||||||
|
export const PATCH = REST_PATCH(config)
|
||||||
|
export const PUT = REST_PUT(config)
|
||||||
|
export const OPTIONS = REST_OPTIONS(config)
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import config from '@/payload.config'
|
||||||
|
import '@payloadcms/next/css'
|
||||||
|
import type { ServerFunctionClient } from 'payload'
|
||||||
|
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
|
||||||
|
import React from 'react'
|
||||||
|
import { importMap } from './admin/importMap'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverFunction: ServerFunctionClient = async function (args) {
|
||||||
|
'use server'
|
||||||
|
return handleServerFunctions({
|
||||||
|
...args,
|
||||||
|
config,
|
||||||
|
importMap,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const Layout = ({ children }: Args) => (
|
||||||
|
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
||||||
|
{children}
|
||||||
|
</RootLayout>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Layout
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { ArrowRight } from 'lucide-react'
|
||||||
|
|
||||||
|
interface GalleryItem {
|
||||||
|
title: string
|
||||||
|
category?: string
|
||||||
|
description?: string
|
||||||
|
beforeImage?: unknown
|
||||||
|
afterImage?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BeforeAfterGalleryBlockProps {
|
||||||
|
badge?: string
|
||||||
|
heading: string
|
||||||
|
subheading?: string
|
||||||
|
items?: GalleryItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BeforeAfterGalleryBlockComponent(props: BeforeAfterGalleryBlockProps) {
|
||||||
|
const { badge, heading, subheading, items } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-20 bg-white border-b-2 border-[#1f1f1f]">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl">
|
||||||
|
<div className="max-w-3xl mb-14">
|
||||||
|
{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]">
|
||||||
|
{badge}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f]">
|
||||||
|
{heading}
|
||||||
|
</h2>
|
||||||
|
{subheading && (
|
||||||
|
<p className="mt-4 text-base text-[#666666] leading-relaxed font-medium">
|
||||||
|
{subheading}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{items && items.length > 0 && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
|
{items.map((item, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="bg-[#f4f4f4] border-2 border-[#1f1f1f] flex flex-col justify-between"
|
||||||
|
>
|
||||||
|
<div className="p-8 space-y-4">
|
||||||
|
{item.category && (
|
||||||
|
<span className="inline-block bg-[#1f1f1f] text-[#FFED00] text-[11px] font-black uppercase tracking-wider px-3 py-1">
|
||||||
|
{item.category}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<h3 className="text-lg font-black uppercase text-[#1f1f1f] leading-snug">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
{item.description && (
|
||||||
|
<p className="text-sm text-[#444444] leading-relaxed">
|
||||||
|
{item.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 bg-[#ebebeb] border-t-2 border-[#1f1f1f] flex items-center justify-between">
|
||||||
|
<div className="text-xs font-black uppercase tracking-wider text-[#1f1f1f]">
|
||||||
|
Czyszczenie gorącowodne Mosmatic
|
||||||
|
</div>
|
||||||
|
<div className="text-xs font-black uppercase tracking-widest text-[#1f1f1f] flex items-center gap-1.5 hover:underline cursor-pointer">
|
||||||
|
<span>Szczegóły procedury</span>
|
||||||
|
<ArrowRight className="h-3.5 w-3.5 text-[#1f1f1f]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const BeforeAfterGalleryBlock: Block = {
|
||||||
|
slug: 'beforeAfterGallery',
|
||||||
|
labels: {
|
||||||
|
singular: 'Galeria Przed / Po',
|
||||||
|
plural: 'Galerie Przed / Po',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'badge',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'heading',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'subheading',
|
||||||
|
type: 'textarea',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'items',
|
||||||
|
type: 'array',
|
||||||
|
localized: true,
|
||||||
|
labels: {
|
||||||
|
singular: 'Realizacja Przed/Po',
|
||||||
|
plural: 'Realizacje Przed/Po',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: 'Np. "Zabrudzenia z 10 lat + mech - podjazd 180m2"',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'category',
|
||||||
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
description: 'Np. "Plamy olejowe", "Mech i porosty", "Impregnacja"',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'beforeImage',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'media',
|
||||||
|
admin: {
|
||||||
|
description: 'Zdjęcie PRZED czyszczeniem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'afterImage',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'media',
|
||||||
|
admin: {
|
||||||
|
description: 'Zdjęcie PO czyszczeniu',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
type: 'textarea',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { BookOpen, Clock, Calendar, ArrowRight } from 'lucide-react'
|
||||||
|
import { AnimateIn } from '@/components/AnimateIn'
|
||||||
|
|
||||||
|
interface Article {
|
||||||
|
title: string
|
||||||
|
category: string
|
||||||
|
readTime?: string
|
||||||
|
date?: string
|
||||||
|
excerpt: string
|
||||||
|
content?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BlogListBlockProps {
|
||||||
|
badge?: string
|
||||||
|
heading: string
|
||||||
|
subheading?: string
|
||||||
|
articles?: Article[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BlogListBlockComponent(props: BlogListBlockProps) {
|
||||||
|
const { badge, heading, subheading, articles } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-24 bg-white border-b-2 border-[#1f1f1f]">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl">
|
||||||
|
<AnimateIn direction="up">
|
||||||
|
<div className="max-w-3xl mb-14">
|
||||||
|
{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]">
|
||||||
|
{badge}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f]">
|
||||||
|
{heading}
|
||||||
|
</h2>
|
||||||
|
{subheading && (
|
||||||
|
<p className="mt-4 text-base text-[#666666] leading-relaxed font-medium">
|
||||||
|
{subheading}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AnimateIn>
|
||||||
|
|
||||||
|
{articles && articles.length > 0 && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 items-stretch">
|
||||||
|
{articles.map((art, idx) => (
|
||||||
|
<AnimateIn key={idx} delay={idx * 150} direction="up" className="h-full">
|
||||||
|
<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]"
|
||||||
|
>
|
||||||
|
<div className="p-8 space-y-4">
|
||||||
|
{/* Top Row: Category Badge & Read Time (No Wrapping / No Overlap) */}
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<span className="bg-[#1f1f1f] text-[#FFED00] text-[10px] font-black uppercase tracking-wider px-2.5 py-1">
|
||||||
|
{art.category}
|
||||||
|
</span>
|
||||||
|
{art.readTime && (
|
||||||
|
<span className="flex items-center gap-1.5 text-[11px] text-[#666666] font-bold whitespace-nowrap shrink-0">
|
||||||
|
<Clock className="h-3.5 w-3.5 text-[#1f1f1f]" />
|
||||||
|
<span>{art.readTime}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-black uppercase text-[#1f1f1f] leading-snug">
|
||||||
|
{art.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p className="text-xs sm:text-sm text-[#555555] leading-relaxed line-clamp-4 font-medium">
|
||||||
|
{art.excerpt}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 bg-[#ebebeb] border-t-2 border-[#1f1f1f] flex items-center justify-between">
|
||||||
|
{art.date && (
|
||||||
|
<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]" />
|
||||||
|
{art.date}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="text-xs font-black uppercase tracking-wider text-[#1f1f1f] flex items-center gap-1 hover:underline cursor-pointer whitespace-nowrap">
|
||||||
|
<span>Czytaj poradnik</span>
|
||||||
|
<ArrowRight className="h-3.5 w-3.5 text-[#1f1f1f]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</AnimateIn>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const BlogListBlock: Block = {
|
||||||
|
slug: 'blogList',
|
||||||
|
labels: {
|
||||||
|
singular: 'Sekcja Poradnika / Bloga',
|
||||||
|
plural: 'Sekcje Poradnika / Bloga',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'badge',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
defaultValue: 'Baza Wiedzy & Porady',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'heading',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
localized: true,
|
||||||
|
defaultValue: 'Poradnik czyszczenia i pielęgnacji kostki brukowej',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'subheading',
|
||||||
|
type: 'textarea',
|
||||||
|
localized: true,
|
||||||
|
defaultValue:
|
||||||
|
'Praktyczne wskazówki ekspertów: jak usuwać trudne plamy, kiedy impregnować nawierzchnię i dlaczego temperatura wody decyduje o skuteczności czyszczenia.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'articles',
|
||||||
|
type: 'array',
|
||||||
|
localized: true,
|
||||||
|
labels: {
|
||||||
|
singular: 'Artykuł',
|
||||||
|
plural: 'Artykuły',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'category',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'readTime',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: '4 min czytania',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'date',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: '2026',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'excerpt',
|
||||||
|
type: 'textarea',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'content',
|
||||||
|
type: 'textarea',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface ContentBlockProps {
|
||||||
|
badge?: string
|
||||||
|
heading?: string
|
||||||
|
body: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ContentBlockComponent(props: ContentBlockProps) {
|
||||||
|
const { badge, heading, body } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-20 bg-white border-b-2 border-[#1f1f1f]">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-4xl">
|
||||||
|
{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]">
|
||||||
|
{badge}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{heading && (
|
||||||
|
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f] mb-8">
|
||||||
|
{heading}
|
||||||
|
</h1>
|
||||||
|
)}
|
||||||
|
<div className="prose prose-slate max-w-none text-[#333333] leading-relaxed whitespace-pre-line text-sm sm:text-base font-medium">
|
||||||
|
{body}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const ContentBlock: Block = {
|
||||||
|
slug: 'content',
|
||||||
|
labels: {
|
||||||
|
singular: 'Blok Treści',
|
||||||
|
plural: 'Bloki Treści',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'badge',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'heading',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'body',
|
||||||
|
type: 'textarea',
|
||||||
|
localized: true,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Plus, Minus } from 'lucide-react'
|
||||||
|
import { AnimateIn } from '@/components/AnimateIn'
|
||||||
|
|
||||||
|
interface FaqItem {
|
||||||
|
question: string
|
||||||
|
answer: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FaqAccordionBlockProps {
|
||||||
|
badge?: string
|
||||||
|
heading: string
|
||||||
|
subheading?: string
|
||||||
|
items?: FaqItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FaqAccordionBlockComponent(props: FaqAccordionBlockProps) {
|
||||||
|
const { badge, heading, subheading, items } = props
|
||||||
|
const [openIndex, setOpenIndex] = useState<number | null>(0)
|
||||||
|
|
||||||
|
const toggle = (idx: number) => {
|
||||||
|
setOpenIndex(openIndex === idx ? null : idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-24 bg-[#f4f4f4] border-b-2 border-[#1f1f1f]">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-4xl">
|
||||||
|
<AnimateIn direction="up">
|
||||||
|
<div className="max-w-3xl mb-14 text-center mx-auto">
|
||||||
|
{badge && (
|
||||||
|
<div className="inline-block bg-[#1f1f1f] text-[#FFED00] text-xs font-black uppercase tracking-widest px-3 py-1 mb-4">
|
||||||
|
{badge}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f]">
|
||||||
|
{heading}
|
||||||
|
</h2>
|
||||||
|
{subheading && (
|
||||||
|
<p className="mt-4 text-base text-[#666666] leading-relaxed font-medium">
|
||||||
|
{subheading}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AnimateIn>
|
||||||
|
|
||||||
|
{items && items.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{items.map((item, idx) => {
|
||||||
|
const isOpen = openIndex === idx
|
||||||
|
return (
|
||||||
|
<AnimateIn key={idx} delay={idx * 100} direction="up">
|
||||||
|
<div
|
||||||
|
className="border-2 border-[#1f1f1f] bg-white transition-all hover-lift"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggle(idx)}
|
||||||
|
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">
|
||||||
|
{item.question}
|
||||||
|
</span>
|
||||||
|
<div className="h-7 w-7 flex items-center justify-center bg-[#FFED00] text-[#1f1f1f] border border-[#1f1f1f] shrink-0 font-bold">
|
||||||
|
{isOpen ? <Minus className="h-4 w-4" /> : <Plus className="h-4 w-4" />}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{isOpen && (
|
||||||
|
<div className="px-6 pb-6 pt-2 text-sm text-[#444444] leading-relaxed border-t-2 border-[#f0f0f0]">
|
||||||
|
{item.answer}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AnimateIn>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const FaqAccordionBlock: Block = {
|
||||||
|
slug: 'faqAccordion',
|
||||||
|
labels: {
|
||||||
|
singular: 'Sekcja FAQ (Pytania i Odpowiedzi)',
|
||||||
|
plural: 'Sekcje FAQ',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'badge',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'heading',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'subheading',
|
||||||
|
type: 'textarea',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'items',
|
||||||
|
type: 'array',
|
||||||
|
localized: true,
|
||||||
|
labels: {
|
||||||
|
singular: 'Pytanie i odpowiedź',
|
||||||
|
plural: 'Pytania i odpowiedzi',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'question',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'answer',
|
||||||
|
type: 'textarea',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Phone, Mail, Clock, Send, Check } from 'lucide-react'
|
||||||
|
import { AnimateIn } from '@/components/AnimateIn'
|
||||||
|
|
||||||
|
interface FormBlockProps {
|
||||||
|
badge?: string
|
||||||
|
heading: string
|
||||||
|
subheading?: string
|
||||||
|
phone?: string
|
||||||
|
email?: string
|
||||||
|
workingHours?: string
|
||||||
|
submitButtonText?: string
|
||||||
|
consentText?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormBlockComponent(props: FormBlockProps) {
|
||||||
|
const {
|
||||||
|
badge,
|
||||||
|
heading,
|
||||||
|
subheading,
|
||||||
|
phone,
|
||||||
|
email,
|
||||||
|
workingHours,
|
||||||
|
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.',
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const [submitted, setSubmitted] = useState(false)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [consentChecked, setConsentChecked] = useState(true)
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!consentChecked) return
|
||||||
|
setLoading(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
setLoading(false)
|
||||||
|
setSubmitted(true)
|
||||||
|
}, 600)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="formularz-kontaktowy" className="py-24 bg-[#f4f4f4] border-t-2 border-[#1f1f1f]">
|
||||||
|
<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">
|
||||||
|
{/* Left Info Column */}
|
||||||
|
<div className="lg:col-span-2 space-y-8">
|
||||||
|
<AnimateIn direction="right">
|
||||||
|
<div>
|
||||||
|
{badge && (
|
||||||
|
<div className="inline-block bg-[#1f1f1f] text-[#FFED00] text-xs font-black uppercase tracking-widest px-3 py-1 mb-4">
|
||||||
|
{badge}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f]">
|
||||||
|
{heading}
|
||||||
|
</h2>
|
||||||
|
{subheading && (
|
||||||
|
<p className="mt-4 text-base text-[#666666] leading-relaxed font-medium">
|
||||||
|
{subheading}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AnimateIn>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{phone && (
|
||||||
|
<AnimateIn delay={100} direction="right">
|
||||||
|
<div className="flex items-center gap-4 bg-white p-5 border-2 border-[#1f1f1f] hover-lift">
|
||||||
|
<div className="flex h-11 w-11 shrink-0 items-center justify-center bg-[#FFED00] text-[#1f1f1f] border border-[#1f1f1f]">
|
||||||
|
<Phone className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[11px] font-black uppercase tracking-wider text-[#666666]">Telefon bezpośredni</div>
|
||||||
|
<a href={`tel:${phone.replace(/\s+/g, '')}`} className="text-base font-black text-[#1f1f1f] font-mono hover:underline">
|
||||||
|
{phone}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnimateIn>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{email && (
|
||||||
|
<AnimateIn delay={200} direction="right">
|
||||||
|
<div className="flex items-center gap-4 bg-white p-5 border-2 border-[#1f1f1f] hover-lift">
|
||||||
|
<div className="flex h-11 w-11 shrink-0 items-center justify-center bg-[#FFED00] text-[#1f1f1f] border border-[#1f1f1f]">
|
||||||
|
<Mail className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[11px] font-black uppercase tracking-wider text-[#666666]">Kontakt e-mail</div>
|
||||||
|
<a href={`mailto:${email}`} className="text-sm font-bold text-[#1f1f1f] hover:underline">
|
||||||
|
{email}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnimateIn>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{workingHours && (
|
||||||
|
<AnimateIn delay={300} direction="right">
|
||||||
|
<div className="flex items-center gap-4 bg-white p-5 border-2 border-[#1f1f1f] hover-lift">
|
||||||
|
<div className="flex h-11 w-11 shrink-0 items-center justify-center bg-[#FFED00] text-[#1f1f1f] border border-[#1f1f1f]">
|
||||||
|
<Clock className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[11px] font-black uppercase tracking-wider text-[#666666]">Godziny pracy</div>
|
||||||
|
<div className="text-sm font-bold text-[#1f1f1f]">
|
||||||
|
{workingHours}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnimateIn>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Form Box */}
|
||||||
|
<div className="lg:col-span-3">
|
||||||
|
<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]">
|
||||||
|
{submitted ? (
|
||||||
|
<div className="text-center py-12 space-y-4">
|
||||||
|
<div className="flex h-14 w-14 items-center justify-center bg-[#FFED00] text-[#1f1f1f] border-2 border-[#1f1f1f] mx-auto">
|
||||||
|
<Check className="h-8 w-8 font-black" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-black uppercase text-[#1f1f1f]">
|
||||||
|
Zgłoszenie zostało wysłane
|
||||||
|
</h3>
|
||||||
|
<p className="text-[#666666] text-sm max-w-md mx-auto leading-relaxed">
|
||||||
|
Nasz doradca techniczny skontaktuje się z Państwem telefonicznie w celu przygotowania dokładnej wyceny.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2">
|
||||||
|
Imię i Nazwisko *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="Jan Kowalski"
|
||||||
|
className="w-full border-2 border-[#1f1f1f] bg-white px-4 py-3.5 text-sm text-[#1f1f1f] focus:bg-[#fffde6] focus:outline-none transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2">
|
||||||
|
Numer Telefonu *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
required
|
||||||
|
placeholder="+48 600 000 000"
|
||||||
|
className="w-full border-2 border-[#1f1f1f] bg-white px-4 py-3.5 text-sm text-[#1f1f1f] focus:bg-[#fffde6] focus:outline-none transition-colors font-mono"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2">
|
||||||
|
Miejscowość
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="np. Opole, Brzeg, Nysa"
|
||||||
|
className="w-full border-2 border-[#1f1f1f] bg-white px-4 py-3.5 text-sm text-[#1f1f1f] focus:border-[#FFED00] focus:outline-none transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2">
|
||||||
|
Szacowany Metraż (m²)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="np. 180 m²"
|
||||||
|
className="w-full border-2 border-[#1f1f1f] bg-white px-4 py-3.5 text-sm text-[#1f1f1f] focus:border-[#FFED00] focus:outline-none transition-colors font-mono"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-black uppercase tracking-wider text-[#1f1f1f] mb-2">
|
||||||
|
Zakres prac / stan nawierzchni
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
placeholder="Np. usuwanie mchu z fug, mycie gorącowodne Kärcher/Mosmatic, odplamianie oleju, impregnacja..."
|
||||||
|
className="w-full border-2 border-[#1f1f1f] bg-white px-4 py-3.5 text-sm text-[#1f1f1f] focus:border-[#FFED00] focus:outline-none transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Custom Styled Kärcher Checkbox */}
|
||||||
|
<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>
|
||||||
|
<span className="text-xs text-[#555555] leading-relaxed group-hover:text-[#1f1f1f] transition-colors">
|
||||||
|
{consentText}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading || !consentChecked}
|
||||||
|
className="w-full inline-flex items-center justify-center gap-3 bg-[#FFED00] hover:bg-[#fae100] border-2 border-[#1f1f1f] py-4 px-8 text-xs font-black uppercase tracking-widest text-[#1f1f1f] transition-all cursor-pointer disabled:opacity-50 shadow-xs hover:-translate-y-0.5"
|
||||||
|
>
|
||||||
|
<Send className="h-4 w-4" />
|
||||||
|
<span>{loading ? 'Wysyłanie...' : submitButtonText}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AnimateIn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const FormBlock: Block = {
|
||||||
|
slug: 'formBlock',
|
||||||
|
labels: {
|
||||||
|
singular: 'Sekcja Formularza i Kontaktu',
|
||||||
|
plural: 'Sekcje Formularza i Kontaktu',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'badge',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'heading',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'subheading',
|
||||||
|
type: 'textarea',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'phone',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'email',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'workingHours',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Phone, ArrowRight, Check } from 'lucide-react'
|
||||||
|
import { AnimateIn } from '@/components/AnimateIn'
|
||||||
|
|
||||||
|
interface TrustBadge {
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HeroBlockProps {
|
||||||
|
badge?: string
|
||||||
|
heading: string
|
||||||
|
subheading?: string
|
||||||
|
trustBadges?: TrustBadge[]
|
||||||
|
ctaPhone?: string
|
||||||
|
ctaPhoneLabel?: string
|
||||||
|
ctaSecondaryLabel?: string
|
||||||
|
ctaSecondaryUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HeroBlockComponent(props: HeroBlockProps) {
|
||||||
|
const {
|
||||||
|
badge,
|
||||||
|
heading,
|
||||||
|
subheading,
|
||||||
|
trustBadges,
|
||||||
|
ctaPhone,
|
||||||
|
ctaPhoneLabel = 'Zadzwoń po bezpłatną wycenę',
|
||||||
|
ctaSecondaryLabel = 'Zobacz cennik za m²',
|
||||||
|
ctaSecondaryUrl = '#cennik',
|
||||||
|
} = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
{/* Hero Banner Section with Background Photo */}
|
||||||
|
<section
|
||||||
|
className="relative w-full min-h-[calc(100vh-8rem)] max-h-[780px] flex items-center bg-cover bg-center overflow-hidden"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "url('/images/paving-hero.jpg')",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Dark gradient overlay for text readability */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-[#141414]/90 via-[#141414]/65 to-black/20" />
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl relative z-10 py-10 lg:py-14">
|
||||||
|
{/* Compact Architectural Content Card */}
|
||||||
|
<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]">
|
||||||
|
{badge && (
|
||||||
|
<div className="inline-block bg-[#1f1f1f] text-[#FFED00] text-[11px] font-black uppercase tracking-widest px-3 py-1 mb-4">
|
||||||
|
{badge}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f] leading-snug mb-4">
|
||||||
|
{heading}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{subheading && (
|
||||||
|
<p className="text-xs sm:text-sm text-[#444444] leading-relaxed mb-6 font-medium line-clamp-3">
|
||||||
|
{subheading}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3">
|
||||||
|
{ctaPhone && (
|
||||||
|
<a
|
||||||
|
href={`tel:${ctaPhone.replace(/\s+/g, '')}`}
|
||||||
|
className="inline-flex items-center justify-center gap-2.5 bg-[#FFED00] hover:bg-[#fae100] text-[#1f1f1f] border-2 border-[#1f1f1f] px-6 py-3.5 text-xs font-black uppercase tracking-widest transition-all shadow-xs hover:-translate-y-0.5"
|
||||||
|
>
|
||||||
|
<Phone className="h-4 w-4" />
|
||||||
|
<span>{ctaPhoneLabel}</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{ctaSecondaryLabel && (
|
||||||
|
<a
|
||||||
|
href={ctaSecondaryUrl}
|
||||||
|
className="inline-flex items-center justify-center gap-2 bg-[#1f1f1f] hover:bg-[#333333] text-white px-6 py-3.5 text-xs font-black uppercase tracking-widest transition-all hover:-translate-y-0.5"
|
||||||
|
>
|
||||||
|
<span>{ctaSecondaryLabel}</span>
|
||||||
|
<ArrowRight className="h-3.5 w-3.5 text-[#FFED00]" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnimateIn>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Full-Width Horizontal Feature Strip Directly Under Hero with Stagger */}
|
||||||
|
{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="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">
|
||||||
|
{trustBadges.slice(0, 4).map((badgeItem, idx) => (
|
||||||
|
<AnimateIn key={idx} delay={idx * 100} direction="up">
|
||||||
|
<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]">
|
||||||
|
<Check className="h-4 w-4 text-[#1f1f1f] stroke-[3]" />
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-black uppercase tracking-tight text-[#f4f4f4] leading-snug">
|
||||||
|
{badgeItem.text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</AnimateIn>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const HeroBlock: Block = {
|
||||||
|
slug: 'hero',
|
||||||
|
labels: {
|
||||||
|
singular: 'Sekcja Hero',
|
||||||
|
plural: 'Sekcje Hero',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'badge',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
admin: {
|
||||||
|
description: 'Mały wyróżnik nad nagłówkiem, np. "Specjaliści od kostki brukowej"',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'heading',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
localized: true,
|
||||||
|
admin: {
|
||||||
|
description: 'Główny nagłówek H1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'subheading',
|
||||||
|
type: 'textarea',
|
||||||
|
localized: true,
|
||||||
|
admin: {
|
||||||
|
description: 'Akapit pod nagłówkiem (opis / lead)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'trustBadges',
|
||||||
|
type: 'array',
|
||||||
|
localized: true,
|
||||||
|
labels: {
|
||||||
|
singular: 'Wyróżnik zaufania',
|
||||||
|
plural: 'Wyróżniki zaufania',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'text',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ctaPhone',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
admin: {
|
||||||
|
description: 'Numer telefonu do szybkiego kontaktu, np. "+48 570 727 895"',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ctaPhoneLabel',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
admin: {
|
||||||
|
description: 'Etykieta przycisku telefonu, np. "Zadzwoń po wycenę"',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ctaSecondaryLabel',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
admin: {
|
||||||
|
description: 'Etykieta drugiego przycisku, np. "Sprawdź cennik"',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ctaSecondaryTarget',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'pages',
|
||||||
|
admin: {
|
||||||
|
description: 'Strona docelowa drugiego przycisku',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'backgroundImage',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'media',
|
||||||
|
admin: {
|
||||||
|
description: 'Zdjęcie tła sekcji hero',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Check } from 'lucide-react'
|
||||||
|
import { AnimateIn } from '@/components/AnimateIn'
|
||||||
|
|
||||||
|
interface Feature {
|
||||||
|
feature: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Package {
|
||||||
|
name: string
|
||||||
|
price: string
|
||||||
|
description?: string
|
||||||
|
features?: Feature[]
|
||||||
|
isPopular?: boolean
|
||||||
|
ctaLabel?: string
|
||||||
|
ctaUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PricingTableBlockProps {
|
||||||
|
badge?: string
|
||||||
|
heading: string
|
||||||
|
subheading?: string
|
||||||
|
disclaimer?: string
|
||||||
|
packages?: Package[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PricingTableBlockComponent(props: PricingTableBlockProps) {
|
||||||
|
const { badge, heading, subheading, disclaimer, packages } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="cennik" className="py-24 bg-[#f4f4f4] border-b-2 border-[#1f1f1f] scroll-mt-20">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl">
|
||||||
|
<AnimateIn direction="up">
|
||||||
|
<div className="max-w-3xl mb-14">
|
||||||
|
{badge && (
|
||||||
|
<div className="inline-block bg-[#1f1f1f] text-[#FFED00] text-xs font-black uppercase tracking-widest px-3 py-1 mb-4">
|
||||||
|
{badge}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f]">
|
||||||
|
{heading}
|
||||||
|
</h2>
|
||||||
|
{subheading && (
|
||||||
|
<p className="mt-4 text-base text-[#666666] leading-relaxed font-medium">
|
||||||
|
{subheading}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AnimateIn>
|
||||||
|
|
||||||
|
{packages && packages.length > 0 && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 items-stretch">
|
||||||
|
{packages.map((pkg, idx) => (
|
||||||
|
<AnimateIn key={idx} delay={idx * 150} direction="up" className="h-full">
|
||||||
|
<div
|
||||||
|
className={`relative p-8 h-full flex flex-col justify-between bg-white border-2 border-[#1f1f1f] hover-lift transition-all ${
|
||||||
|
pkg.isPopular ? 'border-t-8 border-t-[#FFED00] shadow-md' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{pkg.isPopular && (
|
||||||
|
<div className="absolute top-0 right-0 bg-[#FFED00] text-[#1f1f1f] border-b-2 border-l-2 border-[#1f1f1f] text-[10px] font-black uppercase tracking-widest px-4 py-1">
|
||||||
|
Polecany Pakiet
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-black uppercase text-[#1f1f1f]">
|
||||||
|
{pkg.name}
|
||||||
|
</h3>
|
||||||
|
{pkg.description && (
|
||||||
|
<p className="mt-2 text-xs text-[#666666] leading-relaxed min-h-[36px]">
|
||||||
|
{pkg.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<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">
|
||||||
|
{pkg.price}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pkg.features && pkg.features.length > 0 && (
|
||||||
|
<ul className="space-y-3 text-xs sm:text-sm">
|
||||||
|
{pkg.features.map((feat, fIdx) => (
|
||||||
|
<li key={fIdx} className="flex items-start gap-3 text-[#222222]">
|
||||||
|
<Check className="h-4 w-4 text-[#1f1f1f] bg-[#FFED00] p-0.5 shrink-0 mt-0.5 font-bold" />
|
||||||
|
<span className="font-semibold">{feat.feature}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-10 pt-6 border-t-2 border-[#f0f0f0]">
|
||||||
|
<a
|
||||||
|
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 ${
|
||||||
|
pkg.isPopular
|
||||||
|
? 'bg-[#FFED00] hover:bg-[#fae100] text-[#1f1f1f] border-2 border-[#1f1f1f] shadow-xs'
|
||||||
|
: 'bg-[#1f1f1f] text-white hover:bg-[#333333]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{pkg.ctaLabel || 'Wybierz pakiet'}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnimateIn>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{disclaimer && (
|
||||||
|
<AnimateIn delay={400} direction="none">
|
||||||
|
<p className="mt-10 text-xs text-[#666666] font-medium">
|
||||||
|
{disclaimer}
|
||||||
|
</p>
|
||||||
|
</AnimateIn>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const PricingTableBlock: Block = {
|
||||||
|
slug: 'pricingTable',
|
||||||
|
labels: {
|
||||||
|
singular: 'Tabela Cennika',
|
||||||
|
plural: 'Tabele Cennika',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'badge',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'heading',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'subheading',
|
||||||
|
type: 'textarea',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'disclaimer',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
admin: {
|
||||||
|
description: 'Np. "*Podane ceny są cenami orientacyjnymi netto za m2."',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'packages',
|
||||||
|
type: 'array',
|
||||||
|
localized: true,
|
||||||
|
labels: {
|
||||||
|
singular: 'Pakiet cennika',
|
||||||
|
plural: 'Pakiety cennika',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'price',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: 'Np. "od 5 zł / m2"',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
type: 'textarea',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'isPopular',
|
||||||
|
type: 'checkbox',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'features',
|
||||||
|
type: 'array',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'feature',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ctaLabel',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: 'Zamów wycenę',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { X, Check } from 'lucide-react'
|
||||||
|
import { AnimateIn } from '@/components/AnimateIn'
|
||||||
|
|
||||||
|
interface ComparisonItem {
|
||||||
|
problemTitle: string
|
||||||
|
problemDescription?: string
|
||||||
|
solutionTitle: string
|
||||||
|
solutionDescription?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProblemSolutionBlockProps {
|
||||||
|
badge?: string
|
||||||
|
heading: string
|
||||||
|
subheading?: string
|
||||||
|
items?: ComparisonItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProblemSolutionBlockComponent(props: ProblemSolutionBlockProps) {
|
||||||
|
const { badge, heading, subheading, items } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-24 bg-white border-b-2 border-[#1f1f1f]">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl">
|
||||||
|
<AnimateIn direction="up">
|
||||||
|
<div className="max-w-3xl mb-14">
|
||||||
|
{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]">
|
||||||
|
{badge}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f]">
|
||||||
|
{heading}
|
||||||
|
</h2>
|
||||||
|
{subheading && (
|
||||||
|
<p className="mt-4 text-base text-[#666666] leading-relaxed font-medium">
|
||||||
|
{subheading}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AnimateIn>
|
||||||
|
|
||||||
|
{items && items.length > 0 && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 items-stretch">
|
||||||
|
{items.map((item, idx) => (
|
||||||
|
<AnimateIn key={idx} delay={idx * 150} direction="up" className="h-full">
|
||||||
|
<div
|
||||||
|
className="bg-[#f8f8f8] border-2 border-[#1f1f1f] p-8 space-y-6 shadow-sm hover-lift h-full flex flex-col justify-between"
|
||||||
|
>
|
||||||
|
{/* Problem */}
|
||||||
|
<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">
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-black uppercase tracking-wide text-rose-950">
|
||||||
|
{item.problemTitle}
|
||||||
|
</h4>
|
||||||
|
{item.problemDescription && (
|
||||||
|
<p className="mt-1 text-xs sm:text-sm text-[#666666] leading-relaxed font-medium">
|
||||||
|
{item.problemDescription}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Solution */}
|
||||||
|
<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">
|
||||||
|
<Check className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-black uppercase tracking-wide text-[#1f1f1f]">
|
||||||
|
{item.solutionTitle}
|
||||||
|
</h4>
|
||||||
|
{item.solutionDescription && (
|
||||||
|
<p className="mt-1 text-xs sm:text-sm text-[#444444] leading-relaxed font-medium">
|
||||||
|
{item.solutionDescription}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnimateIn>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const ProblemSolutionBlock: Block = {
|
||||||
|
slug: 'problemSolution',
|
||||||
|
labels: {
|
||||||
|
singular: 'Sekcja Problem vs Rozwiązanie',
|
||||||
|
plural: 'Sekcje Problem vs Rozwiązanie',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'badge',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'heading',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'subheading',
|
||||||
|
type: 'textarea',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'items',
|
||||||
|
type: 'array',
|
||||||
|
localized: true,
|
||||||
|
labels: {
|
||||||
|
singular: 'Karta Problem-Rozwiązanie',
|
||||||
|
plural: 'Karty Problem-Rozwiązanie',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'problemTitle',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'problemDescription',
|
||||||
|
type: 'textarea',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'solutionTitle',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'solutionDescription',
|
||||||
|
type: 'textarea',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { AnimateIn } from '@/components/AnimateIn'
|
||||||
|
|
||||||
|
interface Step {
|
||||||
|
stepNumber: string
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProcessStepsBlockProps {
|
||||||
|
badge?: string
|
||||||
|
heading: string
|
||||||
|
subheading?: string
|
||||||
|
steps?: Step[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProcessStepsBlockComponent(props: ProcessStepsBlockProps) {
|
||||||
|
const { badge, heading, subheading, steps } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-24 bg-[#f4f4f4] border-b-2 border-[#1f1f1f]">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl">
|
||||||
|
<AnimateIn direction="up">
|
||||||
|
<div className="max-w-3xl mb-14">
|
||||||
|
{badge && (
|
||||||
|
<div className="inline-block bg-[#1f1f1f] text-[#FFED00] text-xs font-black uppercase tracking-widest px-3 py-1 mb-4">
|
||||||
|
{badge}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f]">
|
||||||
|
{heading}
|
||||||
|
</h2>
|
||||||
|
{subheading && (
|
||||||
|
<p className="mt-4 text-base text-[#666666] leading-relaxed font-medium">
|
||||||
|
{subheading}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AnimateIn>
|
||||||
|
|
||||||
|
{steps && steps.length > 0 && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-6 items-stretch">
|
||||||
|
{steps.map((step, idx) => (
|
||||||
|
<AnimateIn key={idx} delay={idx * 100} direction="up" className="h-full">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
{step.stepNumber}
|
||||||
|
</div>
|
||||||
|
<h4 className="text-sm font-black uppercase tracking-tight text-[#1f1f1f] mb-3">
|
||||||
|
{step.title}
|
||||||
|
</h4>
|
||||||
|
{step.description && (
|
||||||
|
<p className="text-xs text-[#555555] leading-relaxed">
|
||||||
|
{step.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnimateIn>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const ProcessStepsBlock: Block = {
|
||||||
|
slug: 'processSteps',
|
||||||
|
labels: {
|
||||||
|
singular: 'Sekcja Etapów Realizacji',
|
||||||
|
plural: 'Sekcje Etapów Realizacji',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'badge',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'heading',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'subheading',
|
||||||
|
type: 'textarea',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'steps',
|
||||||
|
type: 'array',
|
||||||
|
localized: true,
|
||||||
|
labels: {
|
||||||
|
singular: 'Krok procesu',
|
||||||
|
plural: 'Kroki procesu',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'stepNumber',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: 'Np. "01", "Krok 1"',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
type: 'textarea',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Star } from 'lucide-react'
|
||||||
|
import { AnimateIn } from '@/components/AnimateIn'
|
||||||
|
|
||||||
|
interface ReviewItem {
|
||||||
|
author: string
|
||||||
|
location?: string
|
||||||
|
rating?: number
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReviewsBlockProps {
|
||||||
|
badge?: string
|
||||||
|
heading: string
|
||||||
|
subheading?: string
|
||||||
|
items?: ReviewItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReviewsBlockComponent(props: ReviewsBlockProps) {
|
||||||
|
const { badge, heading, subheading, items } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-24 bg-white border-b-2 border-[#1f1f1f]">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl">
|
||||||
|
<AnimateIn direction="up">
|
||||||
|
<div className="max-w-3xl mb-14">
|
||||||
|
{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]">
|
||||||
|
{badge}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f]">
|
||||||
|
{heading}
|
||||||
|
</h2>
|
||||||
|
{subheading && (
|
||||||
|
<p className="mt-4 text-base text-[#666666] leading-relaxed font-medium">
|
||||||
|
{subheading}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AnimateIn>
|
||||||
|
|
||||||
|
{items && items.length > 0 && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 items-stretch">
|
||||||
|
{items.map((item, idx) => (
|
||||||
|
<AnimateIn key={idx} delay={idx * 150} direction="up" className="h-full">
|
||||||
|
<div
|
||||||
|
className="bg-[#f4f4f4] border-2 border-[#1f1f1f] p-8 h-full flex flex-col justify-between hover-lift transition-all"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-1 text-[#1f1f1f] mb-6">
|
||||||
|
{Array.from({ length: item.rating || 5 }).map((_, sIdx) => (
|
||||||
|
<Star key={sIdx} className="h-5 w-5 fill-[#FFED00] stroke-[#1f1f1f]" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-[#333333] leading-relaxed font-medium">
|
||||||
|
"{item.content}"
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 pt-6 border-t-2 border-[#e0e0e0]">
|
||||||
|
<div className="font-black uppercase text-[#1f1f1f] text-xs tracking-wider">
|
||||||
|
{item.author}
|
||||||
|
</div>
|
||||||
|
{item.location && (
|
||||||
|
<div className="text-xs text-[#666666] mt-0.5 font-medium">
|
||||||
|
{item.location}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnimateIn>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const ReviewsBlock: Block = {
|
||||||
|
slug: 'reviews',
|
||||||
|
labels: {
|
||||||
|
singular: 'Sekcja Opinii Klientów',
|
||||||
|
plural: 'Sekcje Opinii Klientów',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'badge',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'heading',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'subheading',
|
||||||
|
type: 'textarea',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'items',
|
||||||
|
type: 'array',
|
||||||
|
localized: true,
|
||||||
|
labels: {
|
||||||
|
singular: 'Opinia klienta',
|
||||||
|
plural: 'Opinie klientów',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'author',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'location',
|
||||||
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
description: 'Np. "Wrocław, Krzyki - Dom jednorodzinny"',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rating',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
defaultValue: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'content',
|
||||||
|
type: 'textarea',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Check } from 'lucide-react'
|
||||||
|
import { AnimateIn } from '@/components/AnimateIn'
|
||||||
|
|
||||||
|
interface TechItem {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TechEquipmentBlockProps {
|
||||||
|
badge?: string
|
||||||
|
heading: string
|
||||||
|
subheading?: string
|
||||||
|
items?: TechItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TechEquipmentBlockComponent(props: TechEquipmentBlockProps) {
|
||||||
|
const { badge, heading, subheading, items } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-24 bg-white border-b-2 border-[#1f1f1f]">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl">
|
||||||
|
<AnimateIn direction="up">
|
||||||
|
<div className="max-w-3xl mb-14">
|
||||||
|
{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]">
|
||||||
|
{badge}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-black uppercase tracking-tight text-[#1f1f1f]">
|
||||||
|
{heading}
|
||||||
|
</h2>
|
||||||
|
{subheading && (
|
||||||
|
<p className="mt-4 text-base text-[#666666] leading-relaxed font-medium">
|
||||||
|
{subheading}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AnimateIn>
|
||||||
|
|
||||||
|
{items && items.length > 0 && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 items-stretch">
|
||||||
|
{items.map((item, idx) => (
|
||||||
|
<AnimateIn key={idx} delay={idx * 150} direction="up" className="h-full">
|
||||||
|
<div
|
||||||
|
className="bg-[#f4f4f4] border-2 border-[#1f1f1f] p-8 h-full flex flex-col justify-between hover-lift transition-all"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="flex h-10 w-10 items-center justify-center bg-[#FFED00] text-[#1f1f1f] border border-[#1f1f1f] mb-6">
|
||||||
|
<Check className="h-5 w-5 font-black" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-base font-black uppercase text-[#1f1f1f] mb-3">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
{item.description && (
|
||||||
|
<p className="text-xs sm:text-sm text-[#555555] leading-relaxed">
|
||||||
|
{item.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnimateIn>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const TechEquipmentBlock: Block = {
|
||||||
|
slug: 'techEquipment',
|
||||||
|
labels: {
|
||||||
|
singular: 'Sekcja Technologia i Sprzęt',
|
||||||
|
plural: 'Sekcje Technologia i Sprzęt',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'badge',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'heading',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'subheading',
|
||||||
|
type: 'textarea',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'items',
|
||||||
|
type: 'array',
|
||||||
|
localized: true,
|
||||||
|
labels: {
|
||||||
|
singular: 'Element sprzętowy',
|
||||||
|
plural: 'Elementy sprzętowe',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
type: 'textarea',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'image',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'media',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import type { BlockComponentMap } from '@intecion/ipal-kit/rsc'
|
||||||
|
import { HeroBlockComponent } from '@/blocks/Hero/Component'
|
||||||
|
import { ProblemSolutionBlockComponent } from '@/blocks/ProblemSolution/Component'
|
||||||
|
import { ProcessStepsBlockComponent } from '@/blocks/ProcessSteps/Component'
|
||||||
|
import { BeforeAfterGalleryBlockComponent } from '@/blocks/BeforeAfterGallery/Component'
|
||||||
|
import { PricingTableBlockComponent } from '@/blocks/PricingTable/Component'
|
||||||
|
import { TechEquipmentBlockComponent } from '@/blocks/TechEquipment/Component'
|
||||||
|
import { FaqAccordionBlockComponent } from '@/blocks/FaqAccordion/Component'
|
||||||
|
import { ReviewsBlockComponent } from '@/blocks/Reviews/Component'
|
||||||
|
import { FormBlockComponent } from '@/blocks/Form/Component'
|
||||||
|
import { ContentBlockComponent } from '@/blocks/Content/Component'
|
||||||
|
import { BlogListBlockComponent } from '@/blocks/BlogList/Component'
|
||||||
|
|
||||||
|
export const blockRegistry: BlockComponentMap = {
|
||||||
|
hero: HeroBlockComponent as never,
|
||||||
|
problemSolution: ProblemSolutionBlockComponent as never,
|
||||||
|
processSteps: ProcessStepsBlockComponent as never,
|
||||||
|
beforeAfterGallery: BeforeAfterGalleryBlockComponent as never,
|
||||||
|
pricingTable: PricingTableBlockComponent as never,
|
||||||
|
techEquipment: TechEquipmentBlockComponent as never,
|
||||||
|
faqAccordion: FaqAccordionBlockComponent as never,
|
||||||
|
reviews: ReviewsBlockComponent as never,
|
||||||
|
formBlock: FormBlockComponent as never,
|
||||||
|
content: ContentBlockComponent as never,
|
||||||
|
blogList: BlogListBlockComponent as never,
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
|
export const Media: CollectionConfig = {
|
||||||
|
slug: 'media',
|
||||||
|
access: {
|
||||||
|
read: () => true,
|
||||||
|
},
|
||||||
|
upload: {
|
||||||
|
staticDir: 'media',
|
||||||
|
adminThumbnail: 'thumbnail',
|
||||||
|
imageSizes: [
|
||||||
|
{
|
||||||
|
name: 'thumbnail',
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
position: 'centre',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'card',
|
||||||
|
width: 768,
|
||||||
|
height: 512,
|
||||||
|
position: 'centre',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hero',
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
position: 'centre',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'alt',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
localized: true,
|
||||||
|
admin: {
|
||||||
|
description: 'Opis alternatywny dla dostępności (SEO i czytniki ekranu)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import type { CollectionConfig } from 'payload'
|
||||||
|
import { buildSlugField } from '@intecion/ipal-kit'
|
||||||
|
import { HeroBlock } from '@/blocks/Hero/config'
|
||||||
|
import { ProblemSolutionBlock } from '@/blocks/ProblemSolution/config'
|
||||||
|
import { ProcessStepsBlock } from '@/blocks/ProcessSteps/config'
|
||||||
|
import { BeforeAfterGalleryBlock } from '@/blocks/BeforeAfterGallery/config'
|
||||||
|
import { PricingTableBlock } from '@/blocks/PricingTable/config'
|
||||||
|
import { TechEquipmentBlock } from '@/blocks/TechEquipment/config'
|
||||||
|
import { FaqAccordionBlock } from '@/blocks/FaqAccordion/config'
|
||||||
|
import { ReviewsBlock } from '@/blocks/Reviews/config'
|
||||||
|
import { FormBlock } from '@/blocks/Form/config'
|
||||||
|
import { ContentBlock } from '@/blocks/Content/config'
|
||||||
|
import { BlogListBlock } from '@/blocks/BlogList/config'
|
||||||
|
|
||||||
|
export const Pages: CollectionConfig = {
|
||||||
|
slug: 'pages',
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'title',
|
||||||
|
defaultColumns: ['title', 'slug', 'updatedAt'],
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
read: () => true,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
buildSlugField({ from: 'title' }),
|
||||||
|
{
|
||||||
|
name: 'layout',
|
||||||
|
type: 'blocks',
|
||||||
|
blocks: [
|
||||||
|
HeroBlock,
|
||||||
|
ProblemSolutionBlock,
|
||||||
|
ProcessStepsBlock,
|
||||||
|
BeforeAfterGalleryBlock,
|
||||||
|
PricingTableBlock,
|
||||||
|
TechEquipmentBlock,
|
||||||
|
FaqAccordionBlock,
|
||||||
|
ReviewsBlock,
|
||||||
|
FormBlock,
|
||||||
|
ContentBlock,
|
||||||
|
BlogListBlock,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
|
export const Users: CollectionConfig = {
|
||||||
|
slug: 'users',
|
||||||
|
auth: true,
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'email',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
interface AnimateInProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
delay?: number // in ms
|
||||||
|
direction?: 'up' | 'down' | 'left' | 'right' | 'none'
|
||||||
|
className?: string
|
||||||
|
threshold?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AnimateIn({
|
||||||
|
children,
|
||||||
|
delay = 0,
|
||||||
|
direction = 'up',
|
||||||
|
className = '',
|
||||||
|
threshold = 0.15,
|
||||||
|
}: AnimateInProps) {
|
||||||
|
const [isVisible, setIsVisible] = useState(false)
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setIsVisible(true)
|
||||||
|
observer.unobserve(entry.target)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
threshold,
|
||||||
|
rootMargin: '0px 0px -40px 0px',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const currentRef = ref.current
|
||||||
|
if (currentRef) {
|
||||||
|
observer.observe(currentRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (currentRef) {
|
||||||
|
observer.unobserve(currentRef)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [threshold])
|
||||||
|
|
||||||
|
const getTransform = () => {
|
||||||
|
if (isVisible) return 'translate3d(0, 0, 0) scale(1)'
|
||||||
|
switch (direction) {
|
||||||
|
case 'up':
|
||||||
|
return 'translate3d(0, 32px, 0)'
|
||||||
|
case 'down':
|
||||||
|
return 'translate3d(0, -32px, 0)'
|
||||||
|
case 'left':
|
||||||
|
return 'translate3d(32px, 0, 0)'
|
||||||
|
case 'right':
|
||||||
|
return 'translate3d(-32px, 0, 0)'
|
||||||
|
case 'none':
|
||||||
|
return 'scale(0.96)'
|
||||||
|
default:
|
||||||
|
return 'translate3d(0, 32px, 0)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
style={{
|
||||||
|
transitionDuration: '700ms',
|
||||||
|
transitionTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
|
transitionDelay: `${delay}ms`,
|
||||||
|
transform: getTransform(),
|
||||||
|
opacity: isVisible ? 1 : 0,
|
||||||
|
}}
|
||||||
|
className={`transition-all ${className}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { MapPin, Phone, Mail, ShieldCheck, Cookie } from 'lucide-react'
|
||||||
|
|
||||||
|
export interface FooterNavItem {
|
||||||
|
id?: string
|
||||||
|
label: string
|
||||||
|
href: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FooterProps {
|
||||||
|
companyName?: string | null
|
||||||
|
footerTagline?: string | null
|
||||||
|
nip?: string | null
|
||||||
|
regon?: string | null
|
||||||
|
address?: string | null
|
||||||
|
city?: string | null
|
||||||
|
phone?: string | null
|
||||||
|
email?: string | null
|
||||||
|
items?: FooterNavItem[]
|
||||||
|
privacyHref?: string
|
||||||
|
privacyLabel?: string
|
||||||
|
cookieHref?: string
|
||||||
|
cookieLabel?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Footer({
|
||||||
|
companyName,
|
||||||
|
footerTagline,
|
||||||
|
nip,
|
||||||
|
regon,
|
||||||
|
address,
|
||||||
|
city,
|
||||||
|
phone,
|
||||||
|
email,
|
||||||
|
items,
|
||||||
|
privacyHref = '/polityka-prywatnosci',
|
||||||
|
privacyLabel = 'Polityka Prywatności',
|
||||||
|
cookieHref = '/polityka-cookies',
|
||||||
|
cookieLabel = 'Polityka Cookies',
|
||||||
|
}: FooterProps) {
|
||||||
|
return (
|
||||||
|
<footer className="bg-[#1f1f1f] text-[#d0d0d0] py-16 border-t-4 border-[#FFED00]">
|
||||||
|
<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">
|
||||||
|
{/* Company Info */}
|
||||||
|
<div className="md:col-span-2 space-y-4">
|
||||||
|
<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">
|
||||||
|
POI
|
||||||
|
</div>
|
||||||
|
{companyName && (
|
||||||
|
<span className="text-xl font-black uppercase tracking-tight text-white">
|
||||||
|
{companyName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{footerTagline && (
|
||||||
|
<p className="text-xs sm:text-sm text-[#a0a0a0] leading-relaxed max-w-md">
|
||||||
|
{footerTagline}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="text-xs text-[#888888] font-mono space-y-1 pt-2 border-t border-[#333333]">
|
||||||
|
{nip && <div>NIP: <span className="text-[#e0e0e0] font-semibold">{nip}</span></div>}
|
||||||
|
{regon && <div>REGON: <span className="text-[#e0e0e0] font-semibold">{regon}</span></div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation Links */}
|
||||||
|
{items && items.length > 0 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="text-xs font-black uppercase tracking-widest text-[#FFED00] border-b border-[#333333] pb-2">
|
||||||
|
Nawigacja
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-2.5 text-xs font-bold uppercase tracking-wider">
|
||||||
|
{items.map((item, idx) => (
|
||||||
|
<li key={item.id || idx}>
|
||||||
|
<a href={item.href} className="text-[#d0d0d0] hover:text-[#FFED00] transition-colors block py-0.5">
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Contact Details */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="text-xs font-black uppercase tracking-widest text-[#FFED00] border-b border-[#333333] pb-2">
|
||||||
|
Kontakt
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-3 text-xs">
|
||||||
|
{phone && (
|
||||||
|
<li className="flex items-center gap-2.5">
|
||||||
|
<Phone className="h-4 w-4 text-[#FFED00] shrink-0" />
|
||||||
|
<a href={`tel:${phone.replace(/\s+/g, '')}`} className="text-white hover:text-[#FFED00] font-mono font-black text-sm">
|
||||||
|
{phone}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{email && (
|
||||||
|
<li className="flex items-center gap-2.5">
|
||||||
|
<Mail className="h-4 w-4 text-[#FFED00] shrink-0" />
|
||||||
|
<a href={`mailto:${email}`} className="text-[#d0d0d0] hover:text-white font-medium">
|
||||||
|
{email}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{(address || city) && (
|
||||||
|
<li className="flex items-start gap-2.5">
|
||||||
|
<MapPin className="h-4 w-4 text-[#FFED00] shrink-0 mt-0.5" />
|
||||||
|
<span className="text-[#a0a0a0]">
|
||||||
|
{address}
|
||||||
|
{address && city ? ', ' : ''}
|
||||||
|
{city}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Bar with Privacy & Cookie Links */}
|
||||||
|
<div className="mt-12 pt-8 border-t border-[#333333] flex flex-col sm:flex-row items-center justify-between gap-4 text-xs text-[#888888]">
|
||||||
|
<div>
|
||||||
|
© {new Date().getFullYear()} {companyName}. Wszelkie prawa zastrzeżone.
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-6 text-xs font-bold uppercase tracking-wider">
|
||||||
|
{privacyHref && (
|
||||||
|
<a href={privacyHref} className="text-[#a0a0a0] hover:text-[#FFED00] transition-colors flex items-center gap-1.5">
|
||||||
|
<ShieldCheck className="h-3.5 w-3.5 text-[#FFED00]" />
|
||||||
|
<span>{privacyLabel}</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{cookieHref && (
|
||||||
|
<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>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useState, useRef, useEffect } from 'react'
|
||||||
|
import { ArrowRight, Globe, ChevronDown } from 'lucide-react'
|
||||||
|
import { usePathname } from 'next/navigation'
|
||||||
|
|
||||||
|
export interface NavItem {
|
||||||
|
id?: string
|
||||||
|
label: string
|
||||||
|
href: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
companyName?: string | null
|
||||||
|
tagline?: string | null
|
||||||
|
items?: NavItem[]
|
||||||
|
phone?: string | null
|
||||||
|
homeHref?: string
|
||||||
|
currentLocale?: string
|
||||||
|
contactHref?: string
|
||||||
|
contactLabel?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Header({
|
||||||
|
companyName,
|
||||||
|
tagline,
|
||||||
|
items,
|
||||||
|
phone,
|
||||||
|
homeHref = '/',
|
||||||
|
currentLocale = 'pl',
|
||||||
|
contactHref = '/kontakt',
|
||||||
|
contactLabel = 'KONTAKT',
|
||||||
|
}: HeaderProps) {
|
||||||
|
const pathname = usePathname() || ''
|
||||||
|
const [langDropdownOpen, setLangDropdownOpen] = useState(false)
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// Close dropdown on outside click
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setLangDropdownOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', handleClickOutside)
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Function to switch locale in URL
|
||||||
|
const getSwitchedLocalePath = (targetLocale: string) => {
|
||||||
|
if (currentLocale === targetLocale) return pathname
|
||||||
|
if (pathname.startsWith(`/${currentLocale}`)) {
|
||||||
|
return pathname.replace(`/${currentLocale}`, `/${targetLocale}`)
|
||||||
|
}
|
||||||
|
return `/${targetLocale}${pathname}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out duplicated "Kontakt" from menu items since it has a dedicated right CTA button
|
||||||
|
const filteredNavItems = items?.filter(
|
||||||
|
(item) => !item.label.toLowerCase().includes('kontakt')
|
||||||
|
)
|
||||||
|
|
||||||
|
const languages = [
|
||||||
|
{ code: 'pl', label: 'PL - Polski' },
|
||||||
|
{ code: 'en', label: 'UK - English' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const activeLang = languages.find((l) => l.code === currentLocale) || languages[0]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="sticky top-0 z-50 w-full bg-white border-b-2 border-[#1f1f1f] shadow-xs">
|
||||||
|
{/* Top Black Utility Bar */}
|
||||||
|
<div className="bg-[#1f1f1f] text-white text-xs py-2 hidden md:block">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="inline-block w-2.5 h-2.5 bg-[#FFED00]" />
|
||||||
|
<span className="font-bold uppercase tracking-wider text-[11px] text-[#e0e0e0]">
|
||||||
|
{tagline || 'Profesjonalne Mycie i Renowacja Nawierzchni Brukowych'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{phone && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-bold uppercase tracking-wider text-[11px] text-[#e0e0e0]">
|
||||||
|
Infolinia / Bezpłatna wycena:
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href={`tel:${phone.replace(/\s+/g, '')}`}
|
||||||
|
className="font-black uppercase tracking-wider text-[11px] text-[#FFED00] hover:underline font-mono"
|
||||||
|
>
|
||||||
|
{phone}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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">
|
||||||
|
<a href={homeHref} className="flex items-center gap-3.5">
|
||||||
|
<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">
|
||||||
|
POI
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{companyName && (
|
||||||
|
<span className="text-xl font-black uppercase tracking-tight text-[#1f1f1f] block leading-none">
|
||||||
|
{companyName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="text-[10px] uppercase tracking-widest text-[#666666] font-bold block mt-1">
|
||||||
|
Czyszczenie Kostki Brukowej
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* Navigation Items (Without duplicated Kontakt) */}
|
||||||
|
{filteredNavItems && filteredNavItems.length > 0 && (
|
||||||
|
<nav className="hidden lg:flex items-center gap-8 text-xs font-black uppercase tracking-wider text-[#1f1f1f]">
|
||||||
|
{filteredNavItems.map((item, idx) => (
|
||||||
|
<a
|
||||||
|
key={item.id || idx}
|
||||||
|
href={item.href}
|
||||||
|
className="hover:text-[#1f1f1f] hover:border-b-2 hover:border-[#FFED00] transition-all py-2"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{/* Wysuwany przełącznik języków (Dropdown Language Switcher) */}
|
||||||
|
<div className="relative" ref={dropdownRef}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<Globe className="h-3.5 w-3.5 text-[#1f1f1f]" />
|
||||||
|
<span className="uppercase">{activeLang.code.toUpperCase()}</span>
|
||||||
|
<ChevronDown className={`h-3.5 w-3.5 transition-transform duration-200 ${langDropdownOpen ? 'rotate-180' : ''}`} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{langDropdownOpen && (
|
||||||
|
<div className="absolute right-0 mt-1 w-36 bg-white border-2 border-[#1f1f1f] shadow-xl z-50 py-1">
|
||||||
|
{languages.map((lang) => (
|
||||||
|
<a
|
||||||
|
key={lang.code}
|
||||||
|
href={getSwitchedLocalePath(lang.code)}
|
||||||
|
onClick={() => setLangDropdownOpen(false)}
|
||||||
|
className={`flex items-center justify-between px-3 py-2 text-xs font-bold transition-colors ${
|
||||||
|
currentLocale === lang.code
|
||||||
|
? 'bg-[#1f1f1f] text-[#FFED00]'
|
||||||
|
: 'text-[#1f1f1f] hover:bg-[#FFED00]/20'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{lang.label}</span>
|
||||||
|
{currentLocale === lang.code && <span className="w-1.5 h-1.5 bg-[#FFED00]" />}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right CTA Button: KONTAKT */}
|
||||||
|
<a
|
||||||
|
href={contactHref}
|
||||||
|
className="inline-flex items-center gap-2 bg-[#FFED00] hover:bg-[#fae100] text-[#1f1f1f] border-2 border-[#1f1f1f] px-6 py-3 text-xs font-black uppercase tracking-widest transition-colors shadow-xs"
|
||||||
|
>
|
||||||
|
<span>{contactLabel}</span>
|
||||||
|
<ArrowRight className="h-4 w-4" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import type { GlobalConfig } from 'payload'
|
||||||
|
|
||||||
|
export const Company: GlobalConfig = {
|
||||||
|
slug: 'company',
|
||||||
|
label: 'Dane Firmy',
|
||||||
|
access: {
|
||||||
|
read: () => true,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
defaultValue: 'POI CLEAN HOUSE Dariusz Król',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'shortName',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: 'POI Clean House',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nip',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: '7542827484',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'regon',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: '521920102',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'address',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: 'ul. Opolska 7B',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'city',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: '45-960 Opole',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'region',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: 'Opole, woj. opolskie, dolnośląskie i śląskie',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'phone',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: '+48 668 973 737',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'email',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: '[email protected]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'workingHours',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: 'Poniedziałek - Sobota: 7:00 - 20:00',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import type { GlobalConfig } from 'payload'
|
||||||
|
|
||||||
|
export const Navigation: GlobalConfig = {
|
||||||
|
slug: 'navigation',
|
||||||
|
label: 'Nawigacja i Menu',
|
||||||
|
access: {
|
||||||
|
read: () => true,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'tagline',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
defaultValue: 'Czyszczenie Kostki Brukowej',
|
||||||
|
admin: {
|
||||||
|
description: 'Podtytuł pod logo w nagłówku',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'headerItems',
|
||||||
|
type: 'array',
|
||||||
|
localized: true,
|
||||||
|
labels: {
|
||||||
|
singular: 'Pozycja menu głównego',
|
||||||
|
plural: 'Pozycje menu głównego',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'label',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'page',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'pages',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'url',
|
||||||
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
description: 'Użyj jeśli link prowadzi do zewnętrznego URL lub kotwicy #',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ctaButton',
|
||||||
|
type: 'group',
|
||||||
|
localized: true,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'label',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: 'Zadzwoń po wycenę',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'page',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'pages',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'url',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'footerTagline',
|
||||||
|
type: 'textarea',
|
||||||
|
localized: true,
|
||||||
|
defaultValue:
|
||||||
|
'Wyspecjalizowane usługi mycia gorącowodnego, usuwania mchu, odplamiania oleju, spoinowania i impregnacji hydrofobowej nawierzchni brukowych.',
|
||||||
|
admin: {
|
||||||
|
description: 'Krótki opis firmy w lewej kolumnie stopki',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'footerItems',
|
||||||
|
type: 'array',
|
||||||
|
localized: true,
|
||||||
|
labels: {
|
||||||
|
singular: 'Pozycja menu stopki',
|
||||||
|
plural: 'Pozycje menu stopki',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'label',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'page',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'pages',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'url',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { I18nConfig } from '@intecion/ipal-kit'
|
||||||
|
|
||||||
|
export const i18nConfig = {
|
||||||
|
defaultLocale: 'pl',
|
||||||
|
locales: [
|
||||||
|
{ code: 'pl', label: 'PL' },
|
||||||
|
{ code: 'en', label: 'UK' },
|
||||||
|
],
|
||||||
|
} as const satisfies I18nConfig
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { createContentHelpers } from '@intecion/ipal-kit'
|
||||||
|
import payloadConfig from '@/payload.config'
|
||||||
|
import { i18nConfig } from '@/i18n.config'
|
||||||
|
|
||||||
|
export const {
|
||||||
|
getCachedPayload,
|
||||||
|
getSettings,
|
||||||
|
getConfiguredLocales,
|
||||||
|
resolveRoute,
|
||||||
|
getEntries,
|
||||||
|
robots,
|
||||||
|
} = createContentHelpers({
|
||||||
|
config: payloadConfig,
|
||||||
|
content: { collections: [] },
|
||||||
|
i18n: i18nConfig,
|
||||||
|
})
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { cache } from 'react'
|
||||||
|
import { getSiteSettings } from '@intecion/ipal-kit'
|
||||||
|
import { getCachedPayload } from './content'
|
||||||
|
|
||||||
|
export const getSettings = cache(async (locale: string) =>
|
||||||
|
getSiteSettings(await getCachedPayload(), { locale: locale as never, depth: 2 }),
|
||||||
|
)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
import { buildConfig } from 'payload'
|
||||||
|
import { sqliteAdapter } from '@payloadcms/db-sqlite'
|
||||||
|
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||||
|
import { ipalKit, mailAdapter } from '@intecion/ipal-kit'
|
||||||
|
import sharp from 'sharp'
|
||||||
|
|
||||||
|
import { i18nConfig } from './i18n.config'
|
||||||
|
import { Users } from './collections/Users'
|
||||||
|
import { Media } from './collections/Media'
|
||||||
|
import { Pages } from './collections/Pages'
|
||||||
|
import { Company } from './globals/Company'
|
||||||
|
import { Navigation } from './globals/Navigation'
|
||||||
|
|
||||||
|
import { seed } from './seed'
|
||||||
|
|
||||||
|
const filename = fileURLToPath(import.meta.url)
|
||||||
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
|
export default buildConfig({
|
||||||
|
admin: {
|
||||||
|
user: Users.slug,
|
||||||
|
importMap: {
|
||||||
|
baseDir: path.resolve(dirname),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
collections: [Users, Media, Pages],
|
||||||
|
globals: [Company, Navigation],
|
||||||
|
editor: lexicalEditor(),
|
||||||
|
sharp,
|
||||||
|
secret: process.env.PAYLOAD_SECRET || 'fallback-secret-for-development-mode-12345',
|
||||||
|
typescript: {
|
||||||
|
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||||
|
},
|
||||||
|
db: sqliteAdapter({
|
||||||
|
client: {
|
||||||
|
url: process.env.DATABASE_URI || 'file:./dev.db',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
email: mailAdapter(),
|
||||||
|
onInit: async (payload) => {
|
||||||
|
try {
|
||||||
|
await seed(payload)
|
||||||
|
} catch (err) {
|
||||||
|
payload.logger.error(`Błąd podczas seedowania: ${err}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
ipalKit({
|
||||||
|
i18n: i18nConfig,
|
||||||
|
access: { authCollection: 'users' },
|
||||||
|
pages: { slug: 'pages' },
|
||||||
|
seo: { collections: ['pages'] },
|
||||||
|
forms: { redirectRelationships: ['pages'] },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import type { NextRequest } from 'next/server'
|
||||||
|
import { createLocaleMiddleware } from '@intecion/ipal-kit/next/middleware'
|
||||||
|
import { i18nConfig } from '@/i18n.config'
|
||||||
|
|
||||||
|
const localeMiddleware = createLocaleMiddleware({ config: i18nConfig })
|
||||||
|
|
||||||
|
export function proxy(request: NextRequest) {
|
||||||
|
const result = localeMiddleware(request)
|
||||||
|
|
||||||
|
const response =
|
||||||
|
result.type === 'next'
|
||||||
|
? NextResponse.next()
|
||||||
|
: NextResponse.redirect(result.location)
|
||||||
|
|
||||||
|
if (result.cookie) {
|
||||||
|
response.cookies.set(result.cookie.name, result.cookie.value)
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: ['/((?!api|admin|_next|.*\\..*).*)'],
|
||||||
|
}
|
||||||
@@ -0,0 +1,639 @@
|
|||||||
|
import type { Payload } from 'payload'
|
||||||
|
|
||||||
|
export async function seed(payload: Payload) {
|
||||||
|
payload.logger.info('Rozpoczynam seedowanie danych początkowych dla POI Clean House...')
|
||||||
|
|
||||||
|
// 1. Sprawdź czy użytkownik admin już istnieje
|
||||||
|
const existingUsers = await payload.find({
|
||||||
|
collection: 'users',
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (existingUsers.totalDocs === 0) {
|
||||||
|
payload.logger.info('Tworzę konto administratora ([email protected])...')
|
||||||
|
await payload.create({
|
||||||
|
collection: 'users',
|
||||||
|
data: {
|
||||||
|
email: '[email protected]',
|
||||||
|
password: '[email protected]',
|
||||||
|
name: 'Dariusz Król - POI Clean House',
|
||||||
|
} as never,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Aktualizacja globala Company
|
||||||
|
payload.logger.info('Aktualizuję dane firmy...')
|
||||||
|
await payload.updateGlobal({
|
||||||
|
slug: 'company',
|
||||||
|
locale: 'pl',
|
||||||
|
data: {
|
||||||
|
name: 'POI CLEAN HOUSE Dariusz Król',
|
||||||
|
shortName: 'POI Clean House',
|
||||||
|
nip: '7542827484',
|
||||||
|
regon: '521920102',
|
||||||
|
address: 'ul. Opolska 7B',
|
||||||
|
city: '45-960 Opole',
|
||||||
|
region: 'Opole oraz całe woj. opolskie, dolnośląskie i śląskie',
|
||||||
|
phone: '+48 668 973 737',
|
||||||
|
email: '[email protected]',
|
||||||
|
workingHours: 'Poniedziałek - Sobota: 7:00 - 20:00',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. Sprawdź czy strony już istnieją
|
||||||
|
const existingPages = await payload.find({
|
||||||
|
collection: 'pages',
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (existingPages.totalDocs > 0) {
|
||||||
|
payload.logger.info('Strony już istnieją w bazie. Pomijam seedowanie stron.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.logger.info('Tworzę strony serwisu...')
|
||||||
|
|
||||||
|
// A. Strona Główna
|
||||||
|
const homePage = await payload.create({
|
||||||
|
collection: 'pages',
|
||||||
|
locale: 'pl',
|
||||||
|
data: {
|
||||||
|
title: 'Strona Główna',
|
||||||
|
slug: 'home',
|
||||||
|
layout: [
|
||||||
|
{
|
||||||
|
blockType: 'hero',
|
||||||
|
badge: 'Eksperci Mycia i Renowacji Kostki Brukowej',
|
||||||
|
heading: 'Przywracamy fabryczny wygląd Twojej kostki brukowej – bez uszkodzeń i bałaganu.',
|
||||||
|
subheading:
|
||||||
|
'Usuwamy wieloletni mech, plamy z oleju silnikowego, zacieki pobudowlane i zabrudzenia atmosferyczne. Myjemy gorącą wodą do 100°C talerzami bezrozbryzgowymi, uzupełniamy spoiny piaskiem kwarcowym i zabezpieczamy trwałą impregnacją hydrofobową.',
|
||||||
|
trustBadges: [
|
||||||
|
{ text: 'Mycie gorącą wodą do 100°C pod ciśnieniem' },
|
||||||
|
{ text: 'Talerze bezrozbryzgowe Mosmatic – czyste elewacje' },
|
||||||
|
{ text: 'Biodegradowalne i bezpieczne środki biobójcze' },
|
||||||
|
{ text: 'Darmowa wycena na posesji lub ze zdjęcia' },
|
||||||
|
],
|
||||||
|
ctaPhone: '+48 668 973 737',
|
||||||
|
ctaPhoneLabel: 'Zadzwoń po bezpłatną wycenę',
|
||||||
|
ctaSecondaryLabel: 'Zobacz cennik za m²',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'problemSolution',
|
||||||
|
badge: 'Dlaczego mycie profesjonalne?',
|
||||||
|
heading: 'Dlaczego domowa myjka nie poradzi sobie z trudnymi zabrudzeniami?',
|
||||||
|
subheading:
|
||||||
|
'Zwykłe mycie zimną wodą pod przypadkowym ciśnieniem najczęściej kończy się wypłukaniem piasku, zniszczeniem wierzchniej warstwy betonu i powrotem mchu po kilku tygodniach.',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
problemTitle: 'Wypłukane spoiny i błyskawiczny powrót mchu',
|
||||||
|
problemDescription:
|
||||||
|
'Zimna woda jedynie urywa zieloną część rośliny. Zarodniki mchu i grzybów wnikają w głąb szczelin i odrastają ze zdwojoną siłą.',
|
||||||
|
solutionTitle: 'Chemia biobójcza + Piaskowanie kwarcowe',
|
||||||
|
solutionDescription:
|
||||||
|
'Stosujemy preparaty niszczące korzenie i zarodniki na poziomie biologicznym, a po myciu maszynowo zamiatamy suszony piasek kwarcowy.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
problemTitle: 'Plamy z oleju silnikowego i tłuszczu z grilla',
|
||||||
|
problemDescription:
|
||||||
|
'Olej wnika w mikroskopijne pory kostki. Mycie bez wysokiej temperatury jedynie rozciera tłuszcz po większej powierzchni.',
|
||||||
|
solutionTitle: 'Woda 90-100°C + Odtłuszczacze przemysłowe',
|
||||||
|
solutionDescription:
|
||||||
|
'Wysoka temperatura emulguje tłuszcze, a przemysłowa chemia rozbija wiązania węglowodorowe, wyciągając olej z głębi kostki.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
problemTitle: 'Zmatowienie i podatność na pękanie zimą',
|
||||||
|
problemDescription:
|
||||||
|
'Otwarta po myciu struktura kostki chłonie wilgoć. Woda zamarzając w mikro-szczelinach kruszy beton od środka.',
|
||||||
|
solutionTitle: 'Impregnacja hydrofobowa "Mokra Kostka"',
|
||||||
|
solutionDescription:
|
||||||
|
'Tworzymy barierę odpychającą wodę, sól drogową i promieniowanie UV, uwydatniając głęboki, nasycony kolor kostki.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'processSteps',
|
||||||
|
badge: 'Proces realizacji',
|
||||||
|
heading: 'Nasz 5-etapowy standard renowacji nawierzchni',
|
||||||
|
subheading:
|
||||||
|
'Działamy metodycznie, dbając o czystość wokół Twojej posesji – od przygotowania po finalną impregnację.',
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
stepNumber: '01',
|
||||||
|
title: 'Inspekcja i przygotowanie terenu',
|
||||||
|
description:
|
||||||
|
'Oceniamy rodzaj kostki (betonowa, granitowa, klinkier), stopień degradacji i zabezpieczamy rośliny ogrodowe oraz elementy małej architektury.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stepNumber: '02',
|
||||||
|
title: 'Aplikacja chemii aktywnej',
|
||||||
|
description:
|
||||||
|
'Nanosimy bezpieczne dla środowiska preparaty biobójcze i odtłuszczające, dając im czas na rozpuszczenie nalotów organicznych i tłuszczowych.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stepNumber: '03',
|
||||||
|
title: 'Mycie gorącowodne talerzami Mosmatic',
|
||||||
|
description:
|
||||||
|
'Czyścimy nawierzchnię strumieniem gorącej wody pod kontrolowanym ciśnieniem. Osłona talerza zapobiega chlapaniu na elewację i ogrodzenie.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stepNumber: '04',
|
||||||
|
title: 'Doczyszczanie krawędzi i spłukiwanie',
|
||||||
|
description:
|
||||||
|
'Precyzyjnie domywamy palisady, krawężniki, schody i obrzeża, spłukując cały urobek do odpływów kanalizacyjnych.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stepNumber: '05',
|
||||||
|
title: 'Fugowanie i Impregnacja',
|
||||||
|
description:
|
||||||
|
'Po wyschnięciu uzupełniamy fugi czystym piaskiem kwarcowym oraz aplikujemy impregnat hydrofobowy zabezpieczający podjazd na sezony.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'pricingTable',
|
||||||
|
badge: 'Przejrzyste zasady',
|
||||||
|
heading: 'Cennik mycia i renowacji kostki brukowej',
|
||||||
|
subheading:
|
||||||
|
'Wybierz pakiet dopasowany do stanu Twojego podjazdu lub skontaktuj się po bezpłatną wycenę indywidualną.',
|
||||||
|
disclaimer: '*Ceny netto za m². Ostateczna cena zależy od całkowitego metrażu oraz stopnia i rodzaju zabrudzeń.',
|
||||||
|
packages: [
|
||||||
|
{
|
||||||
|
name: 'Pakiet Podstawowy',
|
||||||
|
price: 'od 5 zł / m²',
|
||||||
|
description: 'Idealny dla posesji mytych regularnie, bez trudnych plam olejowych.',
|
||||||
|
isPopular: false,
|
||||||
|
ctaLabel: 'Wybierz pakiet podstawowy',
|
||||||
|
features: [
|
||||||
|
{ feature: 'Mycie hydrodynamiczne talerzem bezrozbryzgowym' },
|
||||||
|
{ feature: 'Usunięcie brudu atmosferycznego, kurzu i liści' },
|
||||||
|
{ feature: 'Zmycie świeżych nalotów mchu' },
|
||||||
|
{ feature: 'Spłukanie i uprzątnięcie posesji po pracy' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Kompleksowa Renowacja',
|
||||||
|
price: 'od 9 zł / m²',
|
||||||
|
description: 'Najbardziej opłacalny i kompleksowy wariant przywracający idealny stan.',
|
||||||
|
isPopular: true,
|
||||||
|
ctaLabel: 'Zamów kompleksową renowację',
|
||||||
|
features: [
|
||||||
|
{ feature: 'Wszystko z Pakietu Podstawowego' },
|
||||||
|
{ feature: 'Mycie gorącą wodą do 100°C' },
|
||||||
|
{ feature: 'Aplikacja biobójczego preparatu przeciw zarodnikom' },
|
||||||
|
{ feature: 'Punktowe usuwanie plam z oleju silnikowego' },
|
||||||
|
{ feature: 'Uzupełnienie fug piaskiem kwarcowym (z materiałem)' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Maksymalna Ochrona',
|
||||||
|
price: 'od 16 zł / m²',
|
||||||
|
description: 'Pełna renowacja z podwójną warstwą profesjonalnego impregnatu.',
|
||||||
|
isPopular: false,
|
||||||
|
ctaLabel: 'Wybierz pakiet z impregnacją',
|
||||||
|
features: [
|
||||||
|
{ feature: 'Wszystko z Kompleksowej Renowacji' },
|
||||||
|
{ feature: 'Głęboka impregnacja hydrofobowa lub "Mokra Kostka"' },
|
||||||
|
{ feature: 'Ochrona przed plamami oleju, soli i promieniowaniem UV' },
|
||||||
|
{ feature: 'Gwarancja odporności na zarastanie mchem' },
|
||||||
|
{ feature: 'Trwały efekt odświeżonego koloru na 3-5 lat' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'techEquipment',
|
||||||
|
badge: 'Park Maszynowy',
|
||||||
|
heading: 'Sprzęt przemysłowy = Bezpieczeństwo i skuteczność',
|
||||||
|
subheading:
|
||||||
|
'Dysponujemy zaawansowaną technologią hydrodynamiczną, która gwarantuje najwyższą kulturę pracy.',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: 'Spalinowe myjki gorącowodne z regulacją ciśnienia',
|
||||||
|
description:
|
||||||
|
'Niezależne od zasilania elektrycznego agregaty grzewcze podgrzewające wodę do temperatury pary, co pozwala rozpuszczać tłuszcze bez niszczenia spoiwa cementowego.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Talerze powierzchniowe Mosmatic Surface Cleaner',
|
||||||
|
description:
|
||||||
|
'Podwójne ramiona rotacyjne pracujące w hermetycznej osłonie ze szczotkami uniemożliwiają wydostawanie się brudnej wody na ściany budynków i okna.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Atestowany piasek kwarcowy i chemia biodegradowalna',
|
||||||
|
description:
|
||||||
|
'Stosujemy wyłącznie suszony piecowo piasek kwarcowy o frakcji 0.2-0.8mm oraz ekologiczne preparaty bezpieczne dla trawników i zwierząt.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'faqAccordion',
|
||||||
|
badge: 'Pytania i odpowiedzi',
|
||||||
|
heading: 'Wszystko, co warto wiedzieć przed zamówieniem usługi',
|
||||||
|
subheading: 'Odpowiedzi na najczęstsze pytania naszych klientów.',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
question: 'Czy mycie ciśnieniowe nie uszkodzi powierzchni kostki brukowej?',
|
||||||
|
answer:
|
||||||
|
'Nie, ponieważ nie używamy agresywnych dysz punktowych z bliskiej odległości. Zastosowanie talerzy myjących Mosmatic z regulacją ciśnienia i gorącą wodą zapewnia równomierne, bezpieczne zmywanie brudu bez wymywania wierzchniej warstwy betonu.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Czy po czyszczeniu konieczne jest ponowne piaskowanie fug?',
|
||||||
|
answer:
|
||||||
|
'Tak, w większości przypadków wysokie ciśnienie usuwa ze szczelin nie tylko mech i chwasty, ale również stary, zabrudzony piasek. Uzupełnienie spoin czystym piaskiem kwarcowym stabilizuje kostkę i zapobiega jej przesuwaniu się oraz zapadaniu.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Co daje impregnacja kostki brukowej?',
|
||||||
|
answer:
|
||||||
|
'Impregnat wnika w strukturę kapilarną betonu, zamykając pory. Dzięki temu woda spływa po powierzchni (efekt perlenia), plamy z oleju nie wsiąkają w podłoże, a kostka jest zabezpieczona przed mrozem i promieniowaniem UV. Opcjonalny efekt "mokrej kostki" dodatkowo wyciąga głębię koloru.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Czy potrzebny jest dostęp do wody i prądu na posesji?',
|
||||||
|
answer:
|
||||||
|
'Zapewnienie dostępu do zwykłego kranu ogrodowego pozwala na najbardziej efektywną realizację. Posiadamy myjki ze spalinowymi silnikami, więc nie potrzebujemy dostępu do prądu.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'reviews',
|
||||||
|
badge: 'Opinie Klientów',
|
||||||
|
heading: 'Co mówią właściciele wyczyszczonych posesji?',
|
||||||
|
subheading: 'Zaufali nam właściciele domów jednorodzinnych, osiedla mieszkaniowe i firmy.',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
author: 'Piotr Wiśniewski',
|
||||||
|
location: 'Wrocław, Jagodno – Podjazd 220 m²',
|
||||||
|
rating: 5,
|
||||||
|
content:
|
||||||
|
'Kostka miała 12 lat i była cała w czarnym nalocie i mchu. Po wizycie ekipy POI Clean House wygląda dokładnie jak w dniu układania. Elewacja i ogrodzenie pozostały idealnie czyste!',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
author: 'Katarzyna Nowak',
|
||||||
|
location: 'Długołęka – Taras i podjazd 160 m²',
|
||||||
|
rating: 5,
|
||||||
|
content:
|
||||||
|
'Impregnacja z efektem mokrej kostki przerosła moje oczekiwania. Kolor jest żywy, a plamy po oleju z kosiarki zniknęły bez śladu. Bardzo polecam!',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
author: 'Tomasz Grabowski',
|
||||||
|
location: 'Trzebnica – Posesja firmowa 450 m²',
|
||||||
|
rating: 5,
|
||||||
|
content:
|
||||||
|
'Szybka, profesjonalna realizacja w uzgodnionym terminie. Wypłukany piasek został w pełni uzupełniony, a parking lśni. Pełen profesjonalizm.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'formBlock',
|
||||||
|
badge: 'Kontakt i Wycena',
|
||||||
|
heading: 'Skontaktuj się i otrzymaj bezpłatną wycenę',
|
||||||
|
subheading:
|
||||||
|
'Zadzwoń do nas lub wypełnij formularz – odezwiemy się w ciągu maksymalnie 2 godzin ze szczegółową kalkulacją kosztów.',
|
||||||
|
phone: '+48 668 973 737',
|
||||||
|
email: '[email protected]',
|
||||||
|
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.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// C. Strona Realizacje
|
||||||
|
const realizacjePage = await payload.create({
|
||||||
|
collection: 'pages',
|
||||||
|
locale: 'pl',
|
||||||
|
data: {
|
||||||
|
title: 'Realizacje',
|
||||||
|
slug: 'realizacje',
|
||||||
|
layout: [
|
||||||
|
{
|
||||||
|
blockType: 'beforeAfterGallery',
|
||||||
|
badge: 'Efekty Prac',
|
||||||
|
heading: 'Galeria naszych realizacji Przed i Po',
|
||||||
|
subheading: 'Zobacz, jak nawet mocno zaniedbana i zarośnięta kostka brukowa odzyskuje fabryczny blask.',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: 'Czyszczenie zarośniętego podjazdu z kostki betonowej (180 m²)',
|
||||||
|
category: 'Usuwanie mchu i porostów',
|
||||||
|
description:
|
||||||
|
'Wieloletni mech i porosty w szczelinach. Zastosowano mycie gorącowodne talerzem Mosmatic, chemię biobójczą oraz zasypanie suszonym piaskiem kwarcowym.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Usuwanie plam z oleju silnikowego i tłuszczu (stanowisko garażowe)',
|
||||||
|
category: 'Odplamianie chemiczne',
|
||||||
|
description:
|
||||||
|
'Głębokie, 3-letnie plamy z oleju. Zastosowano odtłuszczacz penetrujący kapilary betonu oraz mycie parowe o temperaturze 100°C.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'formBlock',
|
||||||
|
badge: 'Wycena',
|
||||||
|
heading: 'Chcesz, aby Twój podjazd wyglądał jak nowy?',
|
||||||
|
subheading: 'Skontaktuj się z nami już dziś i umów dogodny termin realizacji.',
|
||||||
|
phone: '+48 668 973 737',
|
||||||
|
email: '[email protected]',
|
||||||
|
workingHours: 'Pon - Sob: 7:00 - 20:00',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// D. Strona O nas / Technologia
|
||||||
|
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({
|
||||||
|
collection: 'pages',
|
||||||
|
locale: 'pl',
|
||||||
|
data: {
|
||||||
|
title: 'Kontakt',
|
||||||
|
slug: 'kontakt',
|
||||||
|
layout: [
|
||||||
|
{
|
||||||
|
blockType: 'formBlock',
|
||||||
|
badge: 'Kontakt',
|
||||||
|
heading: 'Skontaktuj się z POI Clean House',
|
||||||
|
subheading:
|
||||||
|
'Działamy na terenie Opola oraz całego województwa opolskiego, dolnośląskiego i śląskiego. Wycena jest zawsze bezpłatna i niezobowiązująca.',
|
||||||
|
phone: '+48 668 973 737',
|
||||||
|
email: '[email protected]',
|
||||||
|
workingHours: 'Poniedziałek - Sobota: 7:00 - 20:00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'faqAccordion',
|
||||||
|
badge: 'FAQ',
|
||||||
|
heading: 'Często zadawane pytania',
|
||||||
|
subheading: 'Wszystko co warto wiedzieć przed przyjazdem ekipy.',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
question: 'Jak szybko możecie zrealizować zlecenie?',
|
||||||
|
answer: 'W sezonie wiosenno-letnim staramy się realizować zlecenia w ciągu 3-7 dni roboczych od akceptacji wyceny.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Czy muszę być obecny podczas prac?',
|
||||||
|
answer: 'Wystarczy udostępnić źródło wody na posesji. Po zakończeniu prac kontaktujemy się z Państwem w celu odbioru.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// F. Strona Polityka Prywatności (System Page: privacyPolicy)
|
||||||
|
const privacyPage = await payload.create({
|
||||||
|
collection: 'pages',
|
||||||
|
locale: 'pl',
|
||||||
|
data: {
|
||||||
|
title: 'Polityka Prywatności',
|
||||||
|
slug: 'polityka-prywatnosci',
|
||||||
|
layout: [
|
||||||
|
{
|
||||||
|
blockType: 'content',
|
||||||
|
badge: 'RODO / GDPR',
|
||||||
|
heading: 'Polityka Prywatności i Ochrony Danych Osobowych',
|
||||||
|
body: `1. Administrator Danych Osobowych
|
||||||
|
Administratorem Państwa danych osobowych jest firma POI CLEAN HOUSE Dariusz Król z siedzibą w Opolu, ul. Opolska 7B, 45-960 Opole, NIP: 7542827484, REGON: 521920102, e-mail: it@intecion.com, tel: +48 668 973 737.
|
||||||
|
|
||||||
|
2. Cel przetwarzania danych
|
||||||
|
Dane osobowe podane w formularzu kontaktowym (imię, numer telefonu, miejscowość, metraż) przetwarzane są wyłącznie w celu przygotowania wyceny, kontaktu zwrotnego oraz realizacji zamówionej usługi czyszczenia nawierzchni brukowej (art. 6 ust. 1 lit. b RODO).
|
||||||
|
|
||||||
|
3. Prawa osoby, której dane dotyczą
|
||||||
|
Każda osoba ma prawo dostępu do swoich danych, ich sprostowania, usunięcia, ograniczenia przetwarzania oraz wniesienia sprzeciwu wobec przetwarzania danych.
|
||||||
|
|
||||||
|
4. Okres przechowywania danych
|
||||||
|
Dane przetwarzane na podstawie zgody będą przechowywane do czasu jej wycofania lub zakończenia realizacji umowy i upływu okresu przedawnienia ewentualnych roszczeń.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// G. Strona Polityka Cookies (System Page: cookiePolicy)
|
||||||
|
const cookiePage = await payload.create({
|
||||||
|
collection: 'pages',
|
||||||
|
locale: 'pl',
|
||||||
|
data: {
|
||||||
|
title: 'Polityka Cookies',
|
||||||
|
slug: 'polityka-cookies',
|
||||||
|
layout: [
|
||||||
|
{
|
||||||
|
blockType: 'content',
|
||||||
|
badge: 'Pliki Cookies',
|
||||||
|
heading: 'Polityka Plików Cookies',
|
||||||
|
body: `1. Czym są pliki cookies?
|
||||||
|
Pliki cookies (ciasteczka) to małe pliki tekstowe zapisywane na urządzeniu użytkownika podczas przeglądania serwisu internetowego.
|
||||||
|
|
||||||
|
2. Rodzaje wykorzystywanych cookies:
|
||||||
|
- Niezbędne: konieczne do prawidłowego działania strony, zapamiętania preferencji zgody oraz bezpieczeństwa sesji.
|
||||||
|
- Funkcjonalne: umożliwiają zapamiętanie wybranych ustawień (np. język serwisu).
|
||||||
|
- Analityczne: pomagają nam zrozumieć, jak użytkownicy korzystają ze strony, co pozwala na jej ciągłe ulepszanie.
|
||||||
|
|
||||||
|
3. Zarządzanie zgodą
|
||||||
|
W każdej chwili możesz zmienić swoje preferencje dotyczące plików cookies klikając w przycisk "Ustawienia cookies" w lewym dolnym rogu strony.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// H. Strona Blog / Poradnik
|
||||||
|
const blogPage = await payload.create({
|
||||||
|
collection: 'pages',
|
||||||
|
locale: 'pl',
|
||||||
|
data: {
|
||||||
|
title: 'Blog i Porady',
|
||||||
|
slug: 'blog',
|
||||||
|
layout: [
|
||||||
|
{
|
||||||
|
blockType: 'blogList',
|
||||||
|
badge: 'Baza Wiedzy i Poradnik',
|
||||||
|
heading: 'Eksperckie porady dotyczące pielęgnacji i czyszczenia nawierzchni brukowych',
|
||||||
|
subheading:
|
||||||
|
'Dowiedz się, jak prawidłowo dbać o podjazd, dlaczego temperatura wody ma kluczowe znaczenie przy odtłuszczaniu i jak zabezpieczyć kostkę przed zarastaniem.',
|
||||||
|
articles: [
|
||||||
|
{
|
||||||
|
title: 'Jak usunąć plamy z oleju silnikowego z kostki brukowej? Porównanie metod',
|
||||||
|
category: 'Odplamianie Chemiczne',
|
||||||
|
readTime: '5 min czytania',
|
||||||
|
date: '12 Lutego 2026',
|
||||||
|
excerpt:
|
||||||
|
'Olej silnikowy błyskawicznie wnika w pory betonu. Zwykłe detergenty domowe rozcierają plamę, powiększając jej powierzchnię. Sprawdź, jak przemysłowa woda 100°C i zasadowe odtłuszczacze rozbijają cząsteczki węglowodorów w kapilarach.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Dlaczego mech odrasta po 3 tygodniach od mycia myjką ciśnieniową?',
|
||||||
|
category: 'Usuwanie Mchu i Biobójczość',
|
||||||
|
readTime: '4 min czytania',
|
||||||
|
date: '28 Stycznia 2026',
|
||||||
|
excerpt:
|
||||||
|
'Zimna woda pod ciśnieniem urywa jedynie nadziemną część mchu. Zarodniki i grzybnia pozostają nienaruszone w szczelinach. Rozwiązaniem jest dwuetapowa procedura: oprysk biobójczy i zasypanie spoin suchym piaskiem kwarcowym.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Impregnacja hydrofobowa kostki brukowej – czy warto inwestować?',
|
||||||
|
category: 'Impregnacja i Ochrona',
|
||||||
|
readTime: '6 min czytania',
|
||||||
|
date: '15 Stycznia 2026',
|
||||||
|
excerpt:
|
||||||
|
'Woda zamarzająca zimą w mikroszczelinach betonu powoduje jego pękanie i łuszczenie. Impregnaty siloksanowe tworzą barierę odpychającą ciecze oraz wydobywają głęboki, nasycony kolor kostki.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'formBlock',
|
||||||
|
badge: 'Wycena',
|
||||||
|
heading: 'Potrzebujesz profesjonalnego czyszczenia?',
|
||||||
|
subheading: 'Skontaktuj się z naszym zespołem i otrzymaj darmową wycenę w ciągu 2 godzin.',
|
||||||
|
phone: '+48 668 973 737',
|
||||||
|
email: '[email protected]',
|
||||||
|
workingHours: 'Pon - Sob: 7:00 - 20:00',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. Przypisanie stron systemowych do globala site-settings
|
||||||
|
payload.logger.info('Konfiguruję strony systemowe w SiteSettings...')
|
||||||
|
try {
|
||||||
|
await payload.updateGlobal({
|
||||||
|
slug: 'site-settings',
|
||||||
|
locale: 'pl',
|
||||||
|
data: {
|
||||||
|
siteName: 'POI Clean House',
|
||||||
|
homepage: homePage.id,
|
||||||
|
privacyPolicy: privacyPage.id,
|
||||||
|
cookiePolicy: cookiePage.id,
|
||||||
|
} as never,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
payload.logger.warn(`Nie udało się automatycznie zaktualizować siteSettings: ${err}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Przypisanie menu do globala navigation
|
||||||
|
payload.logger.info('Konfiguruję menu w Navigation...')
|
||||||
|
try {
|
||||||
|
await payload.updateGlobal({
|
||||||
|
slug: 'navigation',
|
||||||
|
locale: 'pl',
|
||||||
|
data: {
|
||||||
|
tagline: 'Czyszczenie Kostki Brukowej',
|
||||||
|
headerItems: [
|
||||||
|
{
|
||||||
|
label: 'Cennik',
|
||||||
|
url: '#cennik',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Realizacje',
|
||||||
|
page: realizacjePage.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Technologia',
|
||||||
|
page: oNasPage.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Blog',
|
||||||
|
page: blogPage.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ctaButton: {
|
||||||
|
label: 'Kontakt',
|
||||||
|
page: kontaktPage.id,
|
||||||
|
},
|
||||||
|
footerTagline:
|
||||||
|
'Wyspecjalizowane usługi mycia gorącowodnego, usuwania mchu, odplamiania oleju, spoinowania i impregnacji hydrofobowej nawierzchni brukowych.',
|
||||||
|
footerItems: [
|
||||||
|
{
|
||||||
|
label: 'Strona główna',
|
||||||
|
page: homePage.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cennik usług',
|
||||||
|
url: '#cennik',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Galeria realizacji',
|
||||||
|
page: realizacjePage.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Technologia i sprzęt',
|
||||||
|
page: oNasPage.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Poradnik i Blog',
|
||||||
|
page: blogPage.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Kontakt i wycena',
|
||||||
|
page: kontaktPage.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as never,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
payload.logger.warn(`Nie udało się zaktualizować navigation: ${err}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.logger.info('Seedowanie zakończone pomyślnie!')
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user