feat(forms): add FormBlock component for form-builder rendering
This commit is contained in:
parent
0925ab2052
commit
6e5e624345
1 changed files with 258 additions and 0 deletions
258
src/components/forms/FormBlock.tsx
Normal file
258
src/components/forms/FormBlock.tsx
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
'use client'
|
||||
|
||||
import { useState, useTransition } from 'react'
|
||||
|
||||
const FONT = 'var(--font-montserrat, Montserrat), sans-serif'
|
||||
|
||||
const INPUT_CLS =
|
||||
'w-full rounded-[12px] border-2 border-white/20 bg-white/10 px-4 py-3 text-[15px] text-white placeholder-white/40 focus:border-[#f28b4a] focus:outline-none'
|
||||
|
||||
interface FieldOption {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface FormField {
|
||||
blockType: string
|
||||
id?: string
|
||||
name: string
|
||||
label?: string
|
||||
required?: boolean
|
||||
placeholder?: string
|
||||
defaultValue?: string
|
||||
options?: FieldOption[]
|
||||
message?: string
|
||||
width?: number
|
||||
}
|
||||
|
||||
interface FormData {
|
||||
id: string
|
||||
title?: string
|
||||
fields?: FormField[]
|
||||
confirmationType?: 'message' | 'redirect'
|
||||
confirmationMessage?: unknown
|
||||
redirect?: { url: string }
|
||||
submitButtonLabel?: string
|
||||
}
|
||||
|
||||
interface FormBlockProps {
|
||||
form: FormData
|
||||
submitLabel?: string
|
||||
}
|
||||
|
||||
export function FormBlock({ form, submitLabel }: FormBlockProps) {
|
||||
const [values, setValues] = useState<Record<string, string>>({})
|
||||
const [success, setSuccess] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
||||
function setValue(name: string, value: string) {
|
||||
setValues((prev) => ({ ...prev, [name]: value }))
|
||||
}
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const submissionData = Object.entries(values).map(([field, value]) => ({ field, value }))
|
||||
const res = await fetch('/api/form-submissions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ form: form.id, submissionData }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
setError('Щось пішло не так. Спробуйте ще раз.')
|
||||
return
|
||||
}
|
||||
if (form.confirmationType === 'redirect' && form.redirect?.url) {
|
||||
window.location.href = form.redirect.url
|
||||
return
|
||||
}
|
||||
setSuccess(true)
|
||||
} catch {
|
||||
setError("Помилка мережі. Перевірте з'єднання та спробуйте ще раз.")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4 py-10 text-center">
|
||||
<div className="text-[48px]">✅</div>
|
||||
<h3 className="text-[24px] font-bold text-white" style={{ fontFamily: FONT }}>
|
||||
Заявку отримано!
|
||||
</h3>
|
||||
<p className="text-[16px] text-white/70" style={{ fontFamily: FONT }}>
|
||||
Менеджер зв'яжеться з вами найближчим часом.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const fields = form.fields ?? []
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
|
||||
{fields.map((field) => {
|
||||
if (field.blockType === 'message') {
|
||||
return (
|
||||
<div
|
||||
key={field.id ?? field.name}
|
||||
className="text-[14px] text-white/60 md:col-span-2"
|
||||
style={{ fontFamily: FONT }}
|
||||
dangerouslySetInnerHTML={{ __html: field.message ?? '' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const isFullWidth =
|
||||
field.blockType === 'textarea' || (field.width != null && field.width > 50)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={field.id ?? field.name}
|
||||
className={`flex flex-col gap-2${isFullWidth ? 'md:col-span-2' : ''}`}
|
||||
>
|
||||
{field.label && (
|
||||
<label
|
||||
htmlFor={field.name}
|
||||
className="text-[14px] font-medium text-white/80"
|
||||
style={{ fontFamily: FONT }}
|
||||
>
|
||||
{field.label}
|
||||
{field.required && ' *'}
|
||||
</label>
|
||||
)}
|
||||
{renderInput(field, values[field.name] ?? '', setValue)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-xl border border-red-400 bg-red-900/30 px-4 py-3 text-[14px] text-red-300">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="mt-2 inline-flex items-center justify-center rounded-[64px] bg-[#f28b4a] px-10 py-4 text-[16px] font-bold text-white transition-shadow hover:shadow-[0_0_20px_0_#f28b4a] disabled:opacity-60"
|
||||
style={{ fontFamily: FONT }}
|
||||
>
|
||||
{isPending
|
||||
? 'Надсилаємо...'
|
||||
: (form.submitButtonLabel ?? submitLabel ?? 'Надіслати заявку')}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
function renderInput(
|
||||
field: FormField,
|
||||
value: string,
|
||||
setValue: (name: string, val: string) => void
|
||||
) {
|
||||
const cls = INPUT_CLS
|
||||
const font = { fontFamily: FONT }
|
||||
const { name, required, placeholder } = field
|
||||
|
||||
switch (field.blockType) {
|
||||
case 'textarea':
|
||||
return (
|
||||
<textarea
|
||||
id={name}
|
||||
rows={4}
|
||||
required={required}
|
||||
value={value}
|
||||
onChange={(e) => setValue(name, e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className={cls + ' resize-none'}
|
||||
style={font}
|
||||
/>
|
||||
)
|
||||
case 'select':
|
||||
return (
|
||||
<select
|
||||
id={name}
|
||||
required={required}
|
||||
value={value}
|
||||
onChange={(e) => setValue(name, e.target.value)}
|
||||
className={cls}
|
||||
style={font}
|
||||
>
|
||||
<option value="">Оберіть...</option>
|
||||
{field.options?.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
case 'email':
|
||||
return (
|
||||
<input
|
||||
id={name}
|
||||
type="email"
|
||||
required={required}
|
||||
value={value}
|
||||
onChange={(e) => setValue(name, e.target.value)}
|
||||
placeholder={placeholder ?? 'your@email.com'}
|
||||
className={cls}
|
||||
style={font}
|
||||
/>
|
||||
)
|
||||
case 'number':
|
||||
return (
|
||||
<input
|
||||
id={name}
|
||||
type="number"
|
||||
required={required}
|
||||
value={value}
|
||||
onChange={(e) => setValue(name, e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className={cls}
|
||||
style={font}
|
||||
/>
|
||||
)
|
||||
case 'date':
|
||||
return (
|
||||
<input
|
||||
id={name}
|
||||
type="date"
|
||||
required={required}
|
||||
value={value}
|
||||
onChange={(e) => setValue(name, e.target.value)}
|
||||
className={cls}
|
||||
style={font}
|
||||
/>
|
||||
)
|
||||
case 'checkbox':
|
||||
return (
|
||||
<input
|
||||
id={name}
|
||||
type="checkbox"
|
||||
required={required}
|
||||
checked={value === 'true'}
|
||||
onChange={(e) => setValue(name, String(e.target.checked))}
|
||||
className="h-5 w-5 rounded accent-[#f28b4a]"
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<input
|
||||
id={name}
|
||||
type="text"
|
||||
required={required}
|
||||
value={value}
|
||||
onChange={(e) => setValue(name, e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className={cls}
|
||||
style={font}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue