wwwfiberdirekt/src/blocks/FDContactFormBlock/Component.tsx

417 lines
14 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'
import { FDButton } from '@/components/FDButton'
/* ------------------------------------------------------------------ */
/* Theme maps */
/* ------------------------------------------------------------------ */
const sectionBgMap: Record<string, string> = {
white: 'bg-white dark:bg-fd-navy',
gray: 'bg-fd-gray-light dark:bg-fd-navy',
navy: 'bg-fd-navy',
navyGradient: 'bg-gradient-to-br from-fd-navy via-[#153350] to-fd-navy',
}
const isExplicitDark = (bg: string) => bg === 'navy' || bg === 'navyGradient'
const cardRadius = 'rounded-[32px] md:rounded-[50px] lg:rounded-[70px]'
/* ------------------------------------------------------------------ */
/* Component */
/* ------------------------------------------------------------------ */
export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
heading,
description,
form: formRelation,
submitText = 'Skicka förfrågan',
privacyText,
privacyLinkText,
privacyLinkUrl,
sideImage,
sectionBackground = 'white',
layout = 'standard',
externalApi,
anchorId,
}) => {
const [formData, setFormData] = useState<Record<string, string>>({})
const [status, setStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle')
const media = sideImage as Media | undefined
const hasSideImage = layout === 'withImage' && Boolean(media?.url)
const dark = isExplicitDark(sectionBackground || 'white')
const sectionBg = sectionBgMap[sectionBackground || 'white']
const isCard = layout === 'card'
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) 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 })),
}),
})
if (externalApi?.enabled && externalApi?.endpoint) {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (externalApi.authToken) headers['Authorization'] = `Bearer ${externalApi.authToken}`
fetch(externalApi.endpoint, {
method: 'POST',
headers,
body: JSON.stringify({
formId,
formName: form?.title || '',
data: formData,
submittedAt: new Date().toISOString(),
}),
}).catch(() => {})
}
setStatus(res.ok ? 'sent' : 'error')
} catch {
setStatus('error')
}
}
/* ---- Input styles ---- */
const inputBase = 'w-full rounded-xl px-4 py-3 font-joey text-base outline-none transition-all'
const inputClass = dark
? `${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`
: `${inputBase} bg-gray-100 text-fd-navy placeholder:text-fd-navy/40 focus:ring-2 focus:ring-fd-navy/20 focus:bg-white dark:bg-white/10 dark:text-white dark:placeholder:text-white/40 dark:border dark:border-white/20 dark:focus:ring-fd-yellow/30 dark:focus:bg-white/15`
const checkboxClass = dark
? 'w-5 h-5 rounded border-white/30 text-fd-yellow focus:ring-fd-yellow/30'
: 'w-5 h-5 rounded border-gray-300 text-fd-navy focus:ring-fd-navy/20 dark:border-white/30 dark:text-fd-yellow dark:focus:ring-fd-yellow/30'
/* ---- Text color helpers ---- */
const headingColor = dark ? 'text-fd-yellow' : 'text-fd-navy dark:text-fd-yellow'
const bodyColor = dark ? 'text-white/80' : 'text-fd-navy/80 dark:text-white/80'
const labelColor = dark ? 'text-white' : 'text-fd-navy dark:text-white'
const mutedColor = dark ? 'text-white/50' : 'text-fd-navy/60 dark:text-white/50'
const errorColor = dark ? 'text-red-300' : 'text-red-600 dark:text-red-300'
const requiredColor = dark ? 'text-fd-yellow' : 'text-fd-navy dark:text-fd-yellow'
const linkColor = dark
? 'underline underline-offset-2 hover:text-fd-yellow transition-colors'
: 'underline underline-offset-2 hover:text-fd-navy dark:hover:text-fd-yellow transition-colors'
/* ---- Field renderer ---- */
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={`${inputClass} resize-y`}
value={formData[name] || ''}
onChange={(e) => handleChange(name, e.target.value)}
/>
)
case 'select':
return (
<select
name={name}
required={required}
className={inputClass}
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={inputClass}
placeholder={field.placeholder || ''}
value={formData[name] || ''}
onChange={(e) => handleChange(name, e.target.value)}
/>
)
case 'number':
return (
<input
type="number"
name={name}
required={required}
className={inputClass}
value={formData[name] || ''}
onChange={(e) => handleChange(name, e.target.value)}
/>
)
case 'checkbox':
return (
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
name={name}
required={required}
className={checkboxClass}
checked={formData[name] === 'true'}
onChange={(e) => handleChange(name, e.target.checked ? 'true' : 'false')}
/>
<span className={`font-joey text-sm ${labelColor}`}>{label}</span>
</label>
)
case 'country':
case 'state':
case 'text':
default:
return (
<input
type="text"
name={name}
required={required}
className={inputClass}
placeholder={field.placeholder || ''}
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-sm md:text-base ${labelColor}`}>
{label}
{required && <span className={requiredColor}> *</span>}
</label>
{input}
</div>
),
width,
}
}
/* ---- Fields layout ---- */
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]
if (field.blockType === 'message') {
elements.push(<div key={i} className={`font-joey text-sm ${mutedColor}`} />)
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
}
/* ---- Success state ---- */
if (status === 'sent') {
return (
<section id={anchorId || undefined} 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={`inline-flex items-center justify-center w-16 h-16 rounded-full mb-6 ${dark ? 'bg-fd-yellow/20' : 'bg-fd-mint/20 dark:bg-fd-yellow/20'}`}>
<svg className={`w-8 h-8 ${dark ? 'text-fd-yellow' : 'text-fd-mint dark:text-fd-yellow'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div>
{/* Priority #6: Use fluid token instead of hardcoded sizes */}
<h2 className={`font-joey-heavy text-fd-h1 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.
</p>
</div>
</section>
)
}
/* ---- Empty state ---- */
if (!form) {
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 text-center">
<p className={`font-joey ${mutedColor}`}>Inget formulär valt.</p>
</div>
</section>
)
}
/* ---- Form content ---- */
const formContent = (
<>
{heading && (
/* Priority #6: Use fluid token text-fd-h1 instead of hardcoded text-3xl/4xl/5xl */
<h2 className={`font-joey-heavy text-fd-h1 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()}
<div className="mt-2">
<FDButton
as="button"
type="submit"
variant="primary"
onDark={dark}
disabled={status === 'sending'}
>
{status === 'sending' ? 'Skickar...' : submitText}
</FDButton>
</div>
{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>
</>
)
/* ---- Layout: Card ---- */
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 ${cardRadius} ${
dark
? 'bg-white/5 border border-white/10 backdrop-blur-sm'
: 'bg-white shadow-lg border border-gray-100 dark:bg-white/5 dark:border-white/10 dark:backdrop-blur-sm'
}`}
>
{formContent}
</div>
</div>
</section>
)
}
/* ---- 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-[560px] object-cover rounded-[32px] md:rounded-[50px] lg:rounded-[70px]"
sizes="380px"
fallbackAlt={heading || ''}
/>
</div>
</div>
</div>
</section>
)
}
/* ---- Layout: Standard ---- */
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>
)
}