319 lines
9.8 KiB
TypeScript
319 lines
9.8 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState } from 'react'
|
|
import type { FDContactFormBlock as FDContactFormBlockProps } from '@/payload-types'
|
|
import type { Media } from '@/payload-types'
|
|
import { FDImage } from '@/components/FDImage'
|
|
|
|
const bgMap: Record<string, string> = {
|
|
white: 'bg-white',
|
|
gray: 'bg-fd-gray-light',
|
|
}
|
|
|
|
export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
|
|
heading,
|
|
description,
|
|
form: formRelation,
|
|
submitText = 'Skicka förfrågan',
|
|
privacyText,
|
|
privacyLinkText,
|
|
privacyLinkUrl,
|
|
sideImage,
|
|
background = 'white',
|
|
}) => {
|
|
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']
|
|
|
|
// Extract form object — could be a populated object or just an ID
|
|
const form = formRelation && typeof formRelation === 'object' ? formRelation : null
|
|
const formId = form ? form.id : (formRelation ?? null)
|
|
|
|
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
|
|
}
|
|
|
|
setStatus('sending')
|
|
try {
|
|
const res = await fetch('/api/form-submissions', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
form: formId,
|
|
submissionData: Object.entries(formData).map(([field, value]) => ({
|
|
field,
|
|
value,
|
|
})),
|
|
}),
|
|
})
|
|
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'
|
|
|
|
const renderFormField = (field: any) => {
|
|
const blockType = field.blockType
|
|
const name = field.name || field.label?.toLowerCase().replace(/\s+/g, '-') || ''
|
|
const label = field.label || ''
|
|
const required = field.required || false
|
|
const width = field.width ? Number(field.width) : 100
|
|
|
|
const input = (() => {
|
|
switch (blockType) {
|
|
case 'textarea':
|
|
return (
|
|
<textarea
|
|
name={name}
|
|
required={required}
|
|
rows={4}
|
|
className={`${baseInputClass} resize-y`}
|
|
value={formData[name] || ''}
|
|
onChange={(e) => handleChange(name, e.target.value)}
|
|
/>
|
|
)
|
|
case 'select':
|
|
return (
|
|
<select
|
|
name={name}
|
|
required={required}
|
|
className={baseInputClass}
|
|
value={formData[name] || ''}
|
|
onChange={(e) => handleChange(name, e.target.value)}
|
|
>
|
|
<option value="">Välj...</option>
|
|
{field.options?.map((opt: any) => (
|
|
<option key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
)
|
|
case 'email':
|
|
return (
|
|
<input
|
|
type="email"
|
|
name={name}
|
|
required={required}
|
|
className={baseInputClass}
|
|
value={formData[name] || ''}
|
|
onChange={(e) => handleChange(name, e.target.value)}
|
|
/>
|
|
)
|
|
case 'number':
|
|
return (
|
|
<input
|
|
type="number"
|
|
name={name}
|
|
required={required}
|
|
className={baseInputClass}
|
|
value={formData[name] || ''}
|
|
onChange={(e) => handleChange(name, e.target.value)}
|
|
/>
|
|
)
|
|
case 'checkbox':
|
|
return (
|
|
<label className="flex items-center gap-2 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"
|
|
checked={formData[name] === 'true'}
|
|
onChange={(e) => handleChange(name, e.target.checked ? 'true' : 'false')}
|
|
/>
|
|
<span className="font-joey text-fd-navy text-sm">{label}</span>
|
|
</label>
|
|
)
|
|
case 'country':
|
|
case 'state':
|
|
case 'text':
|
|
default:
|
|
return (
|
|
<input
|
|
type="text"
|
|
name={name}
|
|
required={required}
|
|
className={baseInputClass}
|
|
value={formData[name] || ''}
|
|
onChange={(e) => handleChange(name, e.target.value)}
|
|
/>
|
|
)
|
|
}
|
|
})()
|
|
|
|
if (blockType === 'checkbox') {
|
|
return { element: input, width }
|
|
}
|
|
|
|
return {
|
|
element: (
|
|
<div className="flex flex-col gap-1.5">
|
|
<label className="font-joey text-fd-navy text-sm md:text-base">
|
|
{label}
|
|
{required && ' *'}
|
|
</label>
|
|
{input}
|
|
</div>
|
|
),
|
|
width,
|
|
}
|
|
}
|
|
|
|
const renderFields = () => {
|
|
if (!form?.fields) return null
|
|
|
|
const elements: React.ReactNode[] = []
|
|
let i = 0
|
|
const fields = form.fields
|
|
|
|
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>,
|
|
)
|
|
i++
|
|
continue
|
|
}
|
|
|
|
const { element, width } = renderFormField(field)
|
|
const isHalf = width <= 50
|
|
|
|
if (isHalf) {
|
|
const nextField = i + 1 < fields.length ? fields[i + 1] : null
|
|
const nextResult = nextField && nextField.blockType !== 'message' ? renderFormField(nextField) : null
|
|
const nextIsHalf = nextResult && nextResult.width <= 50
|
|
|
|
if (nextIsHalf && nextResult) {
|
|
elements.push(
|
|
<div key={i} className="grid grid-cols-1 sm:grid-cols-2 gap-4 md:gap-6">
|
|
{element}
|
|
{nextResult.element}
|
|
</div>,
|
|
)
|
|
i += 2
|
|
continue
|
|
}
|
|
}
|
|
|
|
elements.push(
|
|
<div key={i} className={isHalf ? 'w-full sm:w-1/2' : 'w-full'}>
|
|
{element}
|
|
</div>,
|
|
)
|
|
i++
|
|
}
|
|
|
|
return elements
|
|
}
|
|
|
|
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}`}>
|
|
<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">
|
|
Vi har tagit emot din förfrågan och återkommer så snart vi kan.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
if (!form) {
|
|
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 text-center">
|
|
<p className="font-joey text-fd-navy/60">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 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 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>
|
|
</div>
|
|
|
|
{hasSideImage && (
|
|
<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]"
|
|
sizes="380px"
|
|
fallbackAlt={heading || ''}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|