fix: resolve contact form placeholders, restore process block layout, and fix testimonials carousel scroll logic

This commit is contained in:
MichalC
2026-09-17 22:25:25 +02:00
parent 7cc68b9f36
commit ab4c5c83dd
91 changed files with 4790 additions and 319 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ import configPromise from '@payload-config'
import type { Config, Page } from '../../../payload-types'
import { getGlobal } from '../../../lib/content'
import '../../globals.css'
import { ConsentProvider, CookieBanner, CookieButton } from '@intecion/ipal-kit/client'
import { ConsentProvider, CookieBanner, CookieButton } from '../../../components/Consent'
import { FloatingWhatsApp } from '../../../components/FloatingWhatsApp'
import { getConsentTexts } from '@intecion/ipal-kit'
import { i18n } from '../../../i18n.config'
+1 -1
View File
@@ -25,7 +25,7 @@ export const ContactInfoBlock: React.FC<Props> = ({
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
className="w-full max-w-4xl bg-card border border-border p-8 md:p-16 relative overflow-hidden"
className="w-full max-w-4xl mx-auto bg-card border border-border p-8 md:p-16 relative overflow-hidden"
style={{
boxShadow: '0 10px 40px -10px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.03)'
}}
+127
View File
@@ -0,0 +1,127 @@
'use client'
import React from 'react'
import { usePathname } from 'next/navigation'
import { MapPin, Phone, Mail, Clock } from 'lucide-react'
import { motion } from 'framer-motion'
export const ContactSectionClient: React.FC<any> = ({
address,
phone,
email,
hours,
formNode,
}) => {
const pathname = usePathname()
const isDe = pathname?.split('/').filter(Boolean)[0] === 'de'
return (
<section className="py-16 md:py-24 bg-background">
<div className="container-premium">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16">
{/* Lewa kolumna: Informacje kontaktowe */}
<motion.div
initial={{ opacity: 0, x: -16 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
className="flex flex-col"
>
<h2 className="font-heading text-3xl md:text-4xl font-extrabold text-foreground mb-8 text-balance">
{isDe ? 'Kontaktinformationen' : 'Dane kontaktowe'}
</h2>
<div className="bg-card border border-border p-8 relative overflow-hidden flex-grow"
style={{ boxShadow: '0 10px 40px -10px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.03)' }}
>
<div className="absolute top-0 right-0 w-12 h-12 bg-background border-l border-b border-border shadow-sm transform translate-x-6 -translate-y-6 rotate-45 pointer-events-none" />
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-1 gap-10">
{address && (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
<MapPin size={24} strokeWidth={2.5} />
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
{isDe ? 'Adresse' : 'Adres'}
</h3>
</div>
<p className="text-foreground/80 leading-relaxed font-medium whitespace-pre-line text-base">
{address}
</p>
</div>
)}
{phone && (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
<Phone size={24} strokeWidth={2.5} />
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
{isDe ? 'Telefon' : 'Telefon'}
</h3>
</div>
<a
href={`tel:${phone.replace(/\s+/g, '')}`}
className="text-foreground/80 leading-relaxed font-medium text-base hover:text-accent transition-colors"
>
{phone}
</a>
</div>
)}
{email && (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
<Mail size={24} strokeWidth={2.5} />
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
{isDe ? 'E-Mail' : 'E-mail'}
</h3>
</div>
<a
href={`mailto:${email}`}
className="text-foreground/80 leading-relaxed font-medium text-base hover:text-accent transition-colors"
>
{email}
</a>
</div>
)}
{hours && (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
<Clock size={24} strokeWidth={2.5} />
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
{isDe ? 'Öffnungszeiten' : 'Godziny pracy'}
</h3>
</div>
<p className="text-foreground/80 leading-relaxed font-medium whitespace-pre-line text-base">
{hours}
</p>
</div>
)}
</div>
</div>
</motion.div>
{/* Prawa kolumna: Formularz kontaktowy */}
<motion.div
initial={{ opacity: 0, x: 16 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1], delay: 0.1 }}
className="flex flex-col"
>
<h2 className="font-heading text-3xl md:text-4xl font-extrabold text-foreground mb-8 text-balance">
{isDe ? 'Schreiben Sie uns' : 'Napisz do nas'}
</h2>
<div className="bg-card border border-border p-8 relative overflow-hidden flex-grow"
style={{ boxShadow: '0 10px 40px -10px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.03)' }}
>
{formNode}
</div>
</motion.div>
</div>
</div>
</section>
)
}
+23
View File
@@ -0,0 +1,23 @@
import React from 'react'
import type { Page } from '../../payload-types'
import { ContactSectionClient } from './Client'
import { FormRenderer } from '../../components/FormRenderer'
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'contactSection' }>
export const ContactSectionBlock: React.FC<Props> = (props) => {
const { form } = props
return (
<ContactSectionClient
{...props}
formNode={
typeof form === 'object' && form !== null ? (
<FormRenderer form={form} />
) : (
<p className="text-foreground/60">Formularz nie został podpięty.</p>
)
}
/>
)
}
+43
View File
@@ -0,0 +1,43 @@
import type { Block } from 'payload'
export const ContactSection: Block = {
slug: 'contactSection',
fields: [
{
name: 'address',
type: 'textarea',
localized: true,
admin: {
description: 'Użyj Enter by przejść do nowej linii',
},
},
{
name: 'phone',
type: 'text',
localized: true,
},
{
name: 'email',
type: 'text',
localized: true,
},
{
name: 'hours',
type: 'textarea',
localized: true,
admin: {
description: 'Użyj Enter by przejść do nowej linii',
},
},
{
name: 'form',
type: 'relationship',
relationTo: 'forms',
required: true,
localized: true,
admin: {
description: 'Wybierz formularz kontaktowy. Dzięki opcji "localized" możesz wybrać inny formularz dla języka polskiego i inny dla niemieckiego.',
}
}
],
}
+2
View File
@@ -15,6 +15,7 @@ import { FinalCTABlock } from './FinalCTA/Component'
import { PageHeaderBlock } from './PageHeader/Component'
import { ContactInfoBlock } from './ContactInfo/Component'
import { ContactSectionBlock } from './ContactSection/Component'
import { BioBlock } from './Bio/Component'
import { TeamBlock } from './Team/Component'
import { ServicesDetailedBlock } from './ServicesDetailed/Component'
@@ -25,6 +26,7 @@ import { StatsBlock } from './Stats/Component'
const blockComponents = {
pageHeader: PageHeaderBlock,
contactInfo: ContactInfoBlock,
contactSection: ContactSectionBlock,
bio: BioBlock,
team: TeamBlock,
servicesDetailed: ServicesDetailedBlock,
@@ -64,6 +64,7 @@ export const ServicesDetailedBlock: React.FC<Props> = ({
return (
<motion.div
key={service.id || index}
id={service.anchor}
variants={{
hidden: { opacity: 0, y: isFirst ? 24 : 16 },
visible: {
+2
View File
@@ -1,4 +1,5 @@
import type { Block } from 'payload'
import { buildSlugField } from '@intecion/ipal-kit'
export const ServicesDetailed: Block = {
slug: 'servicesDetailed',
@@ -12,6 +13,7 @@ export const ServicesDetailed: Block = {
required: true,
fields: [
{ name: 'title', type: 'text', required: true },
buildSlugField({ name: 'anchor', from: 'title', required: true, localized: false }),
{ name: 'description', type: 'textarea', required: true },
{
name: 'icon',
+4 -2
View File
@@ -7,13 +7,15 @@ export const Team: Block = {
name: 'title',
type: 'text',
localized: true,
defaultValue: 'Nasz zespół',
defaultValue: ({ locale }: { locale: string }) => locale === 'de' ? 'Unser Team' : 'Nasz zespół',
},
{
name: 'subtitle',
type: 'textarea',
localized: true,
defaultValue: 'Poznaj ekspertów, którzy z zaangażowaniem dbają o Twoje finanse. Każdy z nas wnosi wieloletnie doświadczenie, by zagwarantować Ci spokój i bezpieczeństwo.',
defaultValue: ({ locale }: { locale: string }) => locale === 'de'
? 'Lernen Sie die Experten kennen, die sich mit Engagement um Ihre Finanzen kümmern. Jeder von uns bringt langjährige Erfahrung mit, um Ihnen Sicherheit und Ruhe zu garantieren.'
: 'Poznaj ekspertów, którzy z zaangażowaniem dbają o Twoje finanse. Każdy z nas wnosi wieloletnie doświadczenie, by zagwarantować Ci spokój i bezpieczeństwo.',
},
{
name: 'members',
+36 -17
View File
@@ -32,21 +32,38 @@ export const TestimonialsCarousel: React.FC<CarouselProps> = ({ title, items })
if (!containerRef.current) return
const { scrollLeft, scrollWidth, clientWidth } = containerRef.current
// Obliczamy który element jest najbardziej widoczny
// Szerokość jednego elementu (przy założeniu że wszystkie są równe i wypełniają szerokość kontenera na mobile,
// lub połowę na desktop)
const cardWidth = containerRef.current.firstElementChild?.clientWidth || clientWidth
const newIndex = Math.round(scrollLeft / cardWidth)
if (newIndex !== activeIndex) {
setActiveIndex(newIndex)
// Jeśli przewinęliśmy do samego końca (uwzględniamy drobną tolerancję na różnice pikselowe w przeglądarkach)
if (Math.abs(scrollWidth - clientWidth - scrollLeft) < 5) {
if (activeIndex !== items.length - 1) {
setActiveIndex(items.length - 1)
}
return
}
}, [activeIndex])
// Znajdź kartę, która jest najbliżej lewej krawędzi (scrollLeft)
const children = Array.from(containerRef.current.children) as HTMLElement[]
let closestIndex = 0
let minDiff = Infinity
children.forEach((child, index) => {
const diff = Math.abs(child.offsetLeft - scrollLeft)
if (diff < minDiff) {
minDiff = diff
closestIndex = index
}
})
if (closestIndex !== activeIndex) {
setActiveIndex(closestIndex)
}
}, [activeIndex, items.length])
useEffect(() => {
const el = containerRef.current
if (el) {
el.addEventListener('scroll', handleScroll, { passive: true })
// Wywołaj raz na start, żeby upewnić się że stan jest zsynchronizowany
handleScroll()
return () => el.removeEventListener('scroll', handleScroll)
}
}, [handleScroll])
@@ -54,14 +71,16 @@ export const TestimonialsCarousel: React.FC<CarouselProps> = ({ title, items })
const scrollTo = (index: number) => {
if (!containerRef.current) return
const cardWidth = containerRef.current.firstElementChild?.clientWidth || containerRef.current.clientWidth
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
containerRef.current.scrollTo({
left: index * cardWidth,
behavior: prefersReducedMotion ? 'auto' : 'smooth'
})
const children = containerRef.current.children
if (index >= 0 && index < children.length) {
const child = children[index] as HTMLElement
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
containerRef.current.scrollTo({
left: child.offsetLeft,
behavior: prefersReducedMotion ? 'auto' : 'smooth'
})
}
}
const scrollPrev = () => {
+2
View File
@@ -13,6 +13,7 @@ import { FinalCTA } from '../blocks/FinalCTA/config'
import { PageHeader } from '../blocks/PageHeader/config'
import { ContactInfo } from '../blocks/ContactInfo/config'
import { ContactSection } from '../blocks/ContactSection/config'
import { TextSection } from '../blocks/TextSection/config'
import { ServicesDetailed } from '../blocks/ServicesDetailed/config'
import { Eligibility } from '../blocks/Eligibility/config'
@@ -45,6 +46,7 @@ export const Pages: CollectionConfig = {
blocks: [
PageHeader,
ContactInfo,
ContactSection,
Bio,
Team,
ServicesDetailed,
+2
View File
@@ -0,0 +1,2 @@
'use client'
export { ConsentProvider, CookieBanner, CookieButton, useConsentContext } from '@intecion/ipal-kit/client'
+2 -1
View File
@@ -2,7 +2,8 @@
import React, { useEffect, useState } from 'react'
import { MessageCircle } from 'lucide-react'
import { motion, AnimatePresence } from 'framer-motion'
import { useConsentContext } from '@intecion/ipal-kit/client'
import { useConsentContext } from '../Consent'
export const FloatingWhatsApp = ({ whatsappLink, ariaLabel }: { whatsappLink: string, ariaLabel: string }) => {
const [isVisible, setIsVisible] = useState(true)
+43
View File
@@ -0,0 +1,43 @@
'use server'
import { getPayload } from 'payload'
import configPromise from '../../payload.config'
import { submitForm } from '@intecion/ipal-kit/server'
import { headers } from 'next/headers'
export async function submitContactFormAction(formData: FormData) {
const payload = await getPayload({ config: configPromise })
const formId = formData.get('formId') as string
const turnstileToken = formData.get('cf-turnstile-response') as string | undefined
const data: Record<string, unknown> = {}
for (const [key, value] of formData.entries()) {
if (key !== 'formId' && key !== 'cf-turnstile-response' && !key.startsWith('$ACTION')) {
data[key] = value
}
}
// The consent checkbox usually sends "on" when checked.
// We need to convert it to true for the form builder to accept it as checked boolean if the field is a checkbox.
// We can do this automatically for all fields that are not empty but 'on' or 'true' or just leave it.
// Wait, payload-plugin-form-builder expects boolean for checkboxes.
for (const [key, value] of Object.entries(data)) {
if (value === 'on') data[key] = true
}
const heads = await headers()
const ip = heads.get('x-forwarded-for') || heads.get('x-real-ip') || undefined
const result = await submitForm({
payload,
formId,
data,
turnstileToken,
ip,
consentFieldName: 'consent', // we can set default 'consent' or leave undefined (it defaults to 'consent')
})
// Next.js Server Actions must return plain objects
return JSON.parse(JSON.stringify(result))
}
+173
View File
@@ -0,0 +1,173 @@
'use client'
import React, { useState } from 'react'
import { Turnstile, resolveFormMessage } from '@intecion/ipal-kit/client'
import { submitContactFormAction } from './actions'
export function FormClient({
form,
notifications,
turnstileSiteKey
}: {
form: any;
notifications: any;
turnstileSiteKey: string
}) {
const [status, setStatus] = useState<'idle' | 'submitting' | 'success'>('idle')
const [errorMsg, setErrorMsg] = useState<string | null>(null)
const [turnstileToken, setTurnstileToken] = useState<string | null>(null)
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setStatus('submitting')
setErrorMsg(null)
const formData = new FormData(e.currentTarget)
formData.set('formId', form.id.toString())
if (turnstileToken) {
formData.set('cf-turnstile-response', turnstileToken)
}
try {
const result = await submitContactFormAction(formData)
if (result.success) {
setStatus('success')
} else {
setStatus('idle')
setErrorMsg(resolveFormMessage(result, notifications))
}
} catch (err) {
setStatus('idle')
setErrorMsg('Wystąpił błąd podczas wysyłania formularza. Spróbuj ponownie później.')
}
}
if (status === 'success') {
return (
<div className="flex flex-col items-center justify-center p-8 text-center bg-accent/5 border border-accent/20 rounded-lg">
<div className="w-16 h-16 bg-accent text-accent-foreground rounded-full flex items-center justify-center mb-6">
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h3 className="text-2xl font-bold text-foreground mb-4 font-heading">{form.confirmationType === 'message' && form.confirmationMessage ? 'Wiadomość wysłana' : 'Sukces'}</h3>
<p className="text-foreground/80 leading-relaxed max-w-md">
Dziękujemy za kontakt. Odpowiemy najszybciej jak to możliwe.
</p>
</div>
)
}
return (
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
{errorMsg && (
<div className="p-4 bg-destructive/10 border border-destructive/20 text-destructive rounded-lg text-sm font-medium">
{errorMsg}
</div>
)}
{form.fields?.map((field: any, idx: number) => {
if (field.blockType === 'text') {
return (
<div key={idx} className="flex flex-col gap-2">
{field.label && <label htmlFor={field.name} className="text-sm font-medium text-foreground/80">{field.label} {field.required && <span className="text-destructive">*</span>}</label>}
<input
type="text"
id={field.name}
name={field.name}
required={field.required}
placeholder={field.defaultValue || ''}
className="flex h-12 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
/>
</div>
)
}
if (field.blockType === 'email') {
return (
<div key={idx} className="flex flex-col gap-2">
{field.label && <label htmlFor={field.name} className="text-sm font-medium text-foreground/80">{field.label} {field.required && <span className="text-destructive">*</span>}</label>}
<input
type="email"
id={field.name}
name={field.name}
required={field.required}
placeholder={field.defaultValue || (field.label === 'Adres e-mail' ? '[email protected]' : '[email protected]')}
className="flex h-12 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
/>
</div>
)
}
if (field.blockType === 'textarea') {
return (
<div key={idx} className="flex flex-col gap-2">
{field.label && <label htmlFor={field.name} className="text-sm font-medium text-foreground/80">{field.label} {field.required && <span className="text-destructive">*</span>}</label>}
<textarea
id={field.name}
name={field.name}
required={field.required}
rows={5}
placeholder={field.defaultValue || ''}
className="flex min-h-[120px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
/>
</div>
)
}
if (field.blockType === 'select') {
return (
<div key={idx} className="flex flex-col gap-2">
{field.label && <label htmlFor={field.name} className="text-sm font-medium text-foreground/80">{field.label} {field.required && <span className="text-destructive">*</span>}</label>}
<select
id={field.name}
name={field.name}
required={field.required}
className="flex h-12 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="">--</option>
{field.options?.map((opt: any, oIdx: number) => (
<option key={oIdx} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
)
}
if (field.blockType === 'checkbox') {
return (
<div key={idx} className="flex flex-row items-start gap-3 mt-2">
<div className="flex items-center h-6">
<input
type="checkbox"
id={field.name}
name={field.name}
required={field.required}
defaultChecked={field.defaultValue}
className="w-5 h-5 rounded border-border text-accent focus:ring-accent"
/>
</div>
{field.label && (
<label htmlFor={field.name} className="text-sm text-foreground/80 leading-tight">
<span dangerouslySetInnerHTML={{ __html: field.label }} />
{field.required && <span className="text-destructive ml-1">*</span>}
</label>
)}
</div>
)
}
return null
})}
{turnstileSiteKey && (
<div className="my-2">
<Turnstile siteKey={turnstileSiteKey} onToken={setTurnstileToken} />
</div>
)}
<button
type="submit"
disabled={status === 'submitting'}
className="mt-4 h-14 bg-accent hover:bg-accent/90 text-white font-bold font-heading tracking-wide uppercase px-8 flex items-center justify-center transition-colors disabled:opacity-70 disabled:cursor-not-allowed"
>
{status === 'submitting' ? 'Wysyłanie...' : (form.submitButtonLabel || 'Wyślij')}
</button>
</form>
)
}
+30
View File
@@ -0,0 +1,30 @@
import React from 'react'
import { getPayload } from 'payload'
import configPromise from '../../payload.config'
import { FormClient } from './client'
import { getNotificationTexts, getSiteIntegrations } from '@intecion/ipal-kit'
import { headers } from 'next/headers'
export async function FormRenderer({ form }: { form: any }) {
if (typeof form !== 'object' || !form) return null
const payload = await getPayload({ config: configPromise })
// Get current locale from headers or fallback
const heads = await headers()
const pathname = heads.get('x-pathname') || ''
const locale = pathname.split('/').filter(Boolean)[0] === 'de' ? 'de' : 'pl'
const notifications = await getNotificationTexts({ payload, locale })
const siteIntegrations = await getSiteIntegrations<any>(payload, { locale })
const turnstileSiteKey = siteIntegrations?.turnstile?.siteKey || ''
return (
<FormClient
form={form}
notifications={notifications.form}
turnstileSiteKey={turnstileSiteKey}
/>
)
}
+4 -4
View File
@@ -29,16 +29,16 @@ export function Header({ navItems = [], whatsappNumber }: { navItems?: any[], wh
return (
<>
<header
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 bg-background/90 backdrop-blur-md shadow-sm border-b border-border ${
isScrolled
? "bg-background/90 backdrop-blur-md shadow-sm border-b border-border py-4"
: "bg-transparent py-6"
? "py-4"
: "py-6"
}`}
style={{ paddingRight: 'var(--scrollbar-width)' }}
>
<div className="container-premium flex items-center justify-between">
{/* Logo */}
<Link href={`/${currentLocale.code}`} className="group flex items-center gap-3 z-50" aria-label="Finanz- & Lohnbuchhaltung Dipl.-Ing. Janusz Kędzierski">
<Link href={`/${currentLocale.code}`} className="group flex items-center gap-3 z-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-background rounded-sm" aria-label="Finanz- & Lohnbuchhaltung Dipl.-Ing. Janusz Kędzierski">
<div className="w-10 h-10 rounded-none bg-accent text-white flex items-center justify-center font-heading font-bold text-xl group-hover:bg-accent-hover transition-colors shadow-sm">
JK
</div>
+385
View File
@@ -71,6 +71,8 @@ export interface Config {
users: User;
media: Media;
pages: Page;
forms: Form;
'form-submissions': FormSubmission;
'payload-mcp-api-keys': PayloadMcpApiKey;
'payload-kv': PayloadKv;
'payload-locked-documents': PayloadLockedDocument;
@@ -82,6 +84,8 @@ export interface Config {
users: UsersSelect<false> | UsersSelect<true>;
media: MediaSelect<false> | MediaSelect<true>;
pages: PagesSelect<false> | PagesSelect<true>;
forms: FormsSelect<false> | FormsSelect<true>;
'form-submissions': FormSubmissionsSelect<false> | FormSubmissionsSelect<true>;
'payload-mcp-api-keys': PayloadMcpApiKeysSelect<false> | PayloadMcpApiKeysSelect<true>;
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
@@ -230,6 +234,25 @@ export interface Page {
blockName?: string | null;
blockType: 'contactInfo';
}
| {
/**
* Użyj Enter by przejść do nowej linii
*/
address?: string | null;
phone?: string | null;
email?: string | null;
/**
* Użyj Enter by przejść do nowej linii
*/
hours?: string | null;
/**
* Wybierz formularz kontaktowy. Dzięki opcji "localized" możesz wybrać inny formularz dla języka polskiego i inny dla niemieckiego.
*/
form: number | Form;
id?: string | null;
blockName?: string | null;
blockType: 'contactSection';
}
| {
title: string;
paragraphs: {
@@ -269,6 +292,10 @@ export interface Page {
subtitle?: string | null;
services: {
title: string;
/**
* Auto-generated from the title when left empty. You can override it.
*/
anchor: string;
description: string;
icon?: ('fileText' | 'calculator' | 'shieldCheck' | 'scale' | 'helpCircle' | 'users' | 'coins') | null;
id?: string | null;
@@ -508,6 +535,193 @@ export interface Page {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "forms".
*/
export interface Form {
id: number;
title: string;
fields?:
| (
| {
name: string;
label?: string | null;
width?: number | null;
required?: boolean | null;
defaultValue?: boolean | null;
id?: string | null;
blockName?: string | null;
blockType: 'checkbox';
}
| {
name: string;
label?: string | null;
width?: number | null;
required?: boolean | null;
id?: string | null;
blockName?: string | null;
blockType: 'country';
}
| {
name: string;
label?: string | null;
width?: number | null;
required?: boolean | null;
id?: string | null;
blockName?: string | null;
blockType: 'email';
}
| {
message?: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
id?: string | null;
blockName?: string | null;
blockType: 'message';
}
| {
name: string;
label?: string | null;
width?: number | null;
defaultValue?: number | null;
required?: boolean | null;
id?: string | null;
blockName?: string | null;
blockType: 'number';
}
| {
name: string;
label?: string | null;
width?: number | null;
defaultValue?: string | null;
placeholder?: string | null;
options?:
| {
label: string;
value: string;
id?: string | null;
}[]
| null;
required?: boolean | null;
id?: string | null;
blockName?: string | null;
blockType: 'select';
}
| {
name: string;
label?: string | null;
width?: number | null;
required?: boolean | null;
id?: string | null;
blockName?: string | null;
blockType: 'state';
}
| {
name: string;
label?: string | null;
width?: number | null;
defaultValue?: string | null;
required?: boolean | null;
id?: string | null;
blockName?: string | null;
blockType: 'text';
}
| {
name: string;
label?: string | null;
width?: number | null;
defaultValue?: string | null;
required?: boolean | null;
id?: string | null;
blockName?: string | null;
blockType: 'textarea';
}
)[]
| null;
submitButtonLabel?: string | null;
confirmationType?: ('message' | 'redirect') | null;
confirmationMessage?: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
redirect?: {
type?: ('reference' | 'custom') | null;
reference?: {
relationTo: 'pages';
value: number | Page;
} | null;
url?: string | null;
};
emails?:
| {
emailTo?: string | null;
cc?: string | null;
bcc?: string | null;
replyTo?: string | null;
emailFrom?: string | null;
subject: string;
message?: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
id?: string | null;
}[]
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "form-submissions".
*/
export interface FormSubmission {
id: number;
form: number | Form;
submissionData?:
| {
field: string;
value: string;
id?: string | null;
}[]
| null;
updatedAt: string;
createdAt: string;
}
/**
* API keys control which collections, resources, tools, and prompts MCP clients can access
*
@@ -571,6 +785,14 @@ export interface PayloadLockedDocument {
relationTo: 'pages';
value: number | Page;
} | null)
| ({
relationTo: 'forms';
value: number | Form;
} | null)
| ({
relationTo: 'form-submissions';
value: number | FormSubmission;
} | null)
| ({
relationTo: 'payload-mcp-api-keys';
value: number | PayloadMcpApiKey;
@@ -696,6 +918,17 @@ export interface PagesSelect<T extends boolean = true> {
id?: T;
blockName?: T;
};
contactSection?:
| T
| {
address?: T;
phone?: T;
email?: T;
hours?: T;
form?: T;
id?: T;
blockName?: T;
};
bio?:
| T
| {
@@ -743,6 +976,7 @@ export interface PagesSelect<T extends boolean = true> {
| T
| {
title?: T;
anchor?: T;
description?: T;
icon?: T;
id?: T;
@@ -986,6 +1220,157 @@ export interface PagesSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "forms_select".
*/
export interface FormsSelect<T extends boolean = true> {
title?: T;
fields?:
| T
| {
checkbox?:
| T
| {
name?: T;
label?: T;
width?: T;
required?: T;
defaultValue?: T;
id?: T;
blockName?: T;
};
country?:
| T
| {
name?: T;
label?: T;
width?: T;
required?: T;
id?: T;
blockName?: T;
};
email?:
| T
| {
name?: T;
label?: T;
width?: T;
required?: T;
id?: T;
blockName?: T;
};
message?:
| T
| {
message?: T;
id?: T;
blockName?: T;
};
number?:
| T
| {
name?: T;
label?: T;
width?: T;
defaultValue?: T;
required?: T;
id?: T;
blockName?: T;
};
select?:
| T
| {
name?: T;
label?: T;
width?: T;
defaultValue?: T;
placeholder?: T;
options?:
| T
| {
label?: T;
value?: T;
id?: T;
};
required?: T;
id?: T;
blockName?: T;
};
state?:
| T
| {
name?: T;
label?: T;
width?: T;
required?: T;
id?: T;
blockName?: T;
};
text?:
| T
| {
name?: T;
label?: T;
width?: T;
defaultValue?: T;
required?: T;
id?: T;
blockName?: T;
};
textarea?:
| T
| {
name?: T;
label?: T;
width?: T;
defaultValue?: T;
required?: T;
id?: T;
blockName?: T;
};
};
submitButtonLabel?: T;
confirmationType?: T;
confirmationMessage?: T;
redirect?:
| T
| {
type?: T;
reference?: T;
url?: T;
};
emails?:
| T
| {
emailTo?: T;
cc?: T;
bcc?: T;
replyTo?: T;
emailFrom?: T;
subject?: T;
message?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "form-submissions_select".
*/
export interface FormSubmissionsSelect<T extends boolean = true> {
form?: T;
submissionData?:
| T
| {
field?: T;
value?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-mcp-api-keys_select".
+12 -2
View File
@@ -1,6 +1,6 @@
import { sqliteAdapter } from '@payloadcms/db-sqlite'
import { mcpPlugin } from '@payloadcms/plugin-mcp'
import { ipalKit } from '@intecion/ipal-kit'
import { ipalKit, panelSmtpAdapter } from '@intecion/ipal-kit'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import path from 'path'
import { buildConfig } from 'payload'
@@ -43,5 +43,15 @@ export default buildConfig({
fallback: true,
defaultLocale: i18n.defaultLocale,
},
plugins: [mcpPlugin({}), ipalKit({ i18n, pages: { slug: 'pages' } })],
email: panelSmtpAdapter(),
plugins: [
mcpPlugin({}),
ipalKit({
i18n,
pages: { slug: 'pages' },
forms: {
redirectRelationships: ['pages']
}
})
],
})