File size: 3,421 Bytes
50eef7d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | import type { DomainPreferenceForm, PreferenceField } from '../utils/api'
type Props = {
form: DomainPreferenceForm
values: Record<string, any>
onChange: (values: Record<string, any>) => void
}
function asArray(value: unknown): string[] {
if (Array.isArray(value)) return value.map(String)
if (typeof value === 'string' && value.trim()) {
return value.split(',').map(part => part.trim()).filter(Boolean)
}
return []
}
/**
* Generic renderer for a server-generated preference form. Adding a domain's
* form is a backend data change; this component renders whatever fields arrive.
*/
export default function PreferenceForm({ form, values, onChange }: Props) {
const setValue = (key: string, value: unknown) => onChange({ ...values, [key]: value })
return (
<div style={{ display: 'grid', gap: 10 }}>
{form.fields.map(field => (
<div key={field.key} style={{ display: 'grid', gap: 4 }}>
<label style={{ fontSize: 13, color: 'var(--muted, #666)' }}>
{field.label}
{field.required && <span style={{ color: 'var(--danger, #c0392b)' }}> *</span>}
</label>
{renderField(field, values[field.key], setValue)}
</div>
))}
</div>
)
}
function renderField(
field: PreferenceField,
value: unknown,
setValue: (key: string, value: unknown) => void,
) {
if (field.type === 'multiselect') {
const selected = asArray(value)
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{field.options.map(option => {
const active = selected.includes(option)
return (
<button
type="button"
key={option}
aria-pressed={active}
onClick={() =>
setValue(
field.key,
active ? selected.filter(item => item !== option) : [...selected, option],
)
}
style={chipStyle(active)}
>
{option}
</button>
)
})}
</div>
)
}
if (field.type === 'select') {
return (
<select
aria-label={field.label}
value={typeof value === 'string' ? value : ''}
onChange={e => setValue(field.key, e.target.value)}
style={inputStyle}
>
<option value="">—</option>
{field.options.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
)
}
// text (default)
return (
<input
aria-label={field.label}
value={typeof value === 'string' ? value : Array.isArray(value) ? value.join(', ') : ''}
placeholder={field.placeholder}
onChange={e => setValue(field.key, e.target.value)}
style={inputStyle}
/>
)
}
function chipStyle(active: boolean): React.CSSProperties {
return {
padding: '4px 10px',
borderRadius: 999,
border: `1px solid ${active ? 'var(--primary, #e8742c)' : 'var(--border, #ddd)'}`,
background: active ? 'var(--primary, #e8742c)' : 'transparent',
color: active ? '#fff' : 'inherit',
cursor: 'pointer',
fontSize: 13,
}
}
const inputStyle: React.CSSProperties = {
width: '100%',
padding: '8px 10px',
borderRadius: 8,
border: '1px solid var(--border, #ddd)',
background: 'var(--bg, #fff)',
color: 'inherit',
fontSize: 14,
}
|