Files
site-builder/craft/src/components/forms/ContactForm.tsx
T

424 lines
16 KiB
TypeScript
Raw Normal View History

import React, { CSSProperties } from 'react';
import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers';
interface ContactFormField {
type: 'text' | 'email' | 'tel' | 'textarea' | 'select';
label: string;
name: string;
placeholder: string;
required: boolean;
options?: string[];
}
interface ContactFormProps {
fields?: ContactFormField[];
submitText?: string;
submitColor?: string;
formAction?: string;
successMessage?: string;
style?: CSSProperties;
labelColor?: string;
inputBg?: string;
inputBorder?: string;
}
const defaultFields: ContactFormField[] = [
{ type: 'text', label: 'Name', name: 'name', placeholder: 'Your name', required: true },
{ type: 'email', label: 'Email', name: 'email', placeholder: 'your@email.com', required: true },
{ type: 'tel', label: 'Phone', name: 'phone', placeholder: '(555) 123-4567', required: false },
{ type: 'textarea', label: 'Message', name: 'message', placeholder: 'How can we help you?', required: true },
];
export const ContactForm: UserComponent<ContactFormProps> = ({
fields = defaultFields,
submitText = 'Send Message',
submitColor = '#3b82f6',
formAction = '#',
successMessage = 'Thank you! We\'ll get back to you soon.',
style = {},
labelColor = '#374151',
inputBg = '#ffffff',
inputBorder = '#d1d5db',
}) => {
const {
connectors: { connect, drag },
selected,
} = useNode((node) => ({
selected: node.events.selected,
}));
const inputBaseStyle: CSSProperties = {
width: '100%',
padding: '10px 14px',
fontSize: '14px',
fontFamily: 'Inter, sans-serif',
border: `1px solid ${inputBorder}`,
borderRadius: '6px',
backgroundColor: inputBg,
color: '#1f2937',
boxSizing: 'border-box',
outline: 'none',
};
return (
<form
ref={(ref: HTMLFormElement | null): void => { if (ref) connect(drag(ref)); }}
action={formAction}
method="POST"
onSubmit={(e) => e.preventDefault()}
style={{
padding: '32px',
display: 'flex',
flexDirection: 'column',
gap: '20px',
outline: selected ? '2px solid #3b82f6' : 'none',
...style,
}}
>
{fields.map((field, i) => (
<div key={i} style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
<label style={{ fontSize: '14px', fontWeight: '500', color: labelColor }}>
{field.label}
{field.required && <span style={{ color: '#ef4444', marginLeft: '2px' }}>*</span>}
</label>
{field.type === 'textarea' ? (
<textarea
name={field.name}
placeholder={field.placeholder}
required={field.required}
rows={4}
style={{ ...inputBaseStyle, resize: 'vertical' }}
/>
) : field.type === 'select' ? (
<select
name={field.name}
required={field.required}
style={{ ...inputBaseStyle, cursor: 'pointer' }}
>
<option value="">{field.placeholder || 'Select...'}</option>
{(field.options || []).map((opt, j) => (
<option key={j} value={opt}>{opt}</option>
))}
</select>
) : (
<input
type={field.type}
name={field.name}
placeholder={field.placeholder}
required={field.required}
style={inputBaseStyle}
/>
)}
</div>
))}
<button
type="submit"
style={{
padding: '12px 32px',
fontSize: '16px',
fontWeight: '600',
fontFamily: 'Inter, sans-serif',
color: '#ffffff',
backgroundColor: submitColor,
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
alignSelf: 'flex-start',
}}
>
{submitText}
</button>
</form>
);
};
/* ---------- Settings panel ---------- */
const fieldTypes: ContactFormField['type'][] = ['text', 'email', 'tel', 'textarea', 'select'];
const ContactFormSettings: React.FC = () => {
const { actions: { setProp }, props } = useNode((node) => ({
props: node.data.props as ContactFormProps,
}));
const fields = props.fields || defaultFields;
const inputStyle: CSSProperties = {
width: '100%', padding: '3px 6px', background: '#27272a', color: '#e4e4e7',
border: '1px solid #3f3f46', borderRadius: 4, fontSize: 11,
};
const updateField = (index: number, key: keyof ContactFormField, value: any) => {
setProp((p: ContactFormProps) => {
const updated = [...(p.fields || defaultFields)];
updated[index] = { ...updated[index], [key]: value };
p.fields = updated;
});
};
const addField = () => {
setProp((p: ContactFormProps) => {
p.fields = [...(p.fields || defaultFields), { type: 'text', label: 'New Field', name: 'new_field', placeholder: '', required: false }];
});
};
const removeField = (index: number) => {
setProp((p: ContactFormProps) => {
const updated = [...(p.fields || defaultFields)];
updated.splice(index, 1);
p.fields = updated;
});
};
const submitColorPresets = ['#3b82f6', '#10b981', '#ef4444', '#8b5cf6', '#f59e0b', '#18181b', '#0ea5e9', '#ec4899'];
return (
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
{/* Form Action */}
<div>
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Form Action URL</label>
<input
type="text"
value={props.formAction || ''}
onChange={(e) => setProp((p: ContactFormProps) => { p.formAction = e.target.value; })}
placeholder="https://... or /api/submit"
style={{ ...inputStyle, padding: '4px 8px', fontSize: 12 }}
/>
</div>
{/* Success Message */}
<div>
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Success Message</label>
<input
type="text"
value={props.successMessage || ''}
onChange={(e) => setProp((p: ContactFormProps) => { p.successMessage = e.target.value; })}
style={{ ...inputStyle, padding: '4px 8px', fontSize: 12 }}
/>
</div>
{/* Submit Button */}
<div>
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Submit Button Text</label>
<input
type="text"
value={props.submitText || ''}
onChange={(e) => setProp((p: ContactFormProps) => { p.submitText = e.target.value; })}
style={{ ...inputStyle, padding: '4px 8px', fontSize: 12 }}
/>
</div>
<div>
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Submit Button Color</label>
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{submitColorPresets.map((c) => (
<button
key={c}
onClick={() => setProp((p: ContactFormProps) => { p.submitColor = c; })}
style={{
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
backgroundColor: c, cursor: 'pointer',
outline: props.submitColor === c ? '2px solid #3b82f6' : 'none',
outlineOffset: 1,
}}
/>
))}
</div>
</div>
{/* Label Color */}
<div>
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Label Color</label>
<input
type="color"
value={props.labelColor || '#374151'}
onChange={(e) => setProp((p: ContactFormProps) => { p.labelColor = e.target.value; })}
style={{ width: 32, height: 24, border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer', background: 'none', padding: 0 }}
/>
</div>
{/* Input Background */}
<div>
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Input Background</label>
<input
type="color"
value={props.inputBg || '#ffffff'}
onChange={(e) => setProp((p: ContactFormProps) => { p.inputBg = e.target.value; })}
style={{ width: 32, height: 24, border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer', background: 'none', padding: 0 }}
/>
</div>
{/* Input Border */}
<div>
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Input Border Color</label>
<input
type="color"
value={props.inputBorder || '#d1d5db'}
onChange={(e) => setProp((p: ContactFormProps) => { p.inputBorder = e.target.value; })}
style={{ width: 32, height: 24, border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer', background: 'none', padding: 0 }}
/>
</div>
{/* Fields Editor */}
<div>
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Fields</label>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{fields.map((field, i) => (
<div key={i} style={{ background: '#1e1e22', borderRadius: 6, padding: 8, display: 'flex', flexDirection: 'column', gap: 4 }}>
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<select
value={field.type}
onChange={(e) => updateField(i, 'type', e.target.value)}
style={{ ...inputStyle, width: 70, flex: 'none', cursor: 'pointer' }}
>
{fieldTypes.map((t) => <option key={t} value={t}>{t}</option>)}
</select>
<input
type="text"
value={field.label}
onChange={(e) => updateField(i, 'label', e.target.value)}
placeholder="Label"
style={{ ...inputStyle, flex: 1 }}
/>
<button
onClick={() => removeField(i)}
style={{ padding: '2px 6px', fontSize: 11, background: '#ef4444', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer', flex: 'none' }}
>
X
</button>
</div>
<div style={{ display: 'flex', gap: 4 }}>
<input
type="text"
value={field.name}
onChange={(e) => updateField(i, 'name', e.target.value)}
placeholder="name attr"
style={{ ...inputStyle, flex: 1 }}
/>
<input
type="text"
value={field.placeholder}
onChange={(e) => updateField(i, 'placeholder', e.target.value)}
placeholder="Placeholder"
style={{ ...inputStyle, flex: 1 }}
/>
</div>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<label style={{ fontSize: 10, color: '#a1a1aa', display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer' }}>
<input
type="checkbox"
checked={field.required}
onChange={(e) => updateField(i, 'required', e.target.checked)}
/>
Required
</label>
</div>
{field.type === 'select' && (
<div>
<label style={{ fontSize: 10, color: '#a1a1aa', display: 'block', marginBottom: 2 }}>Options (one per line)</label>
<textarea
value={(field.options || []).join('\n')}
onChange={(e) => updateField(i, 'options', e.target.value.split('\n').filter((s: string) => s.trim()))}
rows={3}
placeholder="Option 1&#10;Option 2&#10;Option 3"
style={{ ...inputStyle, resize: 'vertical' }}
/>
</div>
)}
</div>
))}
</div>
<button
onClick={addField}
style={{ marginTop: 6, width: '100%', padding: '6px', fontSize: 11, background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer' }}
>
+ Add Field
</button>
</div>
</div>
);
};
/* ---------- Craft config ---------- */
ContactForm.craft = {
displayName: 'Contact Form',
props: {
fields: defaultFields,
submitText: 'Send Message',
submitColor: '#3b82f6',
formAction: '#',
successMessage: 'Thank you! We\'ll get back to you soon.',
style: {
backgroundColor: '#ffffff',
borderRadius: '12px',
border: '1px solid #e5e7eb',
},
labelColor: '#374151',
inputBg: '#ffffff',
inputBorder: '#d1d5db',
},
rules: {
canDrag: () => true,
canMoveIn: () => false,
canMoveOut: () => true,
},
related: {
settings: ContactFormSettings,
},
};
/* ---------- HTML export ---------- */
(ContactForm as any).toHtml = (props: ContactFormProps, _childrenHtml: string) => {
const esc = (s: string) => s.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const formStyle = cssPropsToString({
padding: '32px',
display: 'flex',
flexDirection: 'column',
gap: '20px',
...props.style,
});
const labelColor = props.labelColor || '#374151';
const inputBg = props.inputBg || '#ffffff';
const inputBorder = props.inputBorder || '#d1d5db';
const inputStyleStr = `width:100%;padding:10px 14px;font-size:14px;font-family:Inter,sans-serif;border:1px solid ${inputBorder};border-radius:6px;background-color:${inputBg};color:#1f2937;box-sizing:border-box;outline:none`;
const fieldsHtml = (props.fields || defaultFields).map((field) => {
const reqStar = field.required ? '<span style="color:#ef4444;margin-left:2px">*</span>' : '';
const labelHtml = `<label style="font-size:14px;font-weight:500;color:${labelColor}">${esc(field.label)}${reqStar}</label>`;
const reqAttr = field.required ? ' required' : '';
let inputHtml = '';
if (field.type === 'textarea') {
inputHtml = `<textarea name="${esc(field.name)}" placeholder="${esc(field.placeholder)}" rows="4" style="${inputStyleStr};resize:vertical"${reqAttr}></textarea>`;
} else if (field.type === 'select') {
const opts = (field.options || []).map((o) => `<option value="${esc(o)}">${esc(o)}</option>`).join('');
inputHtml = `<select name="${esc(field.name)}" style="${inputStyleStr};cursor:pointer"${reqAttr}><option value="">${esc(field.placeholder || 'Select...')}</option>${opts}</select>`;
} else {
inputHtml = `<input type="${field.type}" name="${esc(field.name)}" placeholder="${esc(field.placeholder)}" style="${inputStyleStr}"${reqAttr} />`;
}
return `<div style="display:flex;flex-direction:column;gap:6px">${labelHtml}${inputHtml}</div>`;
}).join('\n ');
const btnStyle = cssPropsToString({
padding: '12px 32px',
fontSize: '16px',
fontWeight: '600',
fontFamily: 'Inter, sans-serif',
color: '#ffffff',
backgroundColor: props.submitColor || '#3b82f6',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
alignSelf: 'flex-start',
});
return {
html: `<form action="${esc(props.formAction || '#')}" method="POST"${formStyle ? ` style="${formStyle}"` : ''}>
${fieldsHtml}
<button type="submit"${btnStyle ? ` style="${btnStyle}"` : ''}>${esc(props.submitText || 'Send Message')}</button>
</form>`,
};
};