import { useMemo, useState } from 'react'; import { __, sprintf } from '../../lib/i18n'; export type DynamicCheckoutFieldType = | 'text' | 'textarea' | 'select' | 'country' | 'checkbox' | 'radio' | 'number' | 'email' | 'tel'; export type DynamicCheckoutField = { id: string; label: string; type: DynamicCheckoutFieldType; placeholder?: string; enabled?: boolean; width?: 'full' | 'half'; required?: boolean; help?: string; options?: Array<{ value: string; label: string }>; visibility?: { depends_on?: string; depends_value?: string; depends_in?: string[]; }; persist_to_user?: boolean; system?: boolean; locked?: boolean; }; function safeJsonParseArray(raw: unknown): DynamicCheckoutField[] { if (typeof raw !== 'string') return []; try { const v = JSON.parse(raw); return Array.isArray(v) ? (v as DynamicCheckoutField[]) : []; } catch { return []; } } function toSchemaString(schema: DynamicCheckoutField[]): string { return JSON.stringify(schema, null, 2); } function slugifyId(input: string): string { return input .toLowerCase() .trim() .replace(/[^a-z0-9_]+/g, '_') .replace(/^_+|_+$/g, '') .slice(0, 64); } function fieldTypeLabel(type: DynamicCheckoutFieldType): string { switch (type) { case 'text': return __('Text', 'sikshya'); case 'textarea': return __('Textarea', 'sikshya'); case 'email': return __('Email', 'sikshya'); case 'tel': return __('Phone', 'sikshya'); case 'number': return __('Number', 'sikshya'); case 'select': return __('Select', 'sikshya'); case 'country': return __('Country', 'sikshya'); case 'radio': return __('Radio', 'sikshya'); case 'checkbox': return __('Checkbox', 'sikshya'); default: return type; } } export function DynamicFieldsBuilder(props: { value: unknown; onChange: (next: string) => void; readOnly?: boolean; }) { const { value, onChange, readOnly } = props; const schema = useMemo(() => safeJsonParseArray(value), [value]); const [activeId, setActiveId] = useState(null); const [rawError, setRawError] = useState(''); const [showAdd, setShowAdd] = useState(false); const [draggedId, setDraggedId] = useState(null); const [dragOverId, setDragOverId] = useState(null); const [newField, setNewField] = useState({ id: '', label: '', type: 'text', placeholder: '', enabled: true, width: 'full', required: false, help: '', options: [], persist_to_user: true, }); const [idTouched, setIdTouched] = useState(false); const update = (nextSchema: DynamicCheckoutField[]) => { setRawError(''); onChange(toSchemaString(nextSchema)); }; const ids = new Set(); for (const f of schema) { if (f && typeof f.id === 'string' && f.id) ids.add(f.id); } const validate = (nextSchema: DynamicCheckoutField[]) => { const nextIds = new Set(); for (const f of nextSchema) { const id = slugifyId(f?.id || ''); if (!id) return __('Each field must have an id.', 'sikshya'); if (nextIds.has(id)) return sprintf(__('Duplicate field id "%s".', 'sikshya'), id); nextIds.add(id); if ((f.type === 'select' || f.type === 'radio') && (!f.options || f.options.length < 1)) { return sprintf(__('Field "%s" needs at least one option.', 'sikshya'), id); } } return ''; }; const addField = () => { const rawId = (newField.id || '').trim() !== '' ? newField.id : newField.label; const id = slugifyId(rawId); if (!id || !newField.label) return; if (ids.has(id)) { setRawError(sprintf(__('Duplicate field id "%s".', 'sikshya'), id)); return; } const nextField: DynamicCheckoutField = { id, label: newField.label, type: newField.type, placeholder: newField.placeholder || '', enabled: newField.enabled !== false, width: newField.width || 'full', required: !!newField.required, help: newField.help || '', options: newField.type === 'select' || newField.type === 'radio' ? (newField.options || []).filter((o) => o.value && o.label) : [], visibility: newField.visibility, persist_to_user: newField.persist_to_user !== false, }; const next: DynamicCheckoutField[] = [...schema, nextField]; const err = validate(next); if (err) { setRawError(err); return; } update(next); setActiveId(id); setShowAdd(false); setNewField({ id: '', label: '', type: 'text', placeholder: '', enabled: true, width: 'full', required: false, help: '', options: [], persist_to_user: true, }); setIdTouched(false); }; const removeField = (id: string) => { const next = schema.filter((f) => slugifyId(f.id) !== slugifyId(id)); update(next); if (activeId === id) setActiveId(null); }; const moveByDrag = (dragId: string, dropId: string) => { const a = schema.findIndex((f) => slugifyId(f.id) === slugifyId(dragId)); const b = schema.findIndex((f) => slugifyId(f.id) === slugifyId(dropId)); if (a < 0 || b < 0 || a === b) return; const next = [...schema]; const [it] = next.splice(a, 1); next.splice(b, 0, it); update(next); }; const move = (id: string, dir: -1 | 1) => { const idx = schema.findIndex((f) => slugifyId(f.id) === slugifyId(id)); if (idx < 0) return; const j = idx + dir; if (j < 0 || j >= schema.length) return; const next = [...schema]; const t = next[idx]; next[idx] = next[j]; next[j] = t; update(next); }; const updateField = (id: string, patch: Partial) => { const next = schema.map((f) => (slugifyId(f.id) === slugifyId(id) ? { ...f, ...patch } : f)); const err = validate(next.map((f) => ({ ...f, id: slugifyId(f.id) }))); if (err) { setRawError(err); return; } update(next.map((f) => ({ ...f, id: slugifyId(f.id) }))); }; const active = activeId ? schema.find((f) => slugifyId(f.id) === slugifyId(activeId)) : null; return (
{__('Field builder', 'sikshya')}
{__('Drag to reorder, enable/disable, set width, options and visibility.', 'sikshya')}
{rawError ? (
{rawError}
) : null} {showAdd ? (
setNewField((p) => { const label = e.target.value; const next: DynamicCheckoutField = { ...p, label }; if (!idTouched) { next.id = slugifyId(label); } return next; }) } placeholder={__('e.g. Company name', 'sikshya')} />
{ setIdTouched(true); setNewField((p) => ({ ...p, id: e.target.value })); }} onBlur={() => { setNewField((p) => ({ ...p, id: slugifyId(p.id || p.label) })); }} placeholder={__('e.g. company_name', 'sikshya')} />
setNewField((p) => ({ ...p, placeholder: e.target.value }))} placeholder={__('Optional', 'sikshya')} />
{(newField.type === 'select' || newField.type === 'radio') && (
{__('Options', 'sikshya')}
{(newField.options || []).map((opt, idx) => (
{ const next = [...(newField.options || [])]; next[idx] = { ...next[idx], value: e.target.value }; setNewField((p) => ({ ...p, options: next })); }} placeholder={__('value', 'sikshya')} /> { const next = [...(newField.options || [])]; next[idx] = { ...next[idx], label: e.target.value }; setNewField((p) => ({ ...p, options: next })); }} placeholder={__('label', 'sikshya')} />
))}
)}
{__('Width', 'sikshya')}
) : null}
{schema.length === 0 ? (
{__('No fields yet. Click “Add field”.', 'sikshya')}
) : null} {schema.map((f, i) => { const id = slugifyId(f.id); const isActive = !!activeId && slugifyId(activeId) === id; const enabled = f.enabled !== false; const locked = !!f.locked || !!f.system; return (
); })}
{active ? (
{__('Edit field', 'sikshya')}
updateField(active.id, { id: e.target.value })} placeholder={__('e.g. company_name', 'sikshya')} />
{__('Used as key in orders/user meta. Lowercase + underscores recommended.', 'sikshya')}
updateField(active.id, { label: e.target.value })} placeholder={__('e.g. Company name', 'sikshya')} />
updateField(active.id, { placeholder: e.target.value })} placeholder={__('Optional', 'sikshya')} /> {active.type === 'select' || active.type === 'radio' || active.type === 'checkbox' || active.type === 'country' ? (
{__('Not used for this field type.', 'sikshya')}
) : null}