import React, { CSSProperties, useCallback, useRef, useState } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { useSiteDesign } from '../../state/SiteDesignContext'; /* ---------- Types ---------- */ interface LogoProps { type?: 'text' | 'image'; text?: string; imageSrc?: string; imageWidth?: string; href?: string; fontFamily?: string; fontSize?: string; fontWeight?: string; color?: string; style?: CSSProperties; } /* ---------- Image upload helper ---------- */ async function uploadToWhp(file: File): Promise { const cfg = (window as any).WHP_CONFIG; if (!cfg) return URL.createObjectURL(file); const formData = new FormData(); formData.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: formData, }); const data = await resp.json(); if (data.success && data.url) return data.url; return null; } catch { return null; } } /* ---------- Helper: escape HTML ---------- */ function esc(str: string): string { return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } /* ---------- Component ---------- */ export const Logo: UserComponent = ({ type = 'text', text = 'MySite', imageSrc = '', imageWidth = '120px', href = '/', fontFamily = 'Inter, sans-serif', fontSize = '20px', fontWeight = '700', color, style = {}, }) => { const { connectors: { connect, drag }, } = useNode(); const { design } = useSiteDesign(); const resolvedColor = color || design.textColor; return ( { if (ref) connect(drag(ref)); }} href={href} onClick={(e) => e.preventDefault()} style={{ textDecoration: 'none', display: 'inline-flex', alignItems: 'center', flexShrink: 0, ...style, }} > {type === 'image' && imageSrc ? ( {text ) : ( {text} )} ); }; /* ---------- Settings panel ---------- */ const LogoSettings: React.FC = () => { const { actions: { setProp }, props } = useNode((node) => ({ props: node.data.props as LogoProps, })); const { design } = useSiteDesign(); const logoType = props.type || 'text'; const fileInputRef = useRef(null); const [showBrowser, setShowBrowser] = useState(false); const [browserAssets, setBrowserAssets] = useState([]); const [browserLoading, setBrowserLoading] = useState(false); const fontFamilies = [ { label: 'Inter', value: 'Inter, sans-serif' }, { label: 'Roboto', value: 'Roboto, sans-serif' }, { label: 'Poppins', value: 'Poppins, sans-serif' }, { label: 'Montserrat', value: 'Montserrat, sans-serif' }, { label: 'Playfair', value: 'Playfair Display, serif' }, { label: 'Merriweather', value: 'Merriweather, serif' }, { label: 'Source Code', value: 'Source Code Pro, monospace' }, { label: 'Open Sans', value: 'Open Sans, sans-serif' }, ]; const handleLogoUpload = useCallback(async (file: File) => { const url = await uploadToWhp(file); if (url) setProp((p: LogoProps) => { p.imageSrc = url; }); }, [setProp]); const handleBrowse = useCallback(async () => { if (showBrowser) { setShowBrowser(false); return; } const cfg = (window as any).WHP_CONFIG; if (!cfg) return; setBrowserLoading(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 images = data.assets.filter((a: any) => (a.type || '').startsWith('image')); setBrowserAssets(images); setShowBrowser(true); } } catch (e) { console.error('Browse failed:', e); } finally { setBrowserLoading(false); } }, [showBrowser]); /* ---- Shared styles ---- */ const labelStyle: CSSProperties = { fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 4 }; const inputStyle: CSSProperties = { width: '100%', padding: '3px 6px', background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, fontSize: 11, }; const btnSmall: CSSProperties = { padding: '2px 6px', fontSize: 11, background: '#27272a', color: '#a1a1aa', border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer', }; const btnActive: CSSProperties = { ...btnSmall, background: '#3b82f6', color: '#fff', borderColor: '#3b82f6', }; return (
{/* Type toggle */}
{logoType === 'text' ? ( <>
setProp((p: LogoProps) => { p.text = e.target.value; })} style={inputStyle} />
setProp((p: LogoProps) => { p.fontSize = e.target.value; })} placeholder="20px" style={inputStyle} />
setProp((p: LogoProps) => { p.color = e.target.value; })} style={{ width: 28, height: 24, padding: 0, border: '1px solid #3f3f46', borderRadius: 3, cursor: 'pointer', background: 'none' }} /> {props.color || 'Auto'}
) : ( <> {/* Image logo controls */} {props.imageSrc ? (
) : (
fileInputRef.current?.click()} onDragOver={(e) => { e.preventDefault(); e.currentTarget.style.borderColor = '#3b82f6'; }} onDragLeave={(e) => { e.currentTarget.style.borderColor = '#3f3f46'; }} onDrop={async (e) => { e.preventDefault(); e.currentTarget.style.borderColor = '#3f3f46'; const file = e.dataTransfer.files?.[0]; if (file && file.type.startsWith('image/')) await handleLogoUpload(file); }} > Drop logo or click to upload
)}
{/* Browse grid */} {showBrowser && (
{browserAssets.map(asset => (
{ setProp((p: LogoProps) => { p.imageSrc = asset.url; }); setShowBrowser(false); }} style={{ cursor: 'pointer', borderRadius: 4, overflow: 'hidden', border: '2px solid transparent', aspectRatio: '1' }} onMouseEnter={(e) => { e.currentTarget.style.borderColor = '#3b82f6'; }} onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'transparent'; }} > {asset.name}
))} {browserAssets.length === 0 && (

No images uploaded yet.

)}
)} { const file = e.target.files?.[0]; if (file) handleLogoUpload(file); e.target.value = ''; }} /> {/* URL paste input */}
setProp((p: LogoProps) => { p.imageSrc = e.target.value; })} placeholder="Or paste image URL..." style={{ ...inputStyle, fontSize: 10, color: '#71717a' }} />
setProp((p: LogoProps) => { p.imageWidth = e.target.value; })} placeholder="120px" style={inputStyle} />
)} {/* Link URL */}
setProp((p: LogoProps) => { p.href = e.target.value; })} placeholder="/" style={inputStyle} />
); }; /* ---------- Craft config ---------- */ Logo.craft = { displayName: 'Logo', props: { type: 'text', text: 'MySite', imageSrc: '', imageWidth: '120px', href: '/', fontFamily: 'Inter, sans-serif', fontSize: '20px', fontWeight: '700', color: undefined, style: {}, } as LogoProps, rules: { canDrag: () => true, canMoveIn: () => false, canMoveOut: () => true, }, related: { settings: LogoSettings, }, }; /* ---------- HTML export ---------- */ (Logo as any).toHtml = (props: LogoProps, _childrenHtml: string) => { const href = props.href || '/'; let innerHtml: string; if (props.type === 'image' && props.imageSrc) { const imgStyle = cssPropsToString({ width: props.imageWidth || '120px', height: 'auto', display: 'block' }); innerHtml = `${esc(props.text || 'Logo')}`; } else { const spanStyle = cssPropsToString({ fontWeight: props.fontWeight || '700', fontSize: props.fontSize || '20px', fontFamily: props.fontFamily || 'Inter, sans-serif', color: props.color || '#1f2937', }); innerHtml = `${esc(props.text || 'MySite')}`; } const aStyle = cssPropsToString({ textDecoration: 'none', display: 'inline-flex', alignItems: 'center', flexShrink: '0', ...props.style, }); return { html: `${innerHtml}`, }; };