Enhanced FDContactFormBlock with dark mode, remove search, add FormBlock to pages
This commit is contained in:
parent
9038b0c894
commit
5cf9186ee2
BIN
media/08-FD-Grön-Fiber-04.mp4
Normal file
BIN
media/08-FD-Grön-Fiber-04.mp4
Normal file
Binary file not shown.
BIN
media/08-FD-Grön-Fiber-5.mp4
Normal file
BIN
media/08-FD-Grön-Fiber-5.mp4
Normal file
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
2
next-env.d.ts
vendored
@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <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
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@ -3,7 +3,7 @@ import React, { useState, useEffect, useRef } from 'react'
|
||||
import type { Header as HeaderType } from '@/payload-types'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { SearchIcon, MenuIcon, XIcon, ChevronDownIcon, ChevronRightIcon } from 'lucide-react'
|
||||
import { MenuIcon, XIcon, ChevronDownIcon, ChevronRightIcon } from 'lucide-react'
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
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 />
|
||||
</nav>
|
||||
|
||||
@ -351,15 +346,6 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
|
||||
</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>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -17,8 +17,6 @@ import { PreviewComponent as PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860 }
|
||||
import { SlugField as SlugField_2b8867833a34864a02ddf429b0728a40 } from '@payloadcms/next/client'
|
||||
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_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_1f6ff6ff633e3695d348f4f3c58f1466 } from '@/Footer/RowLabel'
|
||||
import { default as default_c1363893506d7ed29a71d1a4da01ddb5 } from '@/components/AdminIcon'
|
||||
@ -47,8 +45,6 @@ export const importMap = {
|
||||
"@payloadcms/next/client#SlugField": SlugField_2b8867833a34864a02ddf429b0728a40,
|
||||
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_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,
|
||||
"@/Footer/RowLabel#RowLabel": RowLabel_1f6ff6ff633e3695d348f4f3c58f1466,
|
||||
"@/components/AdminIcon#default": default_c1363893506d7ed29a71d1a4da01ddb5,
|
||||
|
||||
@ -5,11 +5,23 @@ import type { FDContactFormBlock as FDContactFormBlockProps } from '@/payload-ty
|
||||
import type { Media } from '@/payload-types'
|
||||
import { FDImage } from '@/components/FDImage'
|
||||
|
||||
const bgMap: Record<string, string> = {
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Theme maps */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const sectionBgMap: Record<string, string> = {
|
||||
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> = ({
|
||||
heading,
|
||||
description,
|
||||
@ -20,31 +32,35 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
|
||||
privacyLinkUrl,
|
||||
sideImage,
|
||||
background = 'white',
|
||||
layout = 'standard',
|
||||
externalApi,
|
||||
}) => {
|
||||
const [formData, setFormData] = useState<Record<string, string>>({})
|
||||
const [status, setStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle')
|
||||
|
||||
const media = sideImage as Media | undefined
|
||||
const hasSideImage = Boolean(media?.url)
|
||||
const sectionBg = bgMap[background || 'white']
|
||||
const hasSideImage = layout === 'withImage' && Boolean(media?.url)
|
||||
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 formId = form ? form.id : (formRelation ?? null)
|
||||
|
||||
/* ---- Handlers ---- */
|
||||
|
||||
const handleChange = (name: string, value: string) => {
|
||||
setFormData((prev) => ({ ...prev, [name]: value }))
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!formId) {
|
||||
console.warn('No form ID configured')
|
||||
return
|
||||
}
|
||||
if (!formId) return
|
||||
|
||||
setStatus('sending')
|
||||
try {
|
||||
// 1. Submit to Payload
|
||||
const res = await fetch('/api/form-submissions', {
|
||||
method: 'POST',
|
||||
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')
|
||||
} catch {
|
||||
setStatus('error')
|
||||
}
|
||||
}
|
||||
|
||||
const baseInputClass =
|
||||
'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'
|
||||
/* ---- Input styles (light vs dark) ---- */
|
||||
|
||||
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 blockType = field.blockType
|
||||
@ -80,7 +137,7 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
|
||||
name={name}
|
||||
required={required}
|
||||
rows={4}
|
||||
className={`${baseInputClass} resize-y`}
|
||||
className={`${inputClass} resize-y`}
|
||||
value={formData[name] || ''}
|
||||
onChange={(e) => handleChange(name, e.target.value)}
|
||||
/>
|
||||
@ -90,7 +147,7 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
|
||||
<select
|
||||
name={name}
|
||||
required={required}
|
||||
className={baseInputClass}
|
||||
className={inputClass}
|
||||
value={formData[name] || ''}
|
||||
onChange={(e) => handleChange(name, e.target.value)}
|
||||
>
|
||||
@ -108,7 +165,8 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
|
||||
type="email"
|
||||
name={name}
|
||||
required={required}
|
||||
className={baseInputClass}
|
||||
className={inputClass}
|
||||
placeholder={field.placeholder || ''}
|
||||
value={formData[name] || ''}
|
||||
onChange={(e) => handleChange(name, e.target.value)}
|
||||
/>
|
||||
@ -119,23 +177,23 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
|
||||
type="number"
|
||||
name={name}
|
||||
required={required}
|
||||
className={baseInputClass}
|
||||
className={inputClass}
|
||||
value={formData[name] || ''}
|
||||
onChange={(e) => handleChange(name, e.target.value)}
|
||||
/>
|
||||
)
|
||||
case 'checkbox':
|
||||
return (
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
name={name}
|
||||
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'}
|
||||
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>
|
||||
)
|
||||
case 'country':
|
||||
@ -147,7 +205,8 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
|
||||
type="text"
|
||||
name={name}
|
||||
required={required}
|
||||
className={baseInputClass}
|
||||
className={inputClass}
|
||||
placeholder={field.placeholder || ''}
|
||||
value={formData[name] || ''}
|
||||
onChange={(e) => handleChange(name, e.target.value)}
|
||||
/>
|
||||
@ -162,9 +221,9 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
|
||||
return {
|
||||
element: (
|
||||
<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}
|
||||
{required && ' *'}
|
||||
{required && <span className={dark ? 'text-fd-yellow' : 'text-fd-navy'}> *</span>}
|
||||
</label>
|
||||
{input}
|
||||
</div>
|
||||
@ -173,6 +232,8 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Fields layout (handles half-width pairing) ---- */
|
||||
|
||||
const renderFields = () => {
|
||||
if (!form?.fields) return null
|
||||
|
||||
@ -183,12 +244,9 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
|
||||
while (i < fields.length) {
|
||||
const field = fields[i]
|
||||
|
||||
// Skip message blocks — render them inline
|
||||
if (field.blockType === 'message') {
|
||||
elements.push(
|
||||
<div key={i} className="font-joey text-fd-navy/70 text-sm">
|
||||
{/* Message blocks contain rich text, render as simple text for now */}
|
||||
</div>,
|
||||
<div key={i} className={`font-joey text-sm ${mutedColor}`} />,
|
||||
)
|
||||
i++
|
||||
continue
|
||||
@ -199,7 +257,8 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
|
||||
|
||||
if (isHalf) {
|
||||
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
|
||||
|
||||
if (nextIsHalf && nextResult) {
|
||||
@ -225,13 +284,19 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
|
||||
return elements
|
||||
}
|
||||
|
||||
/* ---- Success state ---- */
|
||||
|
||||
if (status === 'sent') {
|
||||
// Use confirmation message from Payload form if available
|
||||
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">
|
||||
<h2 className="font-joey-heavy text-fd-navy text-3xl md:text-4xl mb-4">Tack!</h2>
|
||||
<p className="font-joey text-fd-navy text-lg">
|
||||
<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'}`}>
|
||||
<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 så snart vi kan.
|
||||
</p>
|
||||
</div>
|
||||
@ -239,79 +304,121 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
/* ---- Empty state ---- */
|
||||
|
||||
if (!form) {
|
||||
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">
|
||||
<p className="font-joey text-fd-navy/60">Inget formulär valt.</p>
|
||||
<p className={`font-joey ${mutedColor}`}>Inget formulär valt.</p>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<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">
|
||||
<div className={`flex flex-col ${hasSideImage ? 'lg:flex-row' : ''} gap-10 lg:gap-16`}>
|
||||
<div className={hasSideImage ? 'flex-1' : 'max-w-[800px]'}>
|
||||
{heading && (
|
||||
<h2 className="font-joey-heavy text-fd-navy text-3xl md:text-4xl lg:text-5xl leading-tight mb-3">
|
||||
{heading}
|
||||
</h2>
|
||||
)}
|
||||
{description && (
|
||||
<p className="font-joey text-fd-navy/80 text-base md:text-lg mb-8">{description}</p>
|
||||
/* ---- Form content ---- */
|
||||
|
||||
const formContent = (
|
||||
<>
|
||||
{heading && (
|
||||
<h2
|
||||
className={`font-joey-heavy text-3xl md:text-4xl lg:text-5xl leading-tight mb-3 ${headingColor}`}
|
||||
>
|
||||
{heading}
|
||||
</h2>
|
||||
)}
|
||||
{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">
|
||||
{renderFields()}
|
||||
/* ---- Layout: Card ---- */
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === 'sending'}
|
||||
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"
|
||||
>
|
||||
{status === 'sending' ? 'Skickar...' : submitText}
|
||||
</button>
|
||||
|
||||
{status === 'error' && (
|
||||
<p className="font-joey text-red-600 text-sm">
|
||||
Något gick fel. Försök igen eller kontakta oss direkt.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{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>
|
||||
if (isCard) {
|
||||
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-[720px] mx-auto p-8 md:p-12 rounded-[40px] md:rounded-[70px] ${
|
||||
dark
|
||||
? 'bg-white/5 border border-white/10 backdrop-blur-sm'
|
||||
: 'bg-white shadow-lg border border-gray-100'
|
||||
}`}
|
||||
>
|
||||
{formContent}
|
||||
</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">
|
||||
<FDImage
|
||||
media={media!}
|
||||
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"
|
||||
fallbackAlt={heading || ''}
|
||||
/>
|
||||
</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>
|
||||
</section>
|
||||
)
|
||||
|
||||
@ -4,10 +4,11 @@ export const FDContactFormBlock: Block = {
|
||||
slug: 'fdContactForm',
|
||||
interfaceName: 'FDContactFormBlock',
|
||||
labels: {
|
||||
singular: 'FD Contact Form',
|
||||
plural: 'FD Contact Forms',
|
||||
singular: 'FD Kontaktformulär',
|
||||
plural: 'FD Kontaktformulär',
|
||||
},
|
||||
fields: [
|
||||
// --- Content ---
|
||||
{
|
||||
name: 'form',
|
||||
type: 'relationship',
|
||||
@ -30,7 +31,7 @@ export const FDContactFormBlock: Block = {
|
||||
type: 'textarea',
|
||||
label: 'Beskrivning',
|
||||
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',
|
||||
@ -38,6 +39,50 @@ export const FDContactFormBlock: Block = {
|
||||
label: 'Skicka-knapp text',
|
||||
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',
|
||||
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.',
|
||||
},
|
||||
{
|
||||
name: 'privacyLinkText',
|
||||
type: 'text',
|
||||
label: 'Integritetslänk text',
|
||||
defaultValue: 'integritetspolicy',
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
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',
|
||||
type: 'text',
|
||||
label: 'Integritetslänk URL',
|
||||
defaultValue: '/integritetspolicy',
|
||||
},
|
||||
{
|
||||
name: 'sideImage',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
label: 'Sidobild (valfri)',
|
||||
name: 'externalApi',
|
||||
type: 'group',
|
||||
label: 'Extern API-integration (valfri)',
|
||||
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',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'background',
|
||||
type: 'select',
|
||||
label: 'Bakgrund',
|
||||
defaultValue: 'white',
|
||||
options: [
|
||||
{ label: 'Vit', value: 'white' },
|
||||
{ label: 'Grå', value: 'gray' },
|
||||
fields: [
|
||||
{
|
||||
name: 'enabled',
|
||||
type: 'checkbox',
|
||||
label: 'Aktivera extern API',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
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),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@ -75,7 +75,6 @@ export interface Config {
|
||||
redirects: Redirect;
|
||||
forms: Form;
|
||||
'form-submissions': FormSubmission;
|
||||
search: Search;
|
||||
'payload-kv': PayloadKv;
|
||||
'payload-jobs': PayloadJob;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
@ -92,7 +91,6 @@ export interface Config {
|
||||
redirects: RedirectsSelect<false> | RedirectsSelect<true>;
|
||||
forms: FormsSelect<false> | FormsSelect<true>;
|
||||
'form-submissions': FormSubmissionsSelect<false> | FormSubmissionsSelect<true>;
|
||||
search: SearchSelect<false> | SearchSelect<true>;
|
||||
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
|
||||
'payload-jobs': PayloadJobsSelect<false> | PayloadJobsSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
@ -758,14 +756,29 @@ export interface FDContactFormBlock {
|
||||
heading: string;
|
||||
description?: string | null;
|
||||
submitText?: string | null;
|
||||
background?: ('white' | 'gray' | 'navy' | 'navyGradient') | null;
|
||||
layout?: ('standard' | 'withImage' | 'card') | null;
|
||||
/**
|
||||
* Visas till höger om formuläret på desktop
|
||||
*/
|
||||
sideImage?: (number | null) | Media;
|
||||
privacyText?: string | null;
|
||||
privacyLinkText?: 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;
|
||||
background?: ('white' | 'gray') | null;
|
||||
externalApi?: {
|
||||
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;
|
||||
blockName?: string | null;
|
||||
blockType: 'fdContactForm';
|
||||
@ -1364,37 +1377,6 @@ export interface FormSubmission {
|
||||
updatedAt: 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
|
||||
* via the `definition` "payload-kv".
|
||||
@ -1542,10 +1524,6 @@ export interface PayloadLockedDocument {
|
||||
| ({
|
||||
relationTo: 'form-submissions';
|
||||
value: number | FormSubmission;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'search';
|
||||
value: number | Search;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
@ -1952,11 +1930,19 @@ export interface FDContactFormBlockSelect<T extends boolean = true> {
|
||||
heading?: T;
|
||||
description?: T;
|
||||
submitText?: T;
|
||||
background?: T;
|
||||
layout?: T;
|
||||
sideImage?: T;
|
||||
privacyText?: T;
|
||||
privacyLinkText?: T;
|
||||
privacyLinkUrl?: T;
|
||||
sideImage?: T;
|
||||
background?: T;
|
||||
externalApi?:
|
||||
| T
|
||||
| {
|
||||
enabled?: T;
|
||||
endpoint?: T;
|
||||
authToken?: T;
|
||||
};
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
}
|
||||
@ -2520,33 +2506,6 @@ export interface FormSubmissionsSelect<T extends boolean = true> {
|
||||
updatedAt?: 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
|
||||
* via the `definition` "payload-kv_select".
|
||||
|
||||
@ -3,12 +3,9 @@ import { FixedToolbarFeature, HeadingFeature, lexicalEditor } from '@payloadcms/
|
||||
import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs'
|
||||
import { redirectsPlugin } from '@payloadcms/plugin-redirects'
|
||||
import { seoPlugin } from '@payloadcms/plugin-seo'
|
||||
import { searchPlugin } from '@payloadcms/plugin-search'
|
||||
import { Plugin } from 'payload'
|
||||
import { revalidateRedirects } from '@/hooks/revalidateRedirects'
|
||||
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 { getServerSideURL } from '@/utilities/getURL'
|
||||
|
||||
@ -79,13 +76,4 @@ export const plugins: Plugin[] = [
|
||||
},
|
||||
},
|
||||
}),
|
||||
searchPlugin({
|
||||
collections: ['posts'],
|
||||
beforeSync: beforeSyncWithSearch,
|
||||
searchOverrides: {
|
||||
fields: ({ defaultFields }) => {
|
||||
return [...defaultFields, ...searchFields]
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user