diff --git a/craft/src/panels/right/styles/FeaturesEditor.tsx b/craft/src/panels/right/styles/FeaturesEditor.tsx new file mode 100644 index 0000000..f1ab0f3 --- /dev/null +++ b/craft/src/panels/right/styles/FeaturesEditor.tsx @@ -0,0 +1,88 @@ +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 { + 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>({}); + + 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 ( +
+ {list.map((feat, i) => ( +
+
+ update(i, 'title', e.target.value)} placeholder="Title" style={{ ...inputStyle, flex: 1 }} /> + +
+