feat(builder): unify StylePanel image fields on AssetPicker
Replace the ad-hoc upload/browse/URL blocks in ImageStylePanel, HeroStylePanel (bgImage + bgVideo), BackgroundSectionStylePanel, NavStylePanel (standalone Logo imageSrc + Navbar logoImage), MediaStylePanel (Gallery/ContentSlider array items), and FeaturesEditor (per-feature image) with the shared AssetPicker component, dropping each panel's duplicated local showBrowser/browserAssets/handleUpload state and inline asset-browser JSX. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,101 +1,24 @@
|
||||
import React, { useCallback, useState, useRef } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useEditor } from '@craftjs/core';
|
||||
import {
|
||||
StylePanelProps,
|
||||
CollapsibleSection,
|
||||
ColorPickerField,
|
||||
uploadToWhp,
|
||||
labelStyle,
|
||||
inputStyle,
|
||||
smallInputStyle,
|
||||
btnActiveStyle,
|
||||
sectionGap,
|
||||
} from './shared';
|
||||
|
||||
/* ---------- Asset Browser Inline ---------- */
|
||||
const AssetBrowser: React.FC<{
|
||||
filter: 'image' | 'video' | 'all';
|
||||
onSelect: (url: string) => void;
|
||||
}> = ({ filter, onSelect }) => {
|
||||
const [assets, setAssets] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const loadAssets = useCallback(async () => {
|
||||
const cfg = (window as any).WHP_CONFIG;
|
||||
if (!cfg) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await fetch(`${cfg.apiUrl}?action=list_assets&site_id=${cfg.siteId}`);
|
||||
const data = await resp.json();
|
||||
if (data.success && Array.isArray(data.assets)) {
|
||||
const filtered = filter === 'all' ? data.assets : data.assets.filter((a: any) => {
|
||||
const t = (a.type || '').toLowerCase();
|
||||
return filter === 'image' ? t.startsWith('image') : t.startsWith('video');
|
||||
});
|
||||
setAssets(filtered);
|
||||
}
|
||||
} catch (e) { console.error('Load assets failed:', e); }
|
||||
setLoading(false);
|
||||
}, [filter]);
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
if (!open) loadAssets();
|
||||
setOpen(!open);
|
||||
}, [open, loadAssets]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={handleToggle} style={{
|
||||
...btnActiveStyle(open), width: '100%', marginTop: 4,
|
||||
}}>
|
||||
<i className={`fa ${loading ? 'fa-spinner fa-spin' : 'fa-folder-open'}`} style={{ marginRight: 4 }} />
|
||||
{open ? 'Close' : 'Browse Assets'}
|
||||
</button>
|
||||
{open && (
|
||||
<div style={{ maxHeight: 160, overflowY: 'auto', display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 4, marginTop: 6, background: '#18181b', borderRadius: 6, padding: 4 }}>
|
||||
{assets.map((asset, i) => (
|
||||
<div key={i} onClick={() => { onSelect(asset.url); setOpen(false); }}
|
||||
style={{ cursor: 'pointer', borderRadius: 4, overflow: 'hidden', border: '2px solid transparent', aspectRatio: '1', transition: 'border-color 0.15s', background: '#27272a', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.borderColor = '#3b82f6'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'transparent'; }}>
|
||||
{(asset.type || '').startsWith('image') ? (
|
||||
<img src={asset.url} alt={asset.name} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', padding: 4 }}>
|
||||
<i className="fa fa-film" style={{ fontSize: 20, color: '#71717a' }} />
|
||||
<div style={{ fontSize: 8, color: '#71717a', marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 70 }}>
|
||||
{asset.name?.replace(/^\d+_[a-f0-9]+_/, '')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{assets.length === 0 && (
|
||||
<p style={{ gridColumn: '1/-1', textAlign: 'center', color: '#71717a', fontSize: 11, padding: 12, margin: 0 }}>
|
||||
No {filter} assets uploaded yet
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import { AssetPicker } from '../../../ui/AssetPicker';
|
||||
|
||||
/* ---------- HERO STYLE PANEL ---------- */
|
||||
export const HeroStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => {
|
||||
const { actions } = useEditor();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const setProp = useCallback((key: string, value: any) => {
|
||||
actions.setProp(selectedId, (props: any) => { props[key] = value; });
|
||||
}, [actions, selectedId]);
|
||||
|
||||
const handleUpload = useCallback(async (file: File, propKey: string) => {
|
||||
const url = await uploadToWhp(file);
|
||||
if (url) setProp(propKey, url);
|
||||
}, [setProp]);
|
||||
|
||||
const bgType = nodeProps.bgType || 'color';
|
||||
|
||||
return (
|
||||
@@ -161,55 +84,20 @@ export const HeroStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProp
|
||||
{bgType === 'image' && (
|
||||
<div style={sectionGap}>
|
||||
<label style={labelStyle}>Background Image</label>
|
||||
{nodeProps.bgImage && (
|
||||
<div style={{ marginBottom: 6, borderRadius: 4, overflow: 'hidden', border: '1px solid #3f3f46', position: 'relative' }}>
|
||||
<img src={nodeProps.bgImage} alt="" style={{ width: '100%', height: 80, objectFit: 'cover', display: 'block' }} />
|
||||
<button onClick={() => setProp('bgImage', '')} style={{ position: 'absolute', top: 2, right: 2, width: 20, height: 20, borderRadius: '50%', background: 'rgba(0,0,0,0.7)', border: 'none', color: '#fff', cursor: 'pointer', fontSize: 10, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<button onClick={() => fileInputRef.current?.click()} style={{ ...btnActiveStyle(false), flex: 1 }}>
|
||||
<i className="fa fa-upload" style={{ marginRight: 4 }} /> Upload
|
||||
</button>
|
||||
</div>
|
||||
<AssetBrowser filter="image" onSelect={(url) => setProp('bgImage', url)} />
|
||||
<input ref={fileInputRef} type="file" accept="image/*" style={{ display: 'none' }}
|
||||
onChange={(e) => { const f = e.target.files?.[0]; if (f) handleUpload(f, 'bgImage'); e.target.value = ''; }} />
|
||||
<input type="text" value={nodeProps.bgImage || ''} placeholder="Or paste URL..."
|
||||
onChange={(e) => setProp('bgImage', e.target.value)} style={{ ...smallInputStyle, width: '100%', marginTop: 4 }} />
|
||||
<AssetPicker value={nodeProps.bgImage || ''} onChange={(url) => setProp('bgImage', url)} variant="full" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{bgType === 'video' && (
|
||||
<div style={sectionGap}>
|
||||
<label style={labelStyle}>Background Video</label>
|
||||
{nodeProps.bgVideo && (
|
||||
<div style={{ marginBottom: 6, padding: 8, background: '#18181b', borderRadius: 4, border: '1px solid #3f3f46', display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<i className="fa fa-film" style={{ color: '#3b82f6' }} />
|
||||
<span style={{ fontSize: 11, color: '#e4e4e7', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{nodeProps.bgVideo.replace(/.*filename=/, '').replace(/^\d+_[a-f0-9]+_/, '') || nodeProps.bgVideo.split('/').pop()}
|
||||
</span>
|
||||
<button onClick={() => setProp('bgVideo', '')} style={{ background: 'none', border: 'none', color: '#ef4444', cursor: 'pointer', fontSize: 12 }}>
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<button onClick={() => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'video/*';
|
||||
input.onchange = () => { const f = input.files?.[0]; if (f) handleUpload(f, 'bgVideo'); };
|
||||
input.click();
|
||||
}} style={{ ...btnActiveStyle(false), flex: 1 }}>
|
||||
<i className="fa fa-upload" style={{ marginRight: 4 }} /> Upload Video
|
||||
</button>
|
||||
</div>
|
||||
<AssetBrowser filter="video" onSelect={(url) => setProp('bgVideo', url)} />
|
||||
<input type="text" value={nodeProps.bgVideo || ''} placeholder="YouTube, Vimeo, or .mp4 URL"
|
||||
onChange={(e) => setProp('bgVideo', e.target.value)} style={{ ...smallInputStyle, width: '100%', marginTop: 4 }} />
|
||||
<AssetPicker
|
||||
mediaType="video"
|
||||
variant="full"
|
||||
value={nodeProps.bgVideo || ''}
|
||||
onChange={(url) => setProp('bgVideo', url)}
|
||||
placeholder="YouTube, Vimeo, or .mp4 URL"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user