Enhanced FDContactFormBlock with dark mode, remove search, add FormBlock to pages

This commit is contained in:
Jeffrey 2026-02-17 17:51:39 +01:00
parent 9038b0c894
commit 5cf9186ee2
14 changed files with 314 additions and 209 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

2
next-env.d.ts vendored
View File

@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@ -3,7 +3,7 @@ import React, { useState, useEffect, useRef } from 'react'
import type { Header as HeaderType } from '@/payload-types' import type { Header as HeaderType } from '@/payload-types'
import Link from 'next/link' import Link from 'next/link'
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
import { SearchIcon, MenuIcon, XIcon, ChevronDownIcon, ChevronRightIcon } from 'lucide-react' import { MenuIcon, XIcon, ChevronDownIcon, ChevronRightIcon } from 'lucide-react'
// ─── Types ─────────────────────────────────────────────────────────────────── // ─── Types ───────────────────────────────────────────────────────────────────
type NavChild = { type NavChild = {
@ -258,11 +258,6 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
) )
})} })}
<Link href="/search" className="text-fd-navy hover:text-fd-yellow transition-colors">
<span className="sr-only">Sök</span>
<SearchIcon className="w-5" />
</Link>
<SwedishFlag /> <SwedishFlag />
</nav> </nav>
@ -351,15 +346,6 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
</Link> </Link>
) )
})} })}
<Link
href="/search"
onClick={closeMobile}
className="py-4 text-white font-joey-medium text-xl border-b border-white/10 hover:text-fd-yellow transition-colors flex items-center gap-3"
>
<SearchIcon className="w-5 h-5" />
Sök
</Link>
</nav> </nav>
</div> </div>
</> </>

View File

@ -17,8 +17,6 @@ import { PreviewComponent as PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860 }
import { SlugField as SlugField_2b8867833a34864a02ddf429b0728a40 } from '@payloadcms/next/client' import { SlugField as SlugField_2b8867833a34864a02ddf429b0728a40 } from '@payloadcms/next/client'
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { LinkToDoc as LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634 } from '@payloadcms/plugin-search/client'
import { ReindexButton as ReindexButton_aead06e4cbf6b2620c5c51c9ab283634 } from '@payloadcms/plugin-search/client'
import { RowLabel as RowLabel_ec255a65fa6fa8d1faeb09cf35284224 } from '@/Header/RowLabel' import { RowLabel as RowLabel_ec255a65fa6fa8d1faeb09cf35284224 } from '@/Header/RowLabel'
import { RowLabel as RowLabel_1f6ff6ff633e3695d348f4f3c58f1466 } from '@/Footer/RowLabel' import { RowLabel as RowLabel_1f6ff6ff633e3695d348f4f3c58f1466 } from '@/Footer/RowLabel'
import { default as default_c1363893506d7ed29a71d1a4da01ddb5 } from '@/components/AdminIcon' import { default as default_c1363893506d7ed29a71d1a4da01ddb5 } from '@/components/AdminIcon'
@ -47,8 +45,6 @@ export const importMap = {
"@payloadcms/next/client#SlugField": SlugField_2b8867833a34864a02ddf429b0728a40, "@payloadcms/next/client#SlugField": SlugField_2b8867833a34864a02ddf429b0728a40,
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/plugin-search/client#LinkToDoc": LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634,
"@payloadcms/plugin-search/client#ReindexButton": ReindexButton_aead06e4cbf6b2620c5c51c9ab283634,
"@/Header/RowLabel#RowLabel": RowLabel_ec255a65fa6fa8d1faeb09cf35284224, "@/Header/RowLabel#RowLabel": RowLabel_ec255a65fa6fa8d1faeb09cf35284224,
"@/Footer/RowLabel#RowLabel": RowLabel_1f6ff6ff633e3695d348f4f3c58f1466, "@/Footer/RowLabel#RowLabel": RowLabel_1f6ff6ff633e3695d348f4f3c58f1466,
"@/components/AdminIcon#default": default_c1363893506d7ed29a71d1a4da01ddb5, "@/components/AdminIcon#default": default_c1363893506d7ed29a71d1a4da01ddb5,

View File

@ -5,11 +5,23 @@ import type { FDContactFormBlock as FDContactFormBlockProps } from '@/payload-ty
import type { Media } from '@/payload-types' import type { Media } from '@/payload-types'
import { FDImage } from '@/components/FDImage' import { FDImage } from '@/components/FDImage'
const bgMap: Record<string, string> = { /* ------------------------------------------------------------------ */
/* Theme maps */
/* ------------------------------------------------------------------ */
const sectionBgMap: Record<string, string> = {
white: 'bg-white', white: 'bg-white',
gray: 'bg-fd-gray-light', gray: 'bg-[#F0F0F0]',
navy: 'bg-fd-navy',
navyGradient: 'bg-gradient-to-br from-fd-navy via-[#153350] to-fd-navy',
} }
const isDark = (bg: string) => bg === 'navy' || bg === 'navyGradient'
/* ------------------------------------------------------------------ */
/* Component */
/* ------------------------------------------------------------------ */
export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
heading, heading,
description, description,
@ -20,31 +32,35 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
privacyLinkUrl, privacyLinkUrl,
sideImage, sideImage,
background = 'white', background = 'white',
layout = 'standard',
externalApi,
}) => { }) => {
const [formData, setFormData] = useState<Record<string, string>>({}) const [formData, setFormData] = useState<Record<string, string>>({})
const [status, setStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle') const [status, setStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle')
const media = sideImage as Media | undefined const media = sideImage as Media | undefined
const hasSideImage = Boolean(media?.url) const hasSideImage = layout === 'withImage' && Boolean(media?.url)
const sectionBg = bgMap[background || 'white'] const dark = isDark(background || 'white')
const sectionBg = sectionBgMap[background || 'white']
const isCard = layout === 'card'
// Extract form object — could be a populated object or just an ID // Extract form object
const form = formRelation && typeof formRelation === 'object' ? formRelation : null const form = formRelation && typeof formRelation === 'object' ? formRelation : null
const formId = form ? form.id : (formRelation ?? null) const formId = form ? form.id : (formRelation ?? null)
/* ---- Handlers ---- */
const handleChange = (name: string, value: string) => { const handleChange = (name: string, value: string) => {
setFormData((prev) => ({ ...prev, [name]: value })) setFormData((prev) => ({ ...prev, [name]: value }))
} }
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
if (!formId) { if (!formId) return
console.warn('No form ID configured')
return
}
setStatus('sending') setStatus('sending')
try { try {
// 1. Submit to Payload
const res = await fetch('/api/form-submissions', { const res = await fetch('/api/form-submissions', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@ -56,14 +72,55 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
})), })),
}), }),
}) })
// 2. Submit to external API if configured
if (externalApi?.enabled && externalApi?.endpoint) {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (externalApi.authToken) {
headers['Authorization'] = `Bearer ${externalApi.authToken}`
}
// Fire and forget — don't block UX on external API
fetch(externalApi.endpoint, {
method: 'POST',
headers,
body: JSON.stringify({
formId,
formName: form?.title || '',
data: formData,
submittedAt: new Date().toISOString(),
}),
}).catch(() => {}) // silently fail
}
setStatus(res.ok ? 'sent' : 'error') setStatus(res.ok ? 'sent' : 'error')
} catch { } catch {
setStatus('error') setStatus('error')
} }
} }
const baseInputClass = /* ---- Input styles (light vs dark) ---- */
'w-full bg-gray-100 rounded-lg px-4 py-3 font-joey text-fd-navy text-base outline-none focus:ring-2 focus:ring-fd-navy/20 transition-shadow'
const inputBase = 'w-full rounded-xl px-4 py-3 font-joey text-base outline-none transition-all'
const inputLight = `${inputBase} bg-gray-100 text-fd-navy placeholder:text-fd-navy/40 focus:ring-2 focus:ring-fd-navy/20 focus:bg-white`
const inputDark = `${inputBase} bg-white/10 text-white placeholder:text-white/40 border border-white/20 focus:ring-2 focus:ring-fd-yellow/30 focus:bg-white/15 focus:border-fd-yellow/50`
const inputClass = dark ? inputDark : inputLight
const checkboxLight = 'w-5 h-5 rounded border-gray-300 text-fd-navy focus:ring-fd-navy/20'
const checkboxDark = 'w-5 h-5 rounded border-white/30 text-fd-yellow focus:ring-fd-yellow/30'
const checkboxClass = dark ? checkboxDark : checkboxLight
/* ---- Text color helpers ---- */
const headingColor = dark ? 'text-fd-yellow' : 'text-fd-navy'
const bodyColor = dark ? 'text-white/80' : 'text-fd-navy/80'
const labelColor = dark ? 'text-white' : 'text-fd-navy'
const mutedColor = dark ? 'text-white/50' : 'text-fd-navy/60'
const linkColor = dark
? 'underline underline-offset-2 hover:text-fd-yellow transition-colors'
: 'underline underline-offset-2 hover:text-fd-navy transition-colors'
const errorColor = dark ? 'text-red-300' : 'text-red-600'
/* ---- Field renderer ---- */
const renderFormField = (field: any) => { const renderFormField = (field: any) => {
const blockType = field.blockType const blockType = field.blockType
@ -80,7 +137,7 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
name={name} name={name}
required={required} required={required}
rows={4} rows={4}
className={`${baseInputClass} resize-y`} className={`${inputClass} resize-y`}
value={formData[name] || ''} value={formData[name] || ''}
onChange={(e) => handleChange(name, e.target.value)} onChange={(e) => handleChange(name, e.target.value)}
/> />
@ -90,7 +147,7 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
<select <select
name={name} name={name}
required={required} required={required}
className={baseInputClass} className={inputClass}
value={formData[name] || ''} value={formData[name] || ''}
onChange={(e) => handleChange(name, e.target.value)} onChange={(e) => handleChange(name, e.target.value)}
> >
@ -108,7 +165,8 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
type="email" type="email"
name={name} name={name}
required={required} required={required}
className={baseInputClass} className={inputClass}
placeholder={field.placeholder || ''}
value={formData[name] || ''} value={formData[name] || ''}
onChange={(e) => handleChange(name, e.target.value)} onChange={(e) => handleChange(name, e.target.value)}
/> />
@ -119,23 +177,23 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
type="number" type="number"
name={name} name={name}
required={required} required={required}
className={baseInputClass} className={inputClass}
value={formData[name] || ''} value={formData[name] || ''}
onChange={(e) => handleChange(name, e.target.value)} onChange={(e) => handleChange(name, e.target.value)}
/> />
) )
case 'checkbox': case 'checkbox':
return ( return (
<label className="flex items-center gap-2 cursor-pointer"> <label className="flex items-center gap-3 cursor-pointer">
<input <input
type="checkbox" type="checkbox"
name={name} name={name}
required={required} required={required}
className="w-5 h-5 rounded border-gray-300 text-fd-navy focus:ring-fd-navy/20" className={checkboxClass}
checked={formData[name] === 'true'} checked={formData[name] === 'true'}
onChange={(e) => handleChange(name, e.target.checked ? 'true' : 'false')} onChange={(e) => handleChange(name, e.target.checked ? 'true' : 'false')}
/> />
<span className="font-joey text-fd-navy text-sm">{label}</span> <span className={`font-joey text-sm ${labelColor}`}>{label}</span>
</label> </label>
) )
case 'country': case 'country':
@ -147,7 +205,8 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
type="text" type="text"
name={name} name={name}
required={required} required={required}
className={baseInputClass} className={inputClass}
placeholder={field.placeholder || ''}
value={formData[name] || ''} value={formData[name] || ''}
onChange={(e) => handleChange(name, e.target.value)} onChange={(e) => handleChange(name, e.target.value)}
/> />
@ -162,9 +221,9 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
return { return {
element: ( element: (
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<label className="font-joey text-fd-navy text-sm md:text-base"> <label className={`font-joey text-sm md:text-base ${labelColor}`}>
{label} {label}
{required && ' *'} {required && <span className={dark ? 'text-fd-yellow' : 'text-fd-navy'}> *</span>}
</label> </label>
{input} {input}
</div> </div>
@ -173,6 +232,8 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
} }
} }
/* ---- Fields layout (handles half-width pairing) ---- */
const renderFields = () => { const renderFields = () => {
if (!form?.fields) return null if (!form?.fields) return null
@ -183,12 +244,9 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
while (i < fields.length) { while (i < fields.length) {
const field = fields[i] const field = fields[i]
// Skip message blocks — render them inline
if (field.blockType === 'message') { if (field.blockType === 'message') {
elements.push( elements.push(
<div key={i} className="font-joey text-fd-navy/70 text-sm"> <div key={i} className={`font-joey text-sm ${mutedColor}`} />,
{/* Message blocks contain rich text, render as simple text for now */}
</div>,
) )
i++ i++
continue continue
@ -199,7 +257,8 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
if (isHalf) { if (isHalf) {
const nextField = i + 1 < fields.length ? fields[i + 1] : null const nextField = i + 1 < fields.length ? fields[i + 1] : null
const nextResult = nextField && nextField.blockType !== 'message' ? renderFormField(nextField) : null const nextResult =
nextField && nextField.blockType !== 'message' ? renderFormField(nextField) : null
const nextIsHalf = nextResult && nextResult.width <= 50 const nextIsHalf = nextResult && nextResult.width <= 50
if (nextIsHalf && nextResult) { if (nextIsHalf && nextResult) {
@ -225,13 +284,19 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
return elements return elements
} }
/* ---- Success state ---- */
if (status === 'sent') { if (status === 'sent') {
// Use confirmation message from Payload form if available
return ( return (
<section className={`w-full py-12 md:py-16 lg:py-20 ${sectionBg}`}> <section className={`w-full py-16 md:py-20 lg:py-[99px] ${sectionBg}`}>
<div className="max-w-[1200px] mx-auto px-6 md:px-8 text-center"> <div className="max-w-[1200px] mx-auto px-6 md:px-8 text-center">
<h2 className="font-joey-heavy text-fd-navy text-3xl md:text-4xl mb-4">Tack!</h2> <div className={`inline-flex items-center justify-center w-16 h-16 rounded-full mb-6 ${dark ? 'bg-fd-yellow/20' : 'bg-fd-mint/20'}`}>
<p className="font-joey text-fd-navy text-lg"> <svg className={`w-8 h-8 ${dark ? 'text-fd-yellow' : 'text-fd-mint'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 className={`font-joey-heavy text-3xl md:text-4xl mb-4 ${headingColor}`}>Tack!</h2>
<p className={`font-joey text-lg ${bodyColor}`}>
Vi har tagit emot din förfrågan och återkommer snart vi kan. Vi har tagit emot din förfrågan och återkommer snart vi kan.
</p> </p>
</div> </div>
@ -239,79 +304,121 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
) )
} }
/* ---- Empty state ---- */
if (!form) { if (!form) {
return ( return (
<section className={`w-full py-12 md:py-16 lg:py-20 ${sectionBg}`}> <section className={`w-full py-16 md:py-20 lg:py-[99px] ${sectionBg}`}>
<div className="max-w-[1200px] mx-auto px-6 md:px-8 text-center"> <div className="max-w-[1200px] mx-auto px-6 md:px-8 text-center">
<p className="font-joey text-fd-navy/60">Inget formulär valt.</p> <p className={`font-joey ${mutedColor}`}>Inget formulär valt.</p>
</div> </div>
</section> </section>
) )
} }
return ( /* ---- Form content ---- */
<section className={`w-full py-12 md:py-16 lg:py-20 ${sectionBg}`}>
<div className="max-w-[1200px] mx-auto px-6 md:px-8"> const formContent = (
<div className={`flex flex-col ${hasSideImage ? 'lg:flex-row' : ''} gap-10 lg:gap-16`}> <>
<div className={hasSideImage ? 'flex-1' : 'max-w-[800px]'}> {heading && (
{heading && ( <h2
<h2 className="font-joey-heavy text-fd-navy text-3xl md:text-4xl lg:text-5xl leading-tight mb-3"> className={`font-joey-heavy text-3xl md:text-4xl lg:text-5xl leading-tight mb-3 ${headingColor}`}
{heading} >
</h2> {heading}
)} </h2>
{description && ( )}
<p className="font-joey text-fd-navy/80 text-base md:text-lg mb-8">{description}</p> {description && (
<p className={`font-joey text-base md:text-lg mb-8 ${bodyColor}`}>{description}</p>
)}
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
{renderFields()}
<button
type="submit"
disabled={status === 'sending'}
className={`self-start inline-flex items-center justify-center px-8 py-3 rounded-full font-joey-bold text-lg transition-colors disabled:opacity-60 mt-2 ${
dark
? 'bg-fd-yellow hover:bg-fd-yellow/90 text-fd-navy'
: 'bg-fd-yellow hover:bg-fd-yellow/90 text-fd-navy'
}`}
>
{status === 'sending' ? 'Skickar...' : submitText}
</button>
{status === 'error' && (
<p className={`font-joey text-sm ${errorColor}`}>
Något gick fel. Försök igen eller kontakta oss direkt.
</p>
)}
{privacyText && (
<p className={`font-joey text-xs md:text-sm leading-relaxed mt-2 ${mutedColor}`}>
{privacyText}{' '}
{privacyLinkText && privacyLinkUrl && (
<>
Läs mer i vår{' '}
<a href={privacyLinkUrl} className={linkColor}>
{privacyLinkText}
</a>
.
</>
)} )}
</p>
)}
</form>
</>
)
<form onSubmit={handleSubmit} className="flex flex-col gap-5"> /* ---- Layout: Card ---- */
{renderFields()}
<button if (isCard) {
type="submit" return (
disabled={status === 'sending'} <section className={`w-full py-16 md:py-20 lg:py-[99px] ${sectionBg}`}>
className="self-start inline-flex items-center justify-center px-8 py-3 bg-fd-yellow hover:bg-fd-yellow/90 rounded-full font-joey-bold text-fd-navy text-lg transition-colors disabled:opacity-60 mt-2" <div className="max-w-[1200px] mx-auto px-6 md:px-8">
> <div
{status === 'sending' ? 'Skickar...' : submitText} className={`max-w-[720px] mx-auto p-8 md:p-12 rounded-[40px] md:rounded-[70px] ${
</button> dark
? 'bg-white/5 border border-white/10 backdrop-blur-sm'
{status === 'error' && ( : 'bg-white shadow-lg border border-gray-100'
<p className="font-joey text-red-600 text-sm"> }`}
Något gick fel. Försök igen eller kontakta oss direkt. >
</p> {formContent}
)}
{privacyText && (
<p className="font-joey text-fd-navy/60 text-xs md:text-sm leading-relaxed mt-2">
{privacyText}{' '}
{privacyLinkText && privacyLinkUrl && (
<>
Läs mer i vår{' '}
<a
href={privacyLinkUrl}
className="underline underline-offset-2 hover:text-fd-navy transition-colors"
>
{privacyLinkText}
</a>
.
</>
)}
</p>
)}
</form>
</div> </div>
</div>
</section>
)
}
{hasSideImage && ( /* ---- Layout: With image ---- */
if (hasSideImage) {
return (
<section className={`w-full py-16 md:py-20 lg:py-[99px] ${sectionBg}`}>
<div className="max-w-[1200px] mx-auto px-6 md:px-8">
<div className="flex flex-col lg:flex-row gap-10 lg:gap-16 items-start">
<div className="flex-1">{formContent}</div>
<div className="hidden lg:block flex-shrink-0"> <div className="hidden lg:block flex-shrink-0">
<FDImage <FDImage
media={media!} media={media!}
size="medium" size="medium"
className="w-[380px] h-auto max-h-[500px] object-cover rounded-[20px]" className="w-[380px] h-auto max-h-[560px] object-cover rounded-[20px]"
sizes="380px" sizes="380px"
fallbackAlt={heading || ''} fallbackAlt={heading || ''}
/> />
</div> </div>
)} </div>
</div> </div>
</section>
)
}
/* ---- Layout: Standard (full width) ---- */
return (
<section className={`w-full py-16 md:py-20 lg:py-[99px] ${sectionBg}`}>
<div className="max-w-[1200px] mx-auto px-6 md:px-8">
<div className="max-w-[800px]">{formContent}</div>
</div> </div>
</section> </section>
) )

View File

@ -4,10 +4,11 @@ export const FDContactFormBlock: Block = {
slug: 'fdContactForm', slug: 'fdContactForm',
interfaceName: 'FDContactFormBlock', interfaceName: 'FDContactFormBlock',
labels: { labels: {
singular: 'FD Contact Form', singular: 'FD Kontaktformulär',
plural: 'FD Contact Forms', plural: 'FD Kontaktformulär',
}, },
fields: [ fields: [
// --- Content ---
{ {
name: 'form', name: 'form',
type: 'relationship', type: 'relationship',
@ -30,7 +31,7 @@ export const FDContactFormBlock: Block = {
type: 'textarea', type: 'textarea',
label: 'Beskrivning', label: 'Beskrivning',
defaultValue: defaultValue:
'Tell us about your goals, our team will reach out to you and help you achieve the right solution.', 'Berätta om era mål — vårt team kontaktar er och hjälper er hitta rätt lösning.',
}, },
{ {
name: 'submitText', name: 'submitText',
@ -38,6 +39,50 @@ export const FDContactFormBlock: Block = {
label: 'Skicka-knapp text', label: 'Skicka-knapp text',
defaultValue: 'Skicka förfrågan', defaultValue: 'Skicka förfrågan',
}, },
// --- Layout ---
{
type: 'row',
fields: [
{
name: 'background',
type: 'select',
label: 'Bakgrund',
defaultValue: 'white',
options: [
{ label: 'Vit', value: 'white' },
{ label: 'Ljusgrå', value: 'gray' },
{ label: 'Navy (mörk)', value: 'navy' },
{ label: 'Navy gradient', value: 'navyGradient' },
],
admin: { width: '50%' },
},
{
name: 'layout',
type: 'select',
label: 'Layout',
defaultValue: 'standard',
options: [
{ label: 'Standard (helbredd)', value: 'standard' },
{ label: 'Med sidobild', value: 'withImage' },
{ label: 'Kort (centrerat)', value: 'card' },
],
admin: { width: '50%' },
},
],
},
{
name: 'sideImage',
type: 'upload',
relationTo: 'media',
label: 'Sidobild',
admin: {
description: 'Visas till höger om formuläret på desktop',
condition: (_, siblingData) => siblingData?.layout === 'withImage',
},
},
// --- Privacy ---
{ {
name: 'privacyText', name: 'privacyText',
type: 'textarea', type: 'textarea',
@ -46,34 +91,58 @@ export const FDContactFormBlock: Block = {
'Vi använder din kontaktinformation för att svara på din förfrågan och dela detaljer om våra produkter och tjänster. Du kan när som helst avregistrera dig.', 'Vi använder din kontaktinformation för att svara på din förfrågan och dela detaljer om våra produkter och tjänster. Du kan när som helst avregistrera dig.',
}, },
{ {
name: 'privacyLinkText', type: 'row',
type: 'text', fields: [
label: 'Integritetslänk text', {
defaultValue: 'integritetspolicy', name: 'privacyLinkText',
type: 'text',
label: 'Integritetslänk text',
defaultValue: 'integritetspolicy',
admin: { width: '50%' },
},
{
name: 'privacyLinkUrl',
type: 'text',
label: 'Integritetslänk URL',
defaultValue: '/integritetspolicy',
admin: { width: '50%' },
},
],
}, },
// --- External API (optional) ---
{ {
name: 'privacyLinkUrl', name: 'externalApi',
type: 'text', type: 'group',
label: 'Integritetslänk URL', label: 'Extern API-integration (valfri)',
defaultValue: '/integritetspolicy',
},
{
name: 'sideImage',
type: 'upload',
relationTo: 'media',
label: 'Sidobild (valfri)',
admin: { admin: {
description: 'Bild som visas till höger om formuläret', description: 'Skicka formulärdata till ett externt system (t.ex. Lime CRM) utöver Payload',
}, },
}, fields: [
{ {
name: 'background', name: 'enabled',
type: 'select', type: 'checkbox',
label: 'Bakgrund', label: 'Aktivera extern API',
defaultValue: 'white', defaultValue: false,
options: [ },
{ label: 'Vit', value: 'white' }, {
{ label: 'Grå', value: 'gray' }, name: 'endpoint',
type: 'text',
label: 'API-URL',
admin: {
description: 'T.ex. https://api.lime-crm.se/webhook/forms',
condition: (_, siblingData) => Boolean(siblingData?.enabled),
},
},
{
name: 'authToken',
type: 'text',
label: 'Auth-token (Bearer)',
admin: {
description: 'Skickas som Authorization: Bearer {token}',
condition: (_, siblingData) => Boolean(siblingData?.enabled),
},
},
], ],
}, },
], ],

View File

@ -75,7 +75,6 @@ export interface Config {
redirects: Redirect; redirects: Redirect;
forms: Form; forms: Form;
'form-submissions': FormSubmission; 'form-submissions': FormSubmission;
search: Search;
'payload-kv': PayloadKv; 'payload-kv': PayloadKv;
'payload-jobs': PayloadJob; 'payload-jobs': PayloadJob;
'payload-locked-documents': PayloadLockedDocument; 'payload-locked-documents': PayloadLockedDocument;
@ -92,7 +91,6 @@ export interface Config {
redirects: RedirectsSelect<false> | RedirectsSelect<true>; redirects: RedirectsSelect<false> | RedirectsSelect<true>;
forms: FormsSelect<false> | FormsSelect<true>; forms: FormsSelect<false> | FormsSelect<true>;
'form-submissions': FormSubmissionsSelect<false> | FormSubmissionsSelect<true>; 'form-submissions': FormSubmissionsSelect<false> | FormSubmissionsSelect<true>;
search: SearchSelect<false> | SearchSelect<true>;
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>; 'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
'payload-jobs': PayloadJobsSelect<false> | PayloadJobsSelect<true>; 'payload-jobs': PayloadJobsSelect<false> | PayloadJobsSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>; 'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
@ -758,14 +756,29 @@ export interface FDContactFormBlock {
heading: string; heading: string;
description?: string | null; description?: string | null;
submitText?: string | null; submitText?: string | null;
background?: ('white' | 'gray' | 'navy' | 'navyGradient') | null;
layout?: ('standard' | 'withImage' | 'card') | null;
/**
* Visas till höger om formuläret desktop
*/
sideImage?: (number | null) | Media;
privacyText?: string | null; privacyText?: string | null;
privacyLinkText?: string | null; privacyLinkText?: string | null;
privacyLinkUrl?: string | null; privacyLinkUrl?: string | null;
/** /**
* Bild som visas till höger om formuläret * Skicka formulärdata till ett externt system (t.ex. Lime CRM) utöver Payload
*/ */
sideImage?: (number | null) | Media; externalApi?: {
background?: ('white' | 'gray') | null; enabled?: boolean | null;
/**
* T.ex. https://api.lime-crm.se/webhook/forms
*/
endpoint?: string | null;
/**
* Skickas som Authorization: Bearer {token}
*/
authToken?: string | null;
};
id?: string | null; id?: string | null;
blockName?: string | null; blockName?: string | null;
blockType: 'fdContactForm'; blockType: 'fdContactForm';
@ -1364,37 +1377,6 @@ export interface FormSubmission {
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/**
* This is a collection of automatically created search results. These results are used by the global site search and will be updated automatically as documents in the CMS are created or updated.
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "search".
*/
export interface Search {
id: number;
title?: string | null;
priority?: number | null;
doc: {
relationTo: 'posts';
value: number | Post;
};
slug?: string | null;
meta?: {
title?: string | null;
description?: string | null;
image?: (number | null) | Media;
};
categories?:
| {
relationTo?: string | null;
categoryID?: string | null;
title?: string | null;
id?: string | null;
}[]
| null;
updatedAt: string;
createdAt: string;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-kv". * via the `definition` "payload-kv".
@ -1542,10 +1524,6 @@ export interface PayloadLockedDocument {
| ({ | ({
relationTo: 'form-submissions'; relationTo: 'form-submissions';
value: number | FormSubmission; value: number | FormSubmission;
} | null)
| ({
relationTo: 'search';
value: number | Search;
} | null); } | null);
globalSlug?: string | null; globalSlug?: string | null;
user: { user: {
@ -1952,11 +1930,19 @@ export interface FDContactFormBlockSelect<T extends boolean = true> {
heading?: T; heading?: T;
description?: T; description?: T;
submitText?: T; submitText?: T;
background?: T;
layout?: T;
sideImage?: T;
privacyText?: T; privacyText?: T;
privacyLinkText?: T; privacyLinkText?: T;
privacyLinkUrl?: T; privacyLinkUrl?: T;
sideImage?: T; externalApi?:
background?: T; | T
| {
enabled?: T;
endpoint?: T;
authToken?: T;
};
id?: T; id?: T;
blockName?: T; blockName?: T;
} }
@ -2520,33 +2506,6 @@ export interface FormSubmissionsSelect<T extends boolean = true> {
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "search_select".
*/
export interface SearchSelect<T extends boolean = true> {
title?: T;
priority?: T;
doc?: T;
slug?: T;
meta?:
| T
| {
title?: T;
description?: T;
image?: T;
};
categories?:
| T
| {
relationTo?: T;
categoryID?: T;
title?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-kv_select". * via the `definition` "payload-kv_select".

View File

@ -3,12 +3,9 @@ import { FixedToolbarFeature, HeadingFeature, lexicalEditor } from '@payloadcms/
import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs' import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs'
import { redirectsPlugin } from '@payloadcms/plugin-redirects' import { redirectsPlugin } from '@payloadcms/plugin-redirects'
import { seoPlugin } from '@payloadcms/plugin-seo' import { seoPlugin } from '@payloadcms/plugin-seo'
import { searchPlugin } from '@payloadcms/plugin-search'
import { Plugin } from 'payload' import { Plugin } from 'payload'
import { revalidateRedirects } from '@/hooks/revalidateRedirects' import { revalidateRedirects } from '@/hooks/revalidateRedirects'
import { GenerateTitle, GenerateURL } from '@payloadcms/plugin-seo/types' import { GenerateTitle, GenerateURL } from '@payloadcms/plugin-seo/types'
import { searchFields } from '@/search/fieldOverrides'
import { beforeSyncWithSearch } from '@/search/beforeSync'
import { Page, Post } from '@/payload-types' import { Page, Post } from '@/payload-types'
import { getServerSideURL } from '@/utilities/getURL' import { getServerSideURL } from '@/utilities/getURL'
@ -79,13 +76,4 @@ export const plugins: Plugin[] = [
}, },
}, },
}), }),
searchPlugin({
collections: ['posts'],
beforeSync: beforeSyncWithSearch,
searchOverrides: {
fields: ({ defaultFields }) => {
return [...defaultFields, ...searchFields]
},
},
}),
] ]