feat: enhance branding features with social media integration and customizable icon settings
This commit is contained in:
+126
-29
@@ -1,10 +1,11 @@
|
||||
import React, { useRef, useState, useEffect, useCallback, useMemo, memo } from 'react';
|
||||
import { Stage, Layer, Rect, Line, Text } from 'react-konva';
|
||||
import { Stage, Layer, Rect, Line, Text, Group, Path } from 'react-konva';
|
||||
import type Konva from 'konva';
|
||||
import { useCanvasStore, createCodeElement, createTextElement, createArrowElement } from '../store/canvasStore';
|
||||
import CodeBlock from './elements/CodeBlock';
|
||||
import TextBlock from './elements/TextBlock';
|
||||
import Arrow from './elements/Arrow';
|
||||
import { SOCIAL_ICON_PATHS, SOCIAL_PLATFORMS_CONFIG } from './elements/SocialIcons';
|
||||
import type { CodeElement, TextElement, ArrowElement } from '../types';
|
||||
|
||||
interface CanvasProps {
|
||||
@@ -188,6 +189,10 @@ const Canvas: React.FC<CanvasProps> = ({ stageRef }) => {
|
||||
const padding = branding.padding || 24;
|
||||
const fontSize = branding.fontSize || 14;
|
||||
const lineHeight = fontSize * 1.5;
|
||||
const iconSize = branding.socialIconSize || 20;
|
||||
const socialLayout = branding.socialLayout || 'horizontal';
|
||||
const iconGap = 16;
|
||||
const iconTextGap = 6; // Gap between icon and its text
|
||||
|
||||
// Build branding text lines
|
||||
const lines: string[] = [];
|
||||
@@ -197,23 +202,31 @@ const Canvas: React.FC<CanvasProps> = ({ stageRef }) => {
|
||||
if (branding.showWebsite && branding.website) {
|
||||
lines.push(branding.website);
|
||||
}
|
||||
if (branding.showSocial && branding.social) {
|
||||
const socialParts: string[] = [];
|
||||
if (branding.social.twitter) socialParts.push(branding.social.twitter);
|
||||
if (branding.social.linkedin) socialParts.push(branding.social.linkedin);
|
||||
if (branding.social.instagram) socialParts.push(branding.social.instagram);
|
||||
if (branding.social.github) socialParts.push(branding.social.github);
|
||||
if (branding.social.youtube) socialParts.push(branding.social.youtube);
|
||||
if (branding.social.tiktok) socialParts.push(branding.social.tiktok);
|
||||
if (socialParts.length > 0) {
|
||||
lines.push(socialParts.join(' • '));
|
||||
}
|
||||
}
|
||||
|
||||
if (lines.length === 0) return null;
|
||||
// Get active social platforms with their values
|
||||
const activeSocialPlatforms = branding.showSocial && branding.social
|
||||
? SOCIAL_PLATFORMS_CONFIG.filter(p => branding.social[p.key as keyof typeof branding.social])
|
||||
.map(p => ({
|
||||
...p,
|
||||
value: branding.social[p.key as keyof typeof branding.social] || ''
|
||||
}))
|
||||
: [];
|
||||
|
||||
const hasTextContent = lines.length > 0;
|
||||
const hasSocialIcons = activeSocialPlatforms.length > 0;
|
||||
|
||||
if (!hasTextContent && !hasSocialIcons) return null;
|
||||
|
||||
const text = lines.join('\n');
|
||||
const textHeight = lines.length * lineHeight;
|
||||
|
||||
// Calculate social section dimensions (icon + text for each platform)
|
||||
const socialItemHeight = Math.max(iconSize, fontSize);
|
||||
const socialHeight = socialLayout === 'vertical'
|
||||
? activeSocialPlatforms.length * socialItemHeight + (activeSocialPlatforms.length - 1) * (iconGap - 4)
|
||||
: socialItemHeight;
|
||||
|
||||
// Calculate total content height
|
||||
const contentHeight = textHeight + (hasTextContent && hasSocialIcons ? 16 : 0) + (hasSocialIcons ? socialHeight : 0);
|
||||
|
||||
// Calculate position based on setting
|
||||
let x = padding;
|
||||
@@ -233,29 +246,113 @@ const Canvas: React.FC<CanvasProps> = ({ stageRef }) => {
|
||||
break;
|
||||
case 'bottom-left':
|
||||
x = padding;
|
||||
y = height - padding - textHeight;
|
||||
y = height - padding - contentHeight;
|
||||
align = 'left';
|
||||
break;
|
||||
case 'bottom-right':
|
||||
x = width - padding;
|
||||
y = height - padding - textHeight;
|
||||
y = height - padding - contentHeight;
|
||||
align = 'right';
|
||||
break;
|
||||
}
|
||||
|
||||
// Calculate icon positions
|
||||
const iconScale = iconSize / 24; // SVG viewBox is 24x24
|
||||
const socialStartY = y + textHeight + (hasTextContent ? 16 : 0);
|
||||
|
||||
return (
|
||||
<Text
|
||||
x={align === 'right' ? 0 : x}
|
||||
y={y}
|
||||
width={align === 'right' ? x : width - padding}
|
||||
text={text}
|
||||
fontSize={fontSize}
|
||||
fontFamily={branding.fontFamily || 'Inter'}
|
||||
fill={branding.color || '#ffffff'}
|
||||
opacity={branding.opacity || 0.8}
|
||||
align={align}
|
||||
lineHeight={1.5}
|
||||
/>
|
||||
<Group opacity={branding.opacity || 0.8}>
|
||||
{/* Text content */}
|
||||
{hasTextContent && (
|
||||
<Text
|
||||
x={align === 'right' ? 0 : x}
|
||||
y={y}
|
||||
width={align === 'right' ? x : width - padding}
|
||||
text={lines.join('\n')}
|
||||
fontSize={fontSize}
|
||||
fontFamily={branding.fontFamily || 'Inter'}
|
||||
fill={branding.color || '#ffffff'}
|
||||
align={align}
|
||||
lineHeight={1.5}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Social icons with text */}
|
||||
{hasSocialIcons && activeSocialPlatforms.map((platform, index) => {
|
||||
const path = SOCIAL_ICON_PATHS[platform.key];
|
||||
if (!path) return null;
|
||||
|
||||
// Calculate position for this social item
|
||||
let itemX: number;
|
||||
let itemY: number;
|
||||
|
||||
if (socialLayout === 'vertical') {
|
||||
itemY = socialStartY + index * (socialItemHeight + iconGap - 4);
|
||||
itemX = align === 'right' ? x : x;
|
||||
} else {
|
||||
itemY = socialStartY;
|
||||
// For horizontal layout, we need to calculate cumulative width
|
||||
// This is simplified - for perfect alignment we'd need to measure text
|
||||
const prevItemsWidth = activeSocialPlatforms.slice(0, index).reduce((acc, p) => {
|
||||
const textWidth = (p.value.length * fontSize * 0.5); // Approximate text width
|
||||
return acc + iconSize + iconTextGap + textWidth + iconGap;
|
||||
}, 0);
|
||||
itemX = align === 'right' ? x - prevItemsWidth : x + prevItemsWidth;
|
||||
}
|
||||
|
||||
// Icon vertical centering within item
|
||||
const iconY = itemY + (socialItemHeight - iconSize) / 2;
|
||||
// Text vertical centering
|
||||
const textY = itemY + (socialItemHeight - fontSize) / 2;
|
||||
|
||||
if (align === 'right') {
|
||||
// For right alignment: text first, then icon
|
||||
const textWidth = platform.value.length * fontSize * 0.5; // Approximate
|
||||
return (
|
||||
<Group key={platform.key}>
|
||||
<Path
|
||||
x={itemX - iconSize}
|
||||
y={iconY}
|
||||
data={path}
|
||||
fill={branding.color || '#ffffff'}
|
||||
scaleX={iconScale}
|
||||
scaleY={iconScale}
|
||||
/>
|
||||
<Text
|
||||
x={itemX - iconSize - iconTextGap - textWidth}
|
||||
y={textY}
|
||||
text={platform.value}
|
||||
fontSize={fontSize}
|
||||
fontFamily={branding.fontFamily || 'Inter'}
|
||||
fill={branding.color || '#ffffff'}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
} else {
|
||||
// For left alignment: icon first, then text
|
||||
return (
|
||||
<Group key={platform.key}>
|
||||
<Path
|
||||
x={itemX}
|
||||
y={iconY}
|
||||
data={path}
|
||||
fill={branding.color || '#ffffff'}
|
||||
scaleX={iconScale}
|
||||
scaleY={iconScale}
|
||||
/>
|
||||
<Text
|
||||
x={itemX + iconSize + iconTextGap}
|
||||
y={textY}
|
||||
text={platform.value}
|
||||
fontSize={fontSize}
|
||||
fontFamily={branding.fontFamily || 'Inter'}
|
||||
fill={branding.color || '#ffffff'}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Group>
|
||||
);
|
||||
}, [background.branding, width, height]);
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
|
||||
// SVG path data for social media icons
|
||||
export const SOCIAL_ICON_PATHS: Record<string, string> = {
|
||||
twitter: 'M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z',
|
||||
linkedin: 'M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z',
|
||||
instagram: 'M12 0C8.74 0 8.333.015 7.053.072 5.775.132 4.905.333 4.14.63c-.789.306-1.459.717-2.126 1.384S.935 3.35.63 4.14C.333 4.905.131 5.775.072 7.053.012 8.333 0 8.74 0 12s.015 3.667.072 4.947c.06 1.277.261 2.148.558 2.913.306.788.717 1.459 1.384 2.126.667.666 1.336 1.079 2.126 1.384.766.296 1.636.499 2.913.558C8.333 23.988 8.74 24 12 24s3.667-.015 4.947-.072c1.277-.06 2.148-.262 2.913-.558.788-.306 1.459-.718 2.126-1.384.666-.667 1.079-1.335 1.384-2.126.296-.765.499-1.636.558-2.913.06-1.28.072-1.687.072-4.947s-.015-3.667-.072-4.947c-.06-1.277-.262-2.149-.558-2.913-.306-.789-.718-1.459-1.384-2.126C21.319 1.347 20.651.935 19.86.63c-.765-.297-1.636-.499-2.913-.558C15.667.012 15.26 0 12 0zm0 2.16c3.203 0 3.585.016 4.85.071 1.17.055 1.805.249 2.227.415.562.217.96.477 1.382.896.419.42.679.819.896 1.381.164.422.36 1.057.413 2.227.057 1.266.07 1.646.07 4.85s-.015 3.585-.074 4.85c-.061 1.17-.256 1.805-.421 2.227-.224.562-.479.96-.899 1.382-.419.419-.824.679-1.38.896-.42.164-1.065.36-2.235.413-1.274.057-1.649.07-4.859.07-3.211 0-3.586-.015-4.859-.074-1.171-.061-1.816-.256-2.236-.421-.569-.224-.96-.479-1.379-.899-.421-.419-.69-.824-.9-1.38-.165-.42-.359-1.065-.42-2.235-.045-1.26-.061-1.649-.061-4.844 0-3.196.016-3.586.061-4.861.061-1.17.255-1.814.42-2.234.21-.57.479-.96.9-1.381.419-.419.81-.689 1.379-.898.42-.166 1.051-.361 2.221-.421 1.275-.045 1.65-.06 4.859-.06l.045.03zm0 3.678c-3.405 0-6.162 2.76-6.162 6.162 0 3.405 2.76 6.162 6.162 6.162 3.405 0 6.162-2.76 6.162-6.162 0-3.405-2.76-6.162-6.162-6.162zM12 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm7.846-10.405c0 .795-.646 1.44-1.44 1.44-.795 0-1.44-.646-1.44-1.44 0-.794.646-1.439 1.44-1.439.793-.001 1.44.645 1.44 1.439z',
|
||||
github: 'M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12',
|
||||
youtube: 'M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z',
|
||||
tiktok: 'M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z',
|
||||
};
|
||||
|
||||
// Social media platform order and colors
|
||||
export const SOCIAL_PLATFORMS_CONFIG = [
|
||||
{ key: 'twitter', label: 'X', color: '#000000' },
|
||||
{ key: 'linkedin', label: 'LinkedIn', color: '#0A66C2' },
|
||||
{ key: 'instagram', label: 'Instagram', color: '#E4405F' },
|
||||
{ key: 'github', label: 'GitHub', color: '#181717' },
|
||||
{ key: 'youtube', label: 'YouTube', color: '#FF0000' },
|
||||
{ key: 'tiktok', label: 'TikTok', color: '#000000' },
|
||||
] as const;
|
||||
|
||||
interface SocialIconProps {
|
||||
platform: string;
|
||||
size?: number;
|
||||
color?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SocialIcon: React.FC<SocialIconProps> = ({
|
||||
platform,
|
||||
size = 24,
|
||||
color = 'currentColor',
|
||||
className = ''
|
||||
}) => {
|
||||
const path = SOCIAL_ICON_PATHS[platform];
|
||||
if (!path) return null;
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill={color}
|
||||
className={className}
|
||||
style={{ display: 'block' }}
|
||||
>
|
||||
<path d={path} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
interface SocialIconsGroupProps {
|
||||
social: {
|
||||
twitter?: string;
|
||||
linkedin?: string;
|
||||
instagram?: string;
|
||||
github?: string;
|
||||
youtube?: string;
|
||||
tiktok?: string;
|
||||
};
|
||||
size?: number;
|
||||
color?: string;
|
||||
layout?: 'horizontal' | 'vertical';
|
||||
gap?: number;
|
||||
}
|
||||
|
||||
export const SocialIconsGroup: React.FC<SocialIconsGroupProps> = ({
|
||||
social,
|
||||
size = 20,
|
||||
color = '#ffffff',
|
||||
layout = 'horizontal',
|
||||
gap = 12,
|
||||
}) => {
|
||||
const activePlatforms = SOCIAL_PLATFORMS_CONFIG.filter(
|
||||
(p) => social[p.key as keyof typeof social]
|
||||
);
|
||||
|
||||
if (activePlatforms.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: layout === 'horizontal' ? 'row' : 'column',
|
||||
gap: `${gap}px`,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{activePlatforms.map((platform) => (
|
||||
<SocialIcon
|
||||
key={platform.key}
|
||||
platform={platform.key}
|
||||
size={size}
|
||||
color={color}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SocialIcon;
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } 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' },
|
||||
@@ -19,40 +21,31 @@ const SOCIAL_PLATFORMS = [
|
||||
] as const;
|
||||
|
||||
const BrandingPanel: React.FC = () => {
|
||||
const { snap, setBackground } = useCanvasStore();
|
||||
|
||||
// Default branding values for backwards compatibility
|
||||
const defaultBranding = {
|
||||
enabled: false,
|
||||
position: 'bottom-right' as const,
|
||||
name: '',
|
||||
website: '',
|
||||
social: {},
|
||||
showName: true,
|
||||
showWebsite: true,
|
||||
showSocial: true,
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
color: '#ffffff',
|
||||
opacity: 0.8,
|
||||
padding: 24,
|
||||
};
|
||||
|
||||
const branding = snap.background.branding || defaultBranding;
|
||||
const { setBackground } = useCanvasStore();
|
||||
const { info, preferences, updateInfo, updateSocial, updatePreferences } = useBrandingStore();
|
||||
|
||||
const updateBranding = (updates: Partial<typeof branding>) => {
|
||||
setBackground({
|
||||
branding: { ...branding, ...updates },
|
||||
});
|
||||
};
|
||||
|
||||
const updateSocial = (platform: string, value: string) => {
|
||||
// Sync branding store to canvas whenever it changes
|
||||
useEffect(() => {
|
||||
setBackground({
|
||||
branding: {
|
||||
...branding,
|
||||
social: { ...(branding.social || {}), [platform]: value },
|
||||
...preferences,
|
||||
name: info.name,
|
||||
website: info.website,
|
||||
social: info.social,
|
||||
},
|
||||
});
|
||||
}, [info, preferences, setBackground]);
|
||||
|
||||
const handleUpdatePreferences = (updates: Partial<typeof preferences>) => {
|
||||
updatePreferences(updates);
|
||||
};
|
||||
|
||||
const handleUpdateInfo = (updates: Partial<typeof info>) => {
|
||||
updateInfo(updates);
|
||||
};
|
||||
|
||||
const handleUpdateSocial = (platform: string, value: string) => {
|
||||
updateSocial(platform, value);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -63,20 +56,20 @@ const BrandingPanel: React.FC = () => {
|
||||
Branding Watermark
|
||||
</label>
|
||||
<button
|
||||
onClick={() => updateBranding({ enabled: !branding.enabled })}
|
||||
onClick={() => handleUpdatePreferences({ enabled: !preferences.enabled })}
|
||||
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
|
||||
branding.enabled ? 'bg-blue-600' : 'bg-white/10'
|
||||
preferences.enabled ? 'bg-blue-600' : 'bg-white/10'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-white transition-transform ${
|
||||
branding.enabled ? 'translate-x-4' : 'translate-x-1'
|
||||
preferences.enabled ? 'translate-x-4' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{branding.enabled && (
|
||||
{preferences.enabled && (
|
||||
<div className="space-y-5">
|
||||
{/* Position */}
|
||||
<div>
|
||||
@@ -87,9 +80,9 @@ const BrandingPanel: React.FC = () => {
|
||||
{POSITION_OPTIONS.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => updateBranding({ position: opt.value })}
|
||||
onClick={() => handleUpdatePreferences({ position: opt.value })}
|
||||
className={`py-2 px-3 rounded-lg text-xs font-medium transition-all border ${
|
||||
branding.position === opt.value
|
||||
preferences.position === opt.value
|
||||
? 'bg-blue-600/20 text-blue-400 border-blue-500/50'
|
||||
: 'bg-white/5 text-neutral-400 hover:text-white hover:bg-white/10 border-white/5'
|
||||
}`}
|
||||
@@ -107,20 +100,20 @@ const BrandingPanel: React.FC = () => {
|
||||
Name
|
||||
</label>
|
||||
<button
|
||||
onClick={() => updateBranding({ showName: !branding.showName })}
|
||||
onClick={() => handleUpdatePreferences({ showName: !preferences.showName })}
|
||||
className={`text-xs px-2 py-0.5 rounded ${
|
||||
branding.showName
|
||||
preferences.showName
|
||||
? 'bg-blue-600/20 text-blue-400'
|
||||
: 'bg-white/5 text-neutral-500'
|
||||
}`}
|
||||
>
|
||||
{branding.showName ? 'Show' : 'Hide'}
|
||||
{preferences.showName ? 'Show' : 'Hide'}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={branding.name}
|
||||
onChange={(e) => updateBranding({ name: e.target.value })}
|
||||
value={info.name}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
@@ -133,20 +126,20 @@ const BrandingPanel: React.FC = () => {
|
||||
Website
|
||||
</label>
|
||||
<button
|
||||
onClick={() => updateBranding({ showWebsite: !branding.showWebsite })}
|
||||
onClick={() => handleUpdatePreferences({ showWebsite: !preferences.showWebsite })}
|
||||
className={`text-xs px-2 py-0.5 rounded ${
|
||||
branding.showWebsite
|
||||
preferences.showWebsite
|
||||
? 'bg-blue-600/20 text-blue-400'
|
||||
: 'bg-white/5 text-neutral-500'
|
||||
}`}
|
||||
>
|
||||
{branding.showWebsite ? 'Show' : 'Hide'}
|
||||
{preferences.showWebsite ? 'Show' : 'Hide'}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={branding.website}
|
||||
onChange={(e) => updateBranding({ website: e.target.value })}
|
||||
value={info.website}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
@@ -159,32 +152,75 @@ const BrandingPanel: React.FC = () => {
|
||||
Social Media
|
||||
</label>
|
||||
<button
|
||||
onClick={() => updateBranding({ showSocial: !branding.showSocial })}
|
||||
onClick={() => handleUpdatePreferences({ showSocial: !preferences.showSocial })}
|
||||
className={`text-xs px-2 py-0.5 rounded ${
|
||||
branding.showSocial
|
||||
preferences.showSocial
|
||||
? 'bg-blue-600/20 text-blue-400'
|
||||
: 'bg-white/5 text-neutral-500'
|
||||
}`}
|
||||
>
|
||||
{branding.showSocial ? 'Show' : 'Hide'}
|
||||
{preferences.showSocial ? 'Show' : 'Hide'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{SOCIAL_PLATFORMS.map((platform) => (
|
||||
<div key={platform.key} className="flex items-center gap-2">
|
||||
<span className="text-xs text-neutral-500 w-20 shrink-0">
|
||||
{platform.label}
|
||||
</span>
|
||||
<div className="w-5 h-5 shrink-0 flex items-center justify-center text-neutral-400">
|
||||
<SocialIcon platform={platform.key} size={16} color="currentColor" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={branding.social[platform.key as keyof typeof branding.social] || ''}
|
||||
onChange={(e) => updateSocial(platform.key, e.target.value)}
|
||||
value={info.social[platform.key as keyof typeof info.social] || ''}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Social Icons Layout */}
|
||||
<div className="mt-4 space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs text-neutral-500 mb-2">Layout</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleUpdatePreferences({ socialLayout: 'horizontal' })}
|
||||
className={`flex-1 py-2 px-3 rounded-lg text-xs font-medium transition-all border ${
|
||||
preferences.socialLayout === 'horizontal'
|
||||
? 'bg-blue-600/20 text-blue-400 border-blue-500/50'
|
||||
: 'bg-white/5 text-neutral-400 hover:text-white hover:bg-white/10 border-white/5'
|
||||
}`}
|
||||
>
|
||||
Horizontal
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleUpdatePreferences({ socialLayout: 'vertical' })}
|
||||
className={`flex-1 py-2 px-3 rounded-lg text-xs font-medium transition-all border ${
|
||||
preferences.socialLayout === 'vertical'
|
||||
? 'bg-blue-600/20 text-blue-400 border-blue-500/50'
|
||||
: 'bg-white/5 text-neutral-400 hover:text-white hover:bg-white/10 border-white/5'
|
||||
}`}
|
||||
>
|
||||
Vertical
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs text-neutral-500 mb-2">
|
||||
Icon Size: {preferences.socialIconSize}px
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="14"
|
||||
max="32"
|
||||
value={preferences.socialIconSize}
|
||||
onChange={(e) => handleUpdatePreferences({ socialIconSize: parseInt(e.target.value) })}
|
||||
className="w-full h-1.5 bg-white/10 rounded-lg appearance-none cursor-pointer accent-blue-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Styling */}
|
||||
@@ -197,8 +233,8 @@ const BrandingPanel: React.FC = () => {
|
||||
<div className="mb-4">
|
||||
<label className="block text-xs text-neutral-500 mb-2">Font</label>
|
||||
<select
|
||||
value={branding.fontFamily}
|
||||
onChange={(e) => updateBranding({ fontFamily: e.target.value })}
|
||||
value={preferences.fontFamily}
|
||||
onChange={(e) => handleUpdatePreferences({ fontFamily: e.target.value })}
|
||||
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"
|
||||
>
|
||||
{FONT_FAMILIES.text.map((font) => (
|
||||
@@ -212,14 +248,14 @@ const BrandingPanel: React.FC = () => {
|
||||
{/* Font Size */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-xs text-neutral-500 mb-2">
|
||||
Font Size: {branding.fontSize}px
|
||||
Font Size: {preferences.fontSize}px
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="10"
|
||||
max="24"
|
||||
value={branding.fontSize}
|
||||
onChange={(e) => updateBranding({ fontSize: parseInt(e.target.value) })}
|
||||
value={preferences.fontSize}
|
||||
onChange={(e) => handleUpdatePreferences({ fontSize: parseInt(e.target.value) })}
|
||||
className="w-full h-1.5 bg-white/10 rounded-lg appearance-none cursor-pointer accent-blue-600"
|
||||
/>
|
||||
</div>
|
||||
@@ -231,15 +267,15 @@ const BrandingPanel: React.FC = () => {
|
||||
<div className="w-6 h-6 rounded overflow-hidden relative border border-white/10 shrink-0">
|
||||
<input
|
||||
type="color"
|
||||
value={branding.color}
|
||||
onChange={(e) => updateBranding({ color: e.target.value })}
|
||||
value={preferences.color}
|
||||
onChange={(e) => handleUpdatePreferences({ color: e.target.value })}
|
||||
className="absolute inset-[-4px] w-[200%] h-[200%] cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={branding.color}
|
||||
onChange={(e) => updateBranding({ color: e.target.value })}
|
||||
value={preferences.color}
|
||||
onChange={(e) => handleUpdatePreferences({ color: e.target.value })}
|
||||
className="w-full bg-transparent text-white text-xs focus:outline-none font-mono"
|
||||
/>
|
||||
</div>
|
||||
@@ -248,15 +284,15 @@ const BrandingPanel: React.FC = () => {
|
||||
{/* Opacity */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-xs text-neutral-500 mb-2">
|
||||
Opacity: {Math.round(branding.opacity * 100)}%
|
||||
Opacity: {Math.round(preferences.opacity * 100)}%
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={branding.opacity}
|
||||
onChange={(e) => updateBranding({ opacity: parseFloat(e.target.value) })}
|
||||
value={preferences.opacity}
|
||||
onChange={(e) => handleUpdatePreferences({ opacity: parseFloat(e.target.value) })}
|
||||
className="w-full h-1.5 bg-white/10 rounded-lg appearance-none cursor-pointer accent-blue-600"
|
||||
/>
|
||||
</div>
|
||||
@@ -264,14 +300,14 @@ const BrandingPanel: React.FC = () => {
|
||||
{/* Padding */}
|
||||
<div>
|
||||
<label className="block text-xs text-neutral-500 mb-2">
|
||||
Padding: {branding.padding}px
|
||||
Padding: {preferences.padding}px
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="8"
|
||||
max="48"
|
||||
value={branding.padding}
|
||||
onChange={(e) => updateBranding({ padding: parseInt(e.target.value) })}
|
||||
value={preferences.padding}
|
||||
onChange={(e) => handleUpdatePreferences({ padding: parseInt(e.target.value) })}
|
||||
className="w-full h-1.5 bg-white/10 rounded-lg appearance-none cursor-pointer accent-blue-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
interface BrandingInfo {
|
||||
name: string;
|
||||
website: string;
|
||||
social: {
|
||||
twitter?: string;
|
||||
linkedin?: string;
|
||||
instagram?: string;
|
||||
github?: string;
|
||||
youtube?: string;
|
||||
tiktok?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface BrandingPreferences {
|
||||
enabled: boolean;
|
||||
position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
||||
showName: boolean;
|
||||
showWebsite: boolean;
|
||||
showSocial: boolean;
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
color: string;
|
||||
opacity: number;
|
||||
padding: number;
|
||||
socialIconSize: number;
|
||||
socialLayout: 'horizontal' | 'vertical';
|
||||
}
|
||||
|
||||
interface BrandingStore {
|
||||
info: BrandingInfo;
|
||||
preferences: BrandingPreferences;
|
||||
|
||||
// Actions
|
||||
updateInfo: (updates: Partial<BrandingInfo>) => void;
|
||||
updateSocial: (platform: string, value: string) => void;
|
||||
updatePreferences: (updates: Partial<BrandingPreferences>) => void;
|
||||
resetToDefaults: () => void;
|
||||
}
|
||||
|
||||
const defaultInfo: BrandingInfo = {
|
||||
name: '',
|
||||
website: '',
|
||||
social: {},
|
||||
};
|
||||
|
||||
const defaultPreferences: BrandingPreferences = {
|
||||
enabled: false,
|
||||
position: 'bottom-right',
|
||||
showName: true,
|
||||
showWebsite: true,
|
||||
showSocial: true,
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
color: '#ffffff',
|
||||
opacity: 0.8,
|
||||
padding: 24,
|
||||
socialIconSize: 20,
|
||||
socialLayout: 'horizontal',
|
||||
};
|
||||
|
||||
export const useBrandingStore = create<BrandingStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
info: defaultInfo,
|
||||
preferences: defaultPreferences,
|
||||
|
||||
updateInfo: (updates) => set((state) => ({
|
||||
info: { ...state.info, ...updates },
|
||||
})),
|
||||
|
||||
updateSocial: (platform, value) => set((state) => ({
|
||||
info: {
|
||||
...state.info,
|
||||
social: { ...state.info.social, [platform]: value },
|
||||
},
|
||||
})),
|
||||
|
||||
updatePreferences: (updates) => set((state) => ({
|
||||
preferences: { ...state.preferences, ...updates },
|
||||
})),
|
||||
|
||||
resetToDefaults: () => set({
|
||||
info: defaultInfo,
|
||||
preferences: defaultPreferences,
|
||||
}),
|
||||
}),
|
||||
{
|
||||
name: 'yvcode-branding',
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -33,6 +33,8 @@ export interface Branding {
|
||||
color: string;
|
||||
opacity: number;
|
||||
padding: number;
|
||||
socialIconSize?: number;
|
||||
socialLayout?: 'horizontal' | 'vertical';
|
||||
}
|
||||
|
||||
export interface BrandStrip {
|
||||
|
||||
Reference in New Issue
Block a user