import React, { useEffect, useCallback } from 'react'; import { useCanvasStore } from '../../store/canvasStore'; import { useBrandingStore } from '../../store/brandingStore'; import { FONT_FAMILIES } from '../../types'; import { SocialIcon } from '../elements/SocialIcons'; const POSITION_OPTIONS = [ { value: 'top-left', label: 'Top Left' }, { value: 'top-right', label: 'Top Right' }, { value: 'bottom-left', label: 'Bottom Left' }, { value: 'bottom-right', label: 'Bottom Right' }, ] as const; const SOCIAL_PLATFORMS = [ { key: 'twitter', label: 'X (Twitter)', placeholder: '@username' }, { key: 'linkedin', label: 'LinkedIn', placeholder: '/in/username' }, { key: 'instagram', label: 'Instagram', placeholder: '@username' }, { key: 'github', label: 'GitHub', placeholder: 'username' }, { key: 'youtube', label: 'YouTube', placeholder: '@channel' }, { key: 'tiktok', label: 'TikTok', placeholder: '@username' }, ] as const; const MAX_AVATAR_DIMENSION = 192; const MAX_EMBEDDED_AVATAR_LENGTH = 350000; const readFileAsDataUrl = (file: File) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { if (typeof reader.result === 'string') { resolve(reader.result); } else { reject(new Error('Could not read file contents.')); } }; reader.onerror = () => reject(reader.error ?? new Error('Avatar read failed.')); reader.readAsDataURL(file); }); const loadImage = (src: string) => new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = () => reject(new Error('Avatar image could not be loaded.')); img.src = src; }); const optimiseAvatarFile = async (file: File): Promise => { const rawDataUrl = await readFileAsDataUrl(file); if (rawDataUrl.length <= MAX_EMBEDDED_AVATAR_LENGTH) { return rawDataUrl; } try { const image = await loadImage(rawDataUrl); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) { return rawDataUrl; } let targetSize = MAX_AVATAR_DIMENSION; let optimised = rawDataUrl; while (targetSize >= 96) { const maxSide = Math.max(image.width, image.height); const scale = maxSide > targetSize ? targetSize / maxSide : 1; const width = Math.max(1, Math.round(image.width * scale)); const height = Math.max(1, Math.round(image.height * scale)); canvas.width = width; canvas.height = height; ctx.clearRect(0, 0, width, height); ctx.drawImage(image, 0, 0, width, height); optimised = canvas.toDataURL('image/png'); if (optimised.length <= MAX_EMBEDDED_AVATAR_LENGTH) { break; } optimised = canvas.toDataURL('image/jpeg', 0.82); if (optimised.length <= MAX_EMBEDDED_AVATAR_LENGTH) { break; } targetSize = Math.floor(targetSize * 0.75); } return optimised.length <= MAX_EMBEDDED_AVATAR_LENGTH ? optimised : rawDataUrl; } catch (error) { console.warn('Avatar optimisation failed, keeping the original image.', error); return rawDataUrl; } }; const BrandingPanel: React.FC = () => { const { setBackground } = useCanvasStore(); const { info, preferences, updateInfo, updateSocial, updatePreferences } = useBrandingStore(); // Sync branding store to canvas whenever it changes useEffect(() => { setBackground({ branding: { ...preferences, name: info.name, website: info.website, social: info.social, avatarUrl: info.avatarUrl, }, }); }, [info, preferences, setBackground]); const handleUpdatePreferences = (updates: Partial) => { updatePreferences(updates); }; const handleUpdateInfo = (updates: Partial) => { updateInfo(updates); }; const handleUpdateSocial = (platform: string, value: string) => { updateSocial(platform, value); }; const handleAvatarUpload = useCallback((event: React.ChangeEvent) => { const file = event.target.files?.[0]; const resetInput = () => { if (event.target) { event.target.value = ''; } }; if (!file || !file.type.startsWith('image/')) { resetInput(); return; } void optimiseAvatarFile(file) .then((dataUrl) => { handleUpdateInfo({ avatarUrl: dataUrl }); handleUpdatePreferences({ showAvatar: true }); }) .catch((error) => { console.warn('Avatar upload cancelled.', error); }) .finally(resetInput); }, [handleUpdateInfo, handleUpdatePreferences]); const handleAvatarClear = useCallback(() => { handleUpdateInfo({ avatarUrl: '' }); }, [handleUpdateInfo]); return (
{/* Enable Branding */}
{preferences.enabled && (
{/* Position */}
{POSITION_OPTIONS.map((opt) => ( ))}
{/* Avatar */}

Use a square image for best results. Supported formats: PNG, JPG, SVG.

{info.avatarUrl && ( )}
{preferences.showAvatar && (
handleUpdatePreferences({ avatarSize: parseInt(e.target.value) })} className="w-full h-1.5 bg-white/10 rounded-lg appearance-none cursor-pointer accent-blue-600" />
)}
{/* Name */}
handleUpdateInfo({ name: e.target.value })} placeholder="Your Name" className="w-full bg-white/5 text-white px-3 py-2 rounded-lg text-sm border border-white/5 focus:border-blue-500/50 focus:outline-none" />
{/* Website */}
handleUpdateInfo({ website: e.target.value })} placeholder="yourwebsite.com" className="w-full bg-white/5 text-white px-3 py-2 rounded-lg text-sm border border-white/5 focus:border-blue-500/50 focus:outline-none" />
{/* Social Media */}
{SOCIAL_PLATFORMS.map((platform) => (
handleUpdateSocial(platform.key, e.target.value)} placeholder={platform.placeholder} className="flex-1 bg-white/5 text-white px-2 py-1.5 rounded-md text-xs border border-white/5 focus:border-blue-500/50 focus:outline-none" />
))}
{/* Social Icons Layout */}
handleUpdatePreferences({ socialIconSize: parseInt(e.target.value) })} className="w-full h-1.5 bg-white/10 rounded-lg appearance-none cursor-pointer accent-blue-600" />
{/* Styling */}
{/* Font Family */}
{/* Font Size */}
handleUpdatePreferences({ fontSize: parseInt(e.target.value) })} className="w-full h-1.5 bg-white/10 rounded-lg appearance-none cursor-pointer accent-blue-600" />
{/* Color */}
handleUpdatePreferences({ color: e.target.value })} className="absolute inset-[-4px] w-[200%] h-[200%] cursor-pointer" />
handleUpdatePreferences({ color: e.target.value })} className="w-full bg-transparent text-white text-xs focus:outline-none font-mono" />
{/* Opacity */}
handleUpdatePreferences({ opacity: parseFloat(e.target.value) })} className="w-full h-1.5 bg-white/10 rounded-lg appearance-none cursor-pointer accent-blue-600" />
{/* Padding */}
handleUpdatePreferences({ padding: parseInt(e.target.value) })} className="w-full h-1.5 bg-white/10 rounded-lg appearance-none cursor-pointer accent-blue-600" />
)}
); }; export default BrandingPanel;