src/components/form/index.tsx'use client'import React, { Children, isValidElement, useRef, useState } from 'react'import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile'import { DateTime } from 'luxon'import clsx from 'clsx'// Sub-componentsimport { FormInput } from './form-input'import { FormSelect } from './form-select'import { FormTextArea } from './form-textarea'import { FormUpload } from './form-upload'import { FormRadioGroup } from './form-radio'import { FormCheckboxGroup } from './form-checkbox'import { FormName, FormFirstName, FormLastName } from './form-name'import { FormButton, type FormButtonProps } from './form-button'import { DatePickerInput } from './date-picker'import { TimePickerInput } from './time-picker'// ============================================================================// Main Form Component// ============================================================================type FormProps = {classname?: stringchildren: React.ReactNodegap?: stringflexDirection?: stringhoneypot?: booleanturnstile?: boolean}function Form({classname,children,gap = 'gap-8',flexDirection = 'flex-col',honeypot = false,turnstile = false,}: FormProps) {const [status, setStatus] = useState('')const [isLoading, setIsLoading] = useState(false)const [formStartTime] = useState(() => DateTime.now().toMillis())const turnstileRef = useRef<TurnstileInstance>(null)const turnstileToken = useRef<string | null>(null)const fileToBase64 = (file: File): Promise<string> =>new Promise((resolve, reject) => {const reader = new FileReader()reader.readAsDataURL(file)reader.onload = () => resolve(reader.result as string)reader.onerror = (error) => reject(error)})const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {e.preventDefault()setIsLoading(true)const form = e.currentTargetconst fd = new FormData(form)const obj: Record<string, any> = {}let submitMessage = ''const submitter = (e.nativeEvent as SubmitEvent)?.submitter as| HTMLButtonElement| HTMLInputElement| nullconst buttonMessage = submitter?.getAttribute?.('data-message') || nullconst hasButtonMessage = Boolean(buttonMessage)if (hasButtonMessage) submitMessage = buttonMessage as stringconst cssEscape = (s: string) =>typeof CSS !== 'undefined' && (CSS as any).escape? (CSS as any).escape(s): s.replace(/["\\]/g, '\\$&')const getFieldLabel = (domName: string) => {const nameSel = cssEscape(domName)const el = form.querySelector<HTMLElement>(`[name="${nameSel}"]`)return el?.getAttribute('data-label') || null}const getSubmitMessage = (domName: string) => {const nameSel = cssEscape(domName)const el = form.querySelector<HTMLElement>(`[name="${nameSel}"]`)return el?.getAttribute('data-message') || null}for (const [k, v] of fd) {// Skip Turnstile's injected hidden inputif (k === 'cf-turnstile-response') continueconst isArray = k.endsWith('[]')const key = isArray ? k.slice(0, -2) : kconst val = v instanceof File ? v : String(v)const label = getFieldLabel(k)if (!hasButtonMessage) {const fieldMsg = getSubmitMessage(k)if (fieldMsg) submitMessage = fieldMsg}if (key === 'files') {if (!obj[key]) obj[key] = []if (val instanceof File && val.name) {const base64 = await fileToBase64(val)obj[key].push({name: val.name,type: val.type,size: val.size,base64,label,})}continue}if (isArray) {if (!obj[key]) obj[key] = { value: [], label }if (!Array.isArray(obj[key].value)) obj[key].value = []obj[key].value.push(val)if (!obj[key].label && label) obj[key].label = labelcontinue}obj[key] = { value: val, label }}// Honeypot checksif (honeypot) {if (obj.website?.value) {setIsLoading(false)setStatus('Message did not send.')return}delete obj.websiteconst submissionTime = DateTime.now().toMillis() - formStartTimeif (submissionTime < 3000) {setIsLoading(false)setStatus('Message did not send.')return}obj._submissionTime = submissionTime}// Turnstile verificationif (turnstile) {turnstileToken.current = nullturnstileRef.current?.execute()const maxWait = 10000const interval = 100let waited = 0while (!turnstileToken.current && waited < maxWait) {await new Promise((r) => setTimeout(r, interval))waited += interval}if (!turnstileToken.current) {setIsLoading(false)setStatus('Verification failed.')return}obj._turnstileToken = turnstileToken.current}const api = '/api/submit-form/'const response = await fetch(api, {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify(obj),})if (turnstile) turnstileRef.current?.reset()if (response.ok) {form.reset()form.querySelectorAll<HTMLInputElement>('input[type="file"]').forEach((input) => {input.value = ''})form.dispatchEvent(new Event('form-cleared', { bubbles: true }))} else {submitMessage = 'An error occurred.'}setIsLoading(false)setStatus(submitMessage)}return (<><formonSubmit={handleSubmit}className={clsx('flex', flexDirection, gap, classname)}>{honeypot && (<inputtype="text"name="website"className="!hidden"tabIndex={-1}aria-hidden="true"autoComplete="off"/>)}{Children.map(children, (child) => {if (isValidElement(child) &&(child.props as any)?.name === 'submit') {const btn = child as React.ReactElement<FormButtonProps>return React.cloneElement<FormButtonProps>(btn, { isLoading })}return child})}</form>{turnstile && (<Turnstileref={turnstileRef}siteKey={process.env.NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY!}options={{ size: 'invisible', execution: 'execute' }}onSuccess={(token) => {turnstileToken.current = token}}onError={() => {}}/>)}{status && (<p className="text-center text-sm font-medium text-contrast-light mt-4">{status}</p>)}</>)}// Attach sub-components for compound component patternForm.Input = FormInputForm.Select = FormSelectForm.TextArea = FormTextAreaForm.File = FormUploadForm.FormRadioGroup = FormRadioGroupForm.FormCheckboxGroup = FormCheckboxGroupForm.FormButton = FormButtonForm.FormName = FormNameForm.FormFirstName = FormFirstNameForm.FormLastName = FormLastName// ============================================================================// Exports// ============================================================================export {Form,FormInput,FormSelect,FormTextArea,FormUpload,FormRadioGroup,FormCheckboxGroup,FormButton,FormName,FormFirstName,FormLastName,DatePickerInput,TimePickerInput,}