198 lines
8.4 KiB
TypeScript
198 lines
8.4 KiB
TypeScript
|
|
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,
|
||
|
|
ColorPickerField,
|
||
|
|
ArrayPropEditor,
|
||
|
|
labelStyle,
|
||
|
|
inputStyle,
|
||
|
|
smallInputStyle,
|
||
|
|
sectionGap,
|
||
|
|
} from './shared';
|
||
|
|
|
||
|
|
/* ---------- MEDIA (Video / Gallery / Map / Slider) ---------- */
|
||
|
|
export const MediaStylePanel: 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 (
|
||
|
|
<>
|
||
|
|
{/* Video URL */}
|
||
|
|
{nodeProps.videoUrl !== undefined && (
|
||
|
|
<div style={sectionGap}>
|
||
|
|
<label style={labelStyle}>Video URL</label>
|
||
|
|
<input type="text" value={nodeProps.videoUrl || ''} onChange={(e) => setProp('videoUrl', e.target.value)} placeholder="YouTube, Vimeo, or .mp4 URL" style={inputStyle} />
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Map URL */}
|
||
|
|
{nodeProps.embedUrl !== undefined && nodeProps.videoUrl === undefined && (
|
||
|
|
<div style={sectionGap}>
|
||
|
|
<label style={labelStyle}>Embed URL</label>
|
||
|
|
<input type="text" value={nodeProps.embedUrl || ''} onChange={(e) => setProp('embedUrl', e.target.value)} placeholder="Google Maps embed URL" style={inputStyle} />
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Map address */}
|
||
|
|
{nodeProps.address !== undefined && (
|
||
|
|
<div style={sectionGap}>
|
||
|
|
<label style={labelStyle}>Address</label>
|
||
|
|
<input type="text" value={nodeProps.address || ''} onChange={(e) => setProp('address', e.target.value)} placeholder="123 Main St..." style={inputStyle} />
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Video options */}
|
||
|
|
{nodeProps.autoplay !== undefined && (
|
||
|
|
<div style={sectionGap}>
|
||
|
|
<label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
|
||
|
|
<input type="checkbox" checked={nodeProps.autoplay || false} onChange={(e) => setProp('autoplay', e.target.checked)} />
|
||
|
|
Autoplay
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{nodeProps.loop !== undefined && (
|
||
|
|
<div style={sectionGap}>
|
||
|
|
<label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
|
||
|
|
<input type="checkbox" checked={nodeProps.loop || false} onChange={(e) => setProp('loop', e.target.checked)} />
|
||
|
|
Loop
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{nodeProps.controls !== undefined && (
|
||
|
|
<div style={sectionGap}>
|
||
|
|
<label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
|
||
|
|
<input type="checkbox" checked={nodeProps.controls !== false} onChange={(e) => setProp('controls', e.target.checked)} />
|
||
|
|
Show Controls
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Gallery items */}
|
||
|
|
{nodeProps.images !== undefined && Array.isArray(nodeProps.images) && (
|
||
|
|
<CollapsibleSection title="Images">
|
||
|
|
<ArrayPropEditor
|
||
|
|
selectedId={selectedId}
|
||
|
|
propKey="images"
|
||
|
|
items={nodeProps.images}
|
||
|
|
renderItem={(item: any, index: number) => (
|
||
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||
|
|
{item.src !== undefined && (
|
||
|
|
<input type="text" value={item.src || ''} onChange={(e) => {
|
||
|
|
actions.setProp(selectedId, (props: any) => {
|
||
|
|
const updated = [...(props.images || [])];
|
||
|
|
updated[index] = { ...updated[index], src: e.target.value };
|
||
|
|
props.images = updated;
|
||
|
|
});
|
||
|
|
}} placeholder="Image URL" style={smallInputStyle} />
|
||
|
|
)}
|
||
|
|
{item.caption !== undefined && (
|
||
|
|
<input type="text" value={item.caption || ''} onChange={(e) => {
|
||
|
|
actions.setProp(selectedId, (props: any) => {
|
||
|
|
const updated = [...(props.images || [])];
|
||
|
|
updated[index] = { ...updated[index], caption: e.target.value };
|
||
|
|
props.images = updated;
|
||
|
|
});
|
||
|
|
}} placeholder="Caption" style={smallInputStyle} />
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
emptyItem={{ src: '', caption: '' }}
|
||
|
|
/>
|
||
|
|
</CollapsibleSection>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Slides */}
|
||
|
|
{nodeProps.slides !== undefined && Array.isArray(nodeProps.slides) && (
|
||
|
|
<CollapsibleSection title="Slides">
|
||
|
|
<ArrayPropEditor
|
||
|
|
selectedId={selectedId}
|
||
|
|
propKey="slides"
|
||
|
|
items={nodeProps.slides}
|
||
|
|
renderItem={(item: any, index: number) => (
|
||
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||
|
|
{item.heading !== undefined && (
|
||
|
|
<input type="text" value={item.heading || ''} onChange={(e) => {
|
||
|
|
actions.setProp(selectedId, (props: any) => {
|
||
|
|
const updated = [...(props.slides || [])];
|
||
|
|
updated[index] = { ...updated[index], heading: e.target.value };
|
||
|
|
props.slides = updated;
|
||
|
|
});
|
||
|
|
}} placeholder="Heading" style={smallInputStyle} />
|
||
|
|
)}
|
||
|
|
{item.text !== undefined && (
|
||
|
|
<textarea value={item.text || ''} onChange={(e) => {
|
||
|
|
actions.setProp(selectedId, (props: any) => {
|
||
|
|
const updated = [...(props.slides || [])];
|
||
|
|
updated[index] = { ...updated[index], text: e.target.value };
|
||
|
|
props.slides = updated;
|
||
|
|
});
|
||
|
|
}} placeholder="Text" rows={2} style={{ ...smallInputStyle, resize: 'vertical' }} />
|
||
|
|
)}
|
||
|
|
{item.image !== undefined && (
|
||
|
|
<input type="text" value={item.image || ''} onChange={(e) => {
|
||
|
|
actions.setProp(selectedId, (props: any) => {
|
||
|
|
const updated = [...(props.slides || [])];
|
||
|
|
updated[index] = { ...updated[index], image: e.target.value };
|
||
|
|
props.slides = updated;
|
||
|
|
});
|
||
|
|
}} placeholder="Image URL" style={smallInputStyle} />
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
emptyItem={{ heading: 'New Slide', text: '', image: '' }}
|
||
|
|
/>
|
||
|
|
</CollapsibleSection>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Overlay */}
|
||
|
|
{nodeProps.overlayColor !== undefined && (
|
||
|
|
<CollapsibleSection title="Overlay" defaultOpen={false}>
|
||
|
|
<ColorPickerField label="Color" value={nodeProps.overlayColor || '#000000'} onChange={(v) => setProp('overlayColor', v)} />
|
||
|
|
{nodeProps.overlayOpacity !== undefined && (
|
||
|
|
<div style={sectionGap}>
|
||
|
|
<label style={labelStyle}>Opacity: {nodeProps.overlayOpacity ?? 0}%</label>
|
||
|
|
<input type="range" min={0} max={100} value={nodeProps.overlayOpacity ?? 0} onChange={(e) => setProp('overlayOpacity', parseInt(e.target.value))} style={{ width: '100%' }} />
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</CollapsibleSection>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Background & padding */}
|
||
|
|
<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>
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
};
|