import type { Dispatch, ReactNode, SetStateAction } from 'react'; import type { SettingsField } from '../types/settingsSchema'; import { DynamicFieldsBuilder } from '../components/settings/DynamicFieldsBuilder'; import { PERMALINK_SETTINGS_PREVIEW_KEYS, previewUrlForPermalinkField } from '../lib/permalinks'; import { __ } from '../lib/i18n'; export function fieldToStringValue(v: unknown): string { if (v === null || v === undefined) return ''; return String(v); } export function isTruthyCheckboxValue(v: unknown): boolean { return v === true || v === 1 || v === '1' || v === 'yes' || v === 'on'; } function renderDescription(desc: string): ReactNode { const raw = String(desc || '').trim(); if (!raw) return null; // Allow only {__('text', 'sikshya')} from server-provided schema descriptions. // Everything else is rendered as plain text (with URL linkify). if (raw.includes('${raw}`, 'text/html'); const root = doc.body.firstElementChild; if (!root) return raw; const out: ReactNode[] = []; const walk = (node: ChildNode) => { if (node.nodeType === Node.TEXT_NODE) { const t = node.textContent || ''; if (t) out.push(t); return; } if (node.nodeType !== Node.ELEMENT_NODE) return; const el = node as HTMLElement; if (el.tagName.toLowerCase() === 'a') { const href = String(el.getAttribute('href') || '').trim(); const text = String(el.textContent || href || '').trim(); if (!href || !/^https?:\/\//i.test(href)) { if (text) out.push(text); return; } out.push( {text || href} ); return; } // Any other element: render its text content only. const t = el.textContent || ''; if (t) out.push(t); }; root.childNodes.forEach((n) => walk(n)); return <>{out}; } catch { // Fall through to linkify. } } // Linkify plain URLs. const parts = raw.split(/(https?:\/\/[^\s)]+)\b/g); if (parts.length <= 1) return raw; return ( <> {parts.map((p, idx) => { const isUrl = /^https?:\/\//i.test(p); if (!isUrl) return {p}; return ( {p} ); })} ); } /** * Wrap a locked field in a disabled overlay with a "Pro" pill and a friendly * reason. Shared between the Settings page, the Email page, and any consumer * that uses `renderSettingsField`. */ function LockedFieldOverlay(props: { f: SettingsField; children: ReactNode; className?: string }) { const { f, children, className = '' } = props; const reason = f.locked_reason || 'Available on a higher Sikshya Pro plan.'; const addonLabel = f.required_addon_label || f.required_addon || ''; const planLabel = f.required_plan_label || ''; return (
{children}
Pro{planLabel ? ` • ${planLabel}` : ''} {addonLabel ? ( <> {__('Addon:', 'sikshya')} {addonLabel} ) : null} {reason}
); } /** * Shared field renderer for Settings and the dedicated Email admin page. */ export function renderSettingsField( draft: Record, setDraft: Dispatch>>, f: SettingsField ) { const k = f.key; const type = f.type || 'text'; const cur = draft[k]; const label = f.label || k; const desc = f.description || ''; const locked = !!f.locked; // When a field is Pro-locked we render read-only controls so the user can // still see the (default) shape of the field but cannot modify it. const readOnly = locked; const onChangeGuard = (cb: (v: T) => void) => (v: T) => { if (readOnly) return; cb(v); }; let body: ReactNode; if (type === 'dynamic_fields_builder') { body = (
{desc ? (

{renderDescription(desc)}

) : null}
((v) => setDraft((p) => ({ ...p, [k]: v })))} />
); } else if (type === 'checkbox') { const checked = isTruthyCheckboxValue(cur); body = (
); } else if (type === 'select') { const opts = f.options || {}; const optKeys = Object.keys(opts).map((x) => String(x)); const ph = f.select_placeholder; const raw = fieldToStringValue(cur ?? f.default ?? ''); const selectValue = ph && (raw === '' || !optKeys.includes(raw)) ? '' : raw; body = (
{desc ? (

{renderDescription(desc)}

) : null}
); } else if (type === 'multi_select') { const opts = f.options || {}; const allowed = Object.keys(opts).map((x) => String(x)); const raw = fieldToStringValue(cur ?? f.default ?? ''); const selected = raw .split(',') .map((x) => String(x || '').trim()) .filter((x) => x !== '') .filter((x, i, a) => a.indexOf(x) === i) .filter((x) => allowed.includes(x)); const setSelected = (vals: string[]) => { const normalized = vals .map((x) => String(x || '').trim()) .filter((x) => x !== '' && allowed.includes(x)) .filter((x, i, a) => a.indexOf(x) === i); setDraft((p) => ({ ...p, [k]: normalized.join(',') })); }; body = (
{desc ? (

{renderDescription(desc)}

) : null} {selected.length ? (
{selected.map((v) => ( {opts[v] || v} {!readOnly ? ( ) : null} ))}
) : (

Select one or more methods. The value is stored as a comma-separated list.

)}
); } else if (type === 'color') { const hex = (() => { const s = fieldToStringValue(cur ?? f.default ?? '#000000'); return /^#[0-9A-Fa-f]{6}$/.test(s) ? s : '#000000'; })(); body = (
{desc ? (

{renderDescription(desc)}

) : null}
onChangeGuard((v) => setDraft((p) => ({ ...p, [k]: v })))(e.target.value)} aria-label={label} /> {hex}
); } else if (type === 'textarea') { body = (
{desc ? (

{renderDescription(desc)}

) : null}