2026-04-05 18:31:16 -07:00
|
|
|
import React, { useCallback } from 'react';
|
|
|
|
|
import { useEditor } from '@craftjs/core';
|
|
|
|
|
import {
|
|
|
|
|
BG_COLORS,
|
|
|
|
|
SPACING_PRESETS,
|
|
|
|
|
RADIUS_PRESETS,
|
|
|
|
|
} from '../../../constants/presets';
|
|
|
|
|
import {
|
|
|
|
|
StylePanelProps,
|
|
|
|
|
SectionLabel,
|
|
|
|
|
ColorSwatchGrid,
|
|
|
|
|
PresetButtonGrid,
|
|
|
|
|
CollapsibleSection,
|
|
|
|
|
labelStyle,
|
|
|
|
|
inputStyle,
|
|
|
|
|
btnActiveStyle,
|
|
|
|
|
sectionGap,
|
|
|
|
|
} from './shared';
|
|
|
|
|
|
|
|
|
|
/* ---------- FORM ---------- */
|
|
|
|
|
export const FormStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => {
|
|
|
|
|
const { actions } = useEditor();
|
|
|
|
|
|
|
|
|
|
const setProp = useCallback((key: string, value: any) => {
|
|
|
|
|
actions.setProp(selectedId, (props: any) => { props[key] = value; });
|
|
|
|
|
}, [actions, selectedId]);
|
|
|
|
|
|
|
|
|
|
const setPropStyle = useCallback((key: string, value: string) => {
|
|
|
|
|
actions.setProp(selectedId, (props: any) => {
|
|
|
|
|
props.style = { ...props.style, [key]: value };
|
|
|
|
|
});
|
|
|
|
|
}, [actions, selectedId]);
|
|
|
|
|
|
|
|
|
|
const style = nodeProps.style || {};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
2026-07-07 14:06:24 -07:00
|
|
|
{/* Contact-form relay: where submissions are emailed. Present on ContactForm
|
|
|
|
|
and FormContainer (both have recipientEmail/thankYouUrl props). */}
|
|
|
|
|
{nodeProps.recipientEmail !== undefined && (
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={labelStyle}>Send submissions to (email)</label>
|
|
|
|
|
<input type="email" value={nodeProps.recipientEmail || ''} onChange={(e) => setProp('recipientEmail', e.target.value)} placeholder="you@example.com" style={inputStyle} />
|
|
|
|
|
<p style={{ fontSize: 10, color: '#71717a', margin: '4px 0 0' }}>
|
|
|
|
|
Emailed via the site's contact-form relay (an admin must enable it in Server Settings). Leave blank to use the Form Action URL instead.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{nodeProps.thankYouUrl !== undefined && (
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={labelStyle}>Thank-you page URL (optional)</label>
|
|
|
|
|
<input type="text" value={nodeProps.thankYouUrl || ''} onChange={(e) => setProp('thankYouUrl', e.target.value)} placeholder="/thank-you (blank = hosted page)" style={inputStyle} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-04-05 18:31:16 -07:00
|
|
|
{/* Form action/method */}
|
|
|
|
|
{nodeProps.action !== undefined && (
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={labelStyle}>Form Action URL</label>
|
|
|
|
|
<input type="text" value={nodeProps.action || ''} onChange={(e) => setProp('action', e.target.value)} placeholder="https://..." style={inputStyle} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{nodeProps.method !== undefined && (
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={labelStyle}>Method</label>
|
|
|
|
|
<div style={{ display: 'flex', gap: 4 }}>
|
|
|
|
|
{['GET', 'POST'].map((m) => (
|
|
|
|
|
<button key={m} onClick={() => setProp('method', m)} style={btnActiveStyle(nodeProps.method === m)}>{m}</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Input field props */}
|
|
|
|
|
{nodeProps.label !== undefined && (
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={labelStyle}>Label</label>
|
|
|
|
|
<input type="text" value={nodeProps.label || ''} onChange={(e) => setProp('label', e.target.value)} style={inputStyle} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{nodeProps.placeholder !== undefined && (
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={labelStyle}>Placeholder</label>
|
|
|
|
|
<input type="text" value={nodeProps.placeholder || ''} onChange={(e) => setProp('placeholder', e.target.value)} style={inputStyle} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{nodeProps.name !== undefined && (
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={labelStyle}>Field Name</label>
|
|
|
|
|
<input type="text" value={nodeProps.name || ''} onChange={(e) => setProp('name', e.target.value)} style={inputStyle} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{nodeProps.type !== undefined && typeof nodeProps.type === 'string' && (
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={labelStyle}>Input Type</label>
|
|
|
|
|
<select value={nodeProps.type} onChange={(e) => setProp('type', e.target.value)} style={{ ...inputStyle, cursor: 'pointer' }}>
|
|
|
|
|
{['text', 'email', 'password', 'number', 'tel', 'url'].map((t) => (
|
|
|
|
|
<option key={t} value={t}>{t}</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{nodeProps.required !== undefined && (
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
|
|
|
|
|
<input type="checkbox" checked={nodeProps.required || false} onChange={(e) => setProp('required', e.target.checked)} />
|
|
|
|
|
Required
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Button text */}
|
|
|
|
|
{nodeProps.text !== undefined && nodeProps.label === undefined && (
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={labelStyle}>Button Text</label>
|
|
|
|
|
<input type="text" value={nodeProps.text || ''} onChange={(e) => setProp('text', e.target.value)} style={inputStyle} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Subscribe form props */}
|
|
|
|
|
{nodeProps.buttonText !== undefined && (
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={labelStyle}>Button Text</label>
|
|
|
|
|
<input type="text" value={nodeProps.buttonText || ''} onChange={(e) => setProp('buttonText', e.target.value)} style={inputStyle} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{nodeProps.placeholderText !== undefined && (
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={labelStyle}>Placeholder Text</label>
|
|
|
|
|
<input type="text" value={nodeProps.placeholderText || ''} onChange={(e) => setProp('placeholderText', e.target.value)} style={inputStyle} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{nodeProps.successMessage !== undefined && (
|
|
|
|
|
<div style={sectionGap}>
|
|
|
|
|
<label style={labelStyle}>Success Message</label>
|
|
|
|
|
<input type="text" value={nodeProps.successMessage || ''} onChange={(e) => setProp('successMessage', e.target.value)} style={inputStyle} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Style */}
|
|
|
|
|
<CollapsibleSection title="Style" defaultOpen={false}>
|
|
|
|
|
<div className="guided-section">
|
|
|
|
|
<SectionLabel>Background</SectionLabel>
|
|
|
|
|
<ColorSwatchGrid colors={BG_COLORS} activeValue={style.backgroundColor} onSelect={(v: string) => setPropStyle('backgroundColor', v)} />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="guided-section">
|
|
|
|
|
<SectionLabel>Padding</SectionLabel>
|
|
|
|
|
<PresetButtonGrid presets={SPACING_PRESETS} activeValue={style.padding as string} onSelect={(v) => setPropStyle('padding', v)} />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="guided-section">
|
|
|
|
|
<SectionLabel>Border Radius</SectionLabel>
|
|
|
|
|
<PresetButtonGrid presets={RADIUS_PRESETS} activeValue={style.borderRadius as string} onSelect={(v) => setPropStyle('borderRadius', v)} />
|
|
|
|
|
</div>
|
|
|
|
|
</CollapsibleSection>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|