Files
site-builder/craft/src/panels/right/styles/FeaturesEditor.tsx
T

89 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useRef } from 'react';
import { useEditor } from '@craftjs/core';
import { labelStyle, inputStyle, sectionGap } from './shared';
/* Upload helper (same contract as ImageBlock/Navbar): uploads to WHP if a
WHP_CONFIG is present, otherwise falls back to a local blob URL. */
async function uploadToWhp(file: File): Promise<string | null> {
const cfg = (window as any).WHP_CONFIG;
if (!cfg) return URL.createObjectURL(file);
const fd = new FormData();
fd.append('file', file);
try {
const resp = await fetch(`${cfg.apiUrl}?action=upload_asset&site_id=${cfg.siteId}`, {
method: 'POST', headers: { 'X-CSRF-Token': cfg.csrfToken }, body: fd,
});
const data = await resp.json();
return data.success && data.url ? data.url : null;
} catch { return null; }
}
interface Feature {
title?: string; description?: string; icon?: string;
image?: string; imageAlt?: string; buttonText?: string; buttonUrl?: string;
}
/**
* Dedicated per-feature editor for the FeaturesGrid guided panel. Unlike the
* generic array editor (which only shows fields already present on the item),
* this always exposes icon / image (upload or URL) / button controls, so it
* works on template-created features that lack those keys. An image renders in
* place of the icon whenever `image` is set.
*/
export const FeaturesEditor: React.FC<{ selectedId: string; features: unknown }> = ({ selectedId, features }) => {
const { actions } = useEditor();
const list: Feature[] = Array.isArray(features) ? (features as Feature[]) : [];
const fileRefs = useRef<Record<number, HTMLInputElement | null>>({});
const mutate = (fn: (arr: Feature[]) => Feature[]) =>
actions.setProp(selectedId, (p: any) => { p.features = fn([...(p.features || [])]); });
const update = (i: number, field: keyof Feature, value: string) =>
mutate((arr) => { arr[i] = { ...arr[i], [field]: value }; return arr; });
const add = () =>
mutate((arr) => [...arr, { title: 'New Feature', description: 'Describe this feature.', icon: '🔧', image: '', imageAlt: '', buttonText: '', buttonUrl: '' }]);
const remove = (i: number) => mutate((arr) => { arr.splice(i, 1); return arr; });
const onUpload = async (i: number, file: File) => { const url = await uploadToWhp(file); if (url) update(i, 'image', url); };
const smallBtn: React.CSSProperties = { padding: '4px 8px', fontSize: 11, borderRadius: 4, cursor: 'pointer', border: '1px solid #3f3f46', background: '#27272a', color: '#e4e4e7' };
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{list.map((feat, i) => (
<div key={i} style={{ background: '#1e1e22', borderRadius: 6, padding: 8, display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<input type="text" value={feat.title || ''} onChange={(e) => update(i, 'title', e.target.value)} placeholder="Title" style={{ ...inputStyle, flex: 1 }} />
<button onClick={() => remove(i)} title="Remove" style={{ padding: '2px 8px', fontSize: 11, background: '#ef4444', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer', flex: 'none' }}>×</button>
</div>
<textarea value={feat.description || ''} onChange={(e) => update(i, 'description', e.target.value)} placeholder="Description" rows={2} style={{ ...inputStyle, resize: 'vertical' }} />
{/* Media: image (upload/URL) takes precedence over the icon when set */}
{feat.image ? (
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<img src={feat.image} alt="" style={{ width: 28, height: 28, objectFit: 'cover', borderRadius: 4, border: '1px solid #3f3f46' }} />
<input type="text" value={feat.image} onChange={(e) => update(i, 'image', e.target.value)} placeholder="Image URL" style={{ ...inputStyle, flex: 1 }} />
<button onClick={() => update(i, 'image', '')} title="Use icon instead" style={smallBtn}>Icon</button>
</div>
) : (
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<input type="text" value={feat.icon || ''} onChange={(e) => update(i, 'icon', e.target.value)} placeholder="Icon (emoji)" style={{ ...inputStyle, width: 60, flex: 'none', textAlign: 'center' }} />
<button onClick={() => fileRefs.current[i]?.click()} style={{ ...smallBtn, flex: 1 }}> Upload image</button>
<input type="text" value={feat.image || ''} onChange={(e) => update(i, 'image', e.target.value)} placeholder="or image URL" style={{ ...inputStyle, flex: 1 }} />
</div>
)}
<input ref={(el) => { fileRefs.current[i] = el; }} type="file" accept="image/*" style={{ display: 'none' }}
onChange={(e) => { const f = e.target.files?.[0]; if (f) onUpload(i, f); e.target.value = ''; }} />
{/* Optional button */}
<div style={{ display: 'flex', gap: 4 }}>
<input type="text" value={feat.buttonText || ''} onChange={(e) => update(i, 'buttonText', e.target.value)} placeholder="Button text (optional)" style={{ ...inputStyle, flex: 1 }} />
<input type="text" value={feat.buttonUrl || ''} onChange={(e) => update(i, 'buttonUrl', e.target.value)} placeholder="Button URL" style={{ ...inputStyle, flex: 1 }} />
</div>
</div>
))}
<button onClick={add} style={{ ...labelStyle, padding: '6px', fontSize: 11, background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer', textAlign: 'center' }}>
+ Add Feature
</button>
</div>
);
};