| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458 |
- import * as React from 'react';
- import { DesignSettings, ThemeName, ButtonStyle, ButtonShape, FontFamily, BackgroundType, BannerType, MediaSource, SideNavSettings, BannerSettings } from '../types';
- import { Icon } from './ui/Icon';
- import { useTranslation } from '../hooks/useI18n';
- interface AccordionProps {
- title: string;
- children?: React.ReactNode;
- defaultOpen?: boolean;
- }
- const Accordion: React.FC<AccordionProps> = ({ title, children, defaultOpen = true }) => {
- const [isOpen, setIsOpen] = React.useState(defaultOpen);
- return (
- <div className="border-b border-gray-200 dark:border-gray-700">
- <button onMouseDown={(e) => e.preventDefault()} onClick={() => setIsOpen(!isOpen)} className="w-full flex justify-between items-center py-4 text-left">
- <h3 className="text-lg font-semibold text-gray-900 dark:text-white">{title}</h3>
- <Icon className={`h-5 w-5 transition-transform text-gray-500 dark:text-gray-400 ${isOpen ? 'rotate-180' : ''}`}><path d="M19 9l-7 7-7-7" /></Icon>
- </button>
- {isOpen && <div className="pb-6 space-y-4">{children}</div>}
- </div>
- );
- };
- const ColorPalettePicker: React.FC<{
- label: string;
- color: string;
- onChange: (color: string) => void;
- themeColors?: string[];
- }> = ({ label, color, onChange, themeColors = ['#ffffff', '#f3f4f6', '#d1d5db', '#6b7280', '#374151', '#111827', '#10b981', '#3b82f6', '#ef4444'] }) => (
- <div className="flex items-center justify-between">
- <label className="text-sm text-gray-600 dark:text-gray-300">{label}</label>
- <div className="flex items-center gap-2">
- <div className="flex gap-1">
- {themeColors.map(c => (
- <button key={c} onMouseDown={(e) => e.preventDefault()} onClick={() => onChange(c)} className={`h-6 w-6 rounded-full border border-gray-300 dark:border-gray-600 ${color.toLowerCase() === c.toLowerCase() ? 'ring-2 ring-brand-primary ring-offset-2 ring-offset-gray-50 dark:ring-offset-gray-900' : ''}`} style={{backgroundColor: c}} />
- ))}
- </div>
- <input type="color" value={color} onChange={e => onChange(e.target.value)} className="w-8 h-8 rounded border-none cursor-pointer bg-transparent" />
- </div>
- </div>
- );
- const predefinedGradients = [
- { name: 'Abyss', value: 'bg-gradient-to-br from-gray-800 to-gray-900' },
- { name: 'Emerald', value: 'bg-gradient-to-br from-green-500 to-teal-600' },
- { name: 'Sunset', value: 'bg-gradient-to-br from-red-500 to-yellow-500' },
- { name: 'Violet', value: 'bg-gradient-to-br from-purple-600 to-indigo-700' },
- ];
- const predefinedBackgrounds: MediaSource[] = [
- { type: 'url', value: 'https://picsum.photos/seed/bg-beach/1200/800' },
- { type: 'url', value: 'https://picsum.photos/seed/bg-music/1200/800' },
- { type: 'url', value: 'https://picsum.photos/seed/bg-city/1200/800' },
- { type: 'url', value: 'https://picsum.photos/seed/bg-dock/1200/800' },
- ];
- const getImageUrl = (source: MediaSource): string => {
- if (source.type === 'url') return source.value;
- if (source.type === 'file') return source.value.previewUrl;
- return ''; // AIGC not applicable for backgrounds
- }
- const themes: { name: string; value: ThemeName }[] = [
- { name: 'Light', value: 'light' }, { name: 'Dark', value: 'dark' }, { name: 'Synthwave', value: 'synthwave' }, { name: 'Retro', value: 'retro' },
- ];
- const fontSizes = [
- { name: 'Small', value: 'text-sm' }, { name: 'Base', value: 'text-base' }, { name: 'Large', value: 'text-lg' },
- ];
- const designTemplates: { name: string, settings: Partial<DesignSettings> & { sideNavSettings: Partial<SideNavSettings>, bannerSettings: Partial<BannerSettings> } }[] = [
- { name: 'Midnight Emerald', settings: { theme: 'dark', fontColor: '#A7F3D0', backgroundType: 'gradient', backgroundValue: 'bg-gradient-to-br from-emerald-900 via-gray-900 to-black', buttonStyle: 'outline', buttonShape: 'rounded', fontFamily: 'sans', sideNavSettings: { backgroundColor: '#064e3b', textColor: '#a7f3d0', activeLinkColor: '#10b981', hoverBackgroundColor: '#047857', hoverTextColor: '#ffffff', fontFamily: 'sans', fontSize: '14px' }, bannerSettings: { type: 'image', value: '', imageSource: { type: 'url', value: 'https://picsum.photos/seed/emerald-city/1200/300' }, height: 300, width: 'contained' } } },
- { name: 'Aspen Light', settings: { theme: 'light', fontColor: '#374151', backgroundType: 'color', backgroundValue: '#f9fafb', buttonStyle: 'filled', buttonShape: 'pill', fontFamily: 'sans', sideNavSettings: { backgroundColor: '#ffffff', textColor: '#4b5563', activeLinkColor: '#d1d5db', hoverBackgroundColor: '#f3f4f6', hoverTextColor: '#111827', fontFamily: 'sans', fontSize: '14px' }, bannerSettings: { type: 'image', value: '', imageSource: { type: 'url', value: 'https://picsum.photos/seed/aspen/1200/300' }, height: 300, width: 'contained' } } },
- { name: 'Sakura Dream', settings: { theme: 'dark', fontColor: '#fbcfe8', backgroundType: 'gradient', backgroundValue: 'bg-gradient-to-br from-gray-900 via-fuchsia-900/50 to-gray-900', buttonStyle: 'outline', buttonShape: 'pill', fontFamily: 'serif', sideNavSettings: { backgroundColor: '#581c87', textColor: '#fbcfe8', activeLinkColor: '#c026d3', hoverBackgroundColor: '#7e22ce', hoverTextColor: '#ffffff', fontFamily: 'serif', fontSize: '14px' }, bannerSettings: { type: 'image', value: '', imageSource: { type: 'url', value: 'https://picsum.photos/seed/sakura/1200/300' }, height: 300, width: 'contained' } } },
- { name: 'Golden Sands', settings: { theme: 'light', fontColor: '#78350f', backgroundType: 'gradient', backgroundValue: 'bg-gradient-to-br from-amber-100 to-yellow-200', buttonStyle: 'filled', buttonShape: 'rounded', fontFamily: 'serif', sideNavSettings: { backgroundColor: '#fef3c7', textColor: '#92400e', activeLinkColor: '#f59e0b', hoverBackgroundColor: '#fcd34d', hoverTextColor: '#78350f', fontFamily: 'serif', fontSize: '14px' }, bannerSettings: { type: 'image', value: '', imageSource: { type: 'url', value: 'https://picsum.photos/seed/sands/1200/300' }, height: 300, width: 'contained' } } },
- { name: 'Neo Tokyo', settings: { theme: 'dark', fontColor: '#a5b4fc', backgroundType: 'gradient', backgroundValue: 'bg-gradient-to-br from-black via-indigo-900 to-fuchsia-800', buttonStyle: 'outline', buttonShape: 'square', fontFamily: 'mono', sideNavSettings: { backgroundColor: '#1e1b4b', textColor: '#c7d2fe', activeLinkColor: '#4f46e5', hoverBackgroundColor: '#3730a3', hoverTextColor: '#ffffff', fontFamily: 'mono', fontSize: '14px' }, bannerSettings: { type: 'image', value: '', imageSource: { type: 'url', value: 'https://picsum.photos/seed/tokyo-night/1200/300' }, height: 300, width: 'contained' } } },
- { name: 'Arctic Dawn', settings: { theme: 'light', fontColor: '#1e3a8a', backgroundType: 'gradient', backgroundValue: 'bg-gradient-to-br from-blue-100 via-white to-blue-50', buttonStyle: 'filled', buttonShape: 'pill', fontFamily: 'sans', sideNavSettings: { backgroundColor: '#eff6ff', textColor: '#1e3a8a', activeLinkColor: '#60a5fa', hoverBackgroundColor: '#dbeafe', hoverTextColor: '#1e293b', fontFamily: 'sans', fontSize: '14px' }, bannerSettings: { type: 'image', value: '', imageSource: { type: 'url', value: 'https://picsum.photos/seed/arctic/1200/300' }, height: 300, width: 'contained' } } },
- { name: 'Mahogany', settings: { theme: 'dark', fontColor: '#fed7aa', backgroundType: 'color', backgroundValue: '#292524', buttonStyle: 'filled', buttonShape: 'square', fontFamily: 'serif', sideNavSettings: { backgroundColor: '#44403c', textColor: '#fed7aa', activeLinkColor: '#f97316', hoverBackgroundColor: '#9a3412', hoverTextColor: '#ffffff', fontFamily: 'serif', fontSize: '14px' }, bannerSettings: { type: 'image', value: '', imageSource: { type: 'url', value: 'https://picsum.photos/seed/mahogany/1200/300' }, height: 300, width: 'contained' } } },
- { name: 'Oceanic Deep', settings: { theme: 'dark', fontColor: '#67e8f9', backgroundType: 'gradient', backgroundValue: 'bg-gradient-to-tr from-gray-900 to-teal-900', buttonStyle: 'outline', buttonShape: 'rounded', fontFamily: 'sans', sideNavSettings: { backgroundColor: '#134e4a', textColor: '#99f6e4', activeLinkColor: '#0d9488', hoverBackgroundColor: '#115e59', hoverTextColor: '#ccfbf1', fontFamily: 'sans', fontSize: '14px' }, bannerSettings: { type: 'image', value: '', imageSource: { type: 'url', value: 'https://picsum.photos/seed/oceanic/1200/300' }, height: 300, width: 'contained' } } },
- { name: 'Scarlet & Slate', settings: { theme: 'dark', fontColor: '#f1f5f9', backgroundType: 'color', backgroundValue: '#1e293b', buttonStyle: 'filled', buttonShape: 'square', fontFamily: 'sans', sideNavSettings: { backgroundColor: '#0f172a', textColor: '#cbd5e1', activeLinkColor: '#dc2626', hoverBackgroundColor: '#475569', hoverTextColor: '#ffffff', fontFamily: 'sans', fontSize: '14px' }, bannerSettings: { type: 'image', value: '', imageSource: { type: 'url', value: 'https://picsum.photos/seed/scarlet-slate/1200/300' }, height: 300, width: 'contained' } } },
- { name: 'Minimalist White', settings: { theme: 'light', fontColor: '#334155', backgroundType: 'color', backgroundValue: '#ffffff', buttonStyle: 'outline', buttonShape: 'square', fontFamily: 'sans', sideNavSettings: { backgroundColor: '#f1f5f9', textColor: '#475569', activeLinkColor: '#94a3b8', hoverBackgroundColor: '#e2e8f0', hoverTextColor: '#0f172a', fontFamily: 'sans', fontSize: '14px' }, bannerSettings: { type: 'image', value: '', imageSource: { type: 'url', value: 'https://picsum.photos/seed/minimal-white/1200/300' }, height: 300, width: 'contained' } } },
- ];
- const extendedDesignTemplates: { name: string, settings: Partial<DesignSettings> & { sideNavSettings: Partial<SideNavSettings>, bannerSettings: Partial<BannerSettings> } }[] = [
- { name: 'Cyberpunk Sunset', settings: { theme: 'dark' as ThemeName, fontColor: '#f472b6', backgroundType: 'gradient' as BackgroundType, backgroundValue: 'bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-900', buttonStyle: 'outline' as ButtonStyle, buttonShape: 'square' as ButtonShape, fontFamily: 'mono' as FontFamily, sideNavSettings: { backgroundColor: '#2b1c58', textColor: '#f472b6', activeLinkColor: '#a855f7', hoverBackgroundColor: '#4c1d95', hoverTextColor: '#f5d0fe', fontFamily: 'mono' as FontFamily, fontSize: '14px' }, bannerSettings: { type: 'image' as BannerType, value: '', imageSource: { type: 'url' as const, value: 'https://picsum.photos/seed/cyberpunk/1200/300' }, height: 300, width: 'contained' as 'contained' } } },
- { name: 'Rustic Charm', settings: { theme: 'light' as ThemeName, fontColor: '#3f3f46', backgroundType: 'color' as BackgroundType, backgroundValue: '#f5f5f4', buttonStyle: 'filled' as ButtonStyle, buttonShape: 'rounded' as ButtonShape, fontFamily: 'serif' as FontFamily, sideNavSettings: { backgroundColor: '#e7e5e4', textColor: '#57534e', activeLinkColor: '#a8a29e', hoverBackgroundColor: '#d6d3d1', hoverTextColor: '#1c1917', fontFamily: 'serif' as FontFamily, fontSize: '14px' }, bannerSettings: { type: 'image' as BannerType, value: '', imageSource: { type: 'url' as const, value: 'https://picsum.photos/seed/rustic/1200/300' }, height: 300, width: 'contained' as 'contained' } } },
- { name: 'Ocean Breeze', settings: { theme: 'light' as ThemeName, fontColor: '#0c4a6e', backgroundType: 'gradient' as BackgroundType, backgroundValue: 'bg-gradient-to-br from-sky-200 to-blue-200', buttonStyle: 'filled' as ButtonStyle, buttonShape: 'pill' as ButtonShape, fontFamily: 'sans' as FontFamily, sideNavSettings: { backgroundColor: '#e0f2fe', textColor: '#075985', activeLinkColor: '#38bdf8', hoverBackgroundColor: '#bae6fd', hoverTextColor: '#0c4a6e', fontFamily: 'sans' as FontFamily, fontSize: '14px' }, bannerSettings: { type: 'image' as BannerType, value: '', imageSource: { type: 'url' as const, value: 'https://picsum.photos/seed/breeze/1200/300' }, height: 300, width: 'contained' as 'contained' } } },
- { name: 'Vintage Noir', settings: { theme: 'dark' as ThemeName, fontColor: '#d6d3d1', backgroundType: 'color' as BackgroundType, backgroundValue: '#1c1917', buttonStyle: 'outline' as ButtonStyle, buttonShape: 'square' as ButtonShape, fontFamily: 'serif' as FontFamily, sideNavSettings: { backgroundColor: '#292524', textColor: '#a8a29e', activeLinkColor: '#facc15', hoverBackgroundColor: '#44403c', hoverTextColor: '#fafafa', fontFamily: 'serif' as FontFamily, fontSize: '14px' }, bannerSettings: { type: 'image' as BannerType, value: '', imageSource: { type: 'url' as const, value: 'https://picsum.photos/seed/noir/1200/300' }, height: 300, width: 'contained' as 'contained' } } },
- { name: 'Jungle Expedition', settings: { theme: 'light' as ThemeName, fontColor: '#1a2e05', backgroundType: 'gradient' as BackgroundType, backgroundValue: 'bg-gradient-to-br from-lime-100 via-green-100 to-lime-200', buttonStyle: 'filled' as ButtonStyle, buttonShape: 'pill' as ButtonShape, fontFamily: 'sans' as FontFamily, sideNavSettings: { backgroundColor: '#dcfce7', textColor: '#166534', activeLinkColor: '#4ade80', hoverBackgroundColor: '#bbf7d0', hoverTextColor: '#14532d', fontFamily: 'sans' as FontFamily, fontSize: '14px' }, bannerSettings: { type: 'image' as BannerType, value: '', imageSource: { type: 'url' as const, value: 'https://picsum.photos/seed/jungle/1200/300' }, height: 300, width: 'contained' as 'contained' } } },
- { name: 'Cosmic Rift', settings: { theme: 'dark' as ThemeName, fontColor: '#e9d5ff', backgroundType: 'gradient' as BackgroundType, backgroundValue: 'bg-[radial-gradient(ellipse_at_bottom,_var(--tw-gradient-stops))] from-gray-700 via-gray-900 to-black', buttonStyle: 'outline' as ButtonStyle, buttonShape: 'pill' as ButtonShape, fontFamily: 'mono' as FontFamily, sideNavSettings: { backgroundColor: '#1e293b', textColor: '#93c5fd', activeLinkColor: '#60a5fa', hoverBackgroundColor: '#334155', hoverTextColor: '#dbeafe', fontFamily: 'mono' as FontFamily, fontSize: '14px' }, bannerSettings: { type: 'image' as BannerType, value: '', imageSource: { type: 'url' as const, value: 'https://picsum.photos/seed/cosmic/1200/300' }, height: 300, width: 'contained' as 'contained' } } },
- { name: 'Autumn Glow', settings: { theme: 'light' as ThemeName, fontColor: '#422006', backgroundType: 'color' as BackgroundType, backgroundValue: '#fff7ed', buttonStyle: 'filled' as ButtonStyle, buttonShape: 'rounded' as ButtonShape, fontFamily: 'serif' as FontFamily, sideNavSettings: { backgroundColor: '#fed7aa', textColor: '#9a3412', activeLinkColor: '#fb923c', hoverBackgroundColor: '#fdba74', hoverTextColor: '#7c2d12', fontFamily: 'serif' as FontFamily, fontSize: '14px' }, bannerSettings: { type: 'image' as BannerType, value: '', imageSource: { type: 'url' as const, value: 'https://picsum.photos/seed/autumn/1200/300' }, height: 300, width: 'contained' as 'contained' } } },
- { name: 'Concrete Jungle', settings: { theme: 'dark' as ThemeName, fontColor: '#e5e7eb', backgroundType: 'color' as BackgroundType, backgroundValue: '#27272a', buttonStyle: 'outline' as ButtonStyle, buttonShape: 'square' as ButtonShape, fontFamily: 'sans' as FontFamily, sideNavSettings: { backgroundColor: '#3f3f46', textColor: '#d4d4d8', activeLinkColor: '#a1a1aa', hoverBackgroundColor: '#52525b', hoverTextColor: '#fafafa', fontFamily: 'sans' as FontFamily, fontSize: '14px' }, bannerSettings: { type: 'image' as BannerType, value: '', imageSource: { type: 'url' as const, value: 'https://picsum.photos/seed/concrete/1200/300' }, height: 300, width: 'contained' as 'contained' } } },
- { name: 'Pastel Playground', settings: { theme: 'light' as ThemeName, fontColor: '#57534e', backgroundType: 'color' as BackgroundType, backgroundValue: '#fdf2f8', buttonStyle: 'filled' as ButtonStyle, buttonShape: 'pill' as ButtonShape, fontFamily: 'sans' as FontFamily, sideNavSettings: { backgroundColor: '#fce7f3', textColor: '#9d174d', activeLinkColor: '#f472b6', hoverBackgroundColor: '#fbcfe8', hoverTextColor: '#831843', fontFamily: 'sans' as FontFamily, fontSize: '14px' }, bannerSettings: { type: 'image' as BannerType, value: '', imageSource: { type: 'url' as const, value: 'https://picsum.photos/seed/pastel/1200/300' }, height: 300, width: 'contained' as 'contained' } } },
- { name: 'Deep Space', settings: { theme: 'dark' as ThemeName, fontColor: '#bfdbfe', backgroundType: 'gradient' as BackgroundType, backgroundValue: 'bg-gradient-to-tl from-gray-900 via-slate-900 to-blue-900', buttonStyle: 'outline' as ButtonStyle, buttonShape: 'rounded' as ButtonShape, fontFamily: 'mono' as FontFamily, sideNavSettings: { backgroundColor: '#0f172a', textColor: '#60a5fa', activeLinkColor: '#3b82f6', hoverBackgroundColor: '#1e293b', hoverTextColor: '#93c5fd', fontFamily: 'mono' as FontFamily, fontSize: '14px' }, bannerSettings: { type: 'image' as BannerType, value: '', imageSource: { type: 'url' as const, value: 'https://picsum.photos/seed/space/1200/300' }, height: 300, width: 'contained' as 'contained' } } },
- ].concat(Array.from({ length: 20 }).map((_, i) => ({
- name: `Pro Theme ${i + 1}`,
- settings: {
- theme: (['dark', 'light'] as const)[i % 2],
- fontColor: i % 2 === 0 ? '#e0e7ff' : '#1e3a8a',
- backgroundType: 'gradient' as const,
- backgroundValue: "bg-gradient-to-br from-slate-800 to-indigo-900",
- buttonStyle: (['filled', 'outline'] as const)[i % 2],
- buttonShape: (['rounded', 'pill', 'square'] as const)[i % 3],
- fontFamily: (['sans', 'serif', 'mono'] as const)[i % 3],
- sideNavSettings: {
- backgroundColor: i % 2 === 0 ? '#1e293b' : '#eef2ff',
- textColor: i % 2 === 0 ? '#94a3b8' : '#312e81',
- activeLinkColor: i % 2 === 0 ? '#4f46e5' : '#818cf8',
- hoverBackgroundColor: i % 2 === 0 ? '#334155' : '#c7d2fe',
- hoverTextColor: i % 2 === 0 ? '#e0e7ff' : '#1e1b4b',
- fontFamily: (['sans', 'serif', 'mono'] as const)[i % 3],
- fontSize: '14px',
- },
- bannerSettings: { type: 'image' as const, value: '', imageSource: { type: 'url' as const, value: `https://picsum.photos/seed/pro-theme-${i + 1}/1200/300` }, height: 300, width: 'contained' as const },
- }
- })));
- interface DesignEditorProps {
- design: DesignSettings;
- setDesign: (newDesign: DesignSettings) => void;
- }
- const DesignEditor: React.FC<DesignEditorProps> = ({ design, setDesign }) => {
- const { t } = useTranslation();
- const [isThemeModalOpen, setIsThemeModalOpen] = React.useState(false);
- const updateDesign = (key: keyof DesignSettings, value: any) => {
- setDesign({ ...design, [key]: value });
- };
- const updateNested = (parentKey: keyof DesignSettings, childKey: string, value: any) => {
- updateDesign(parentKey, { ...(design[parentKey] as any), [childKey]: value });
- };
- const applyThemeTemplate = (templateSettings: any) => {
- const newSettings = JSON.parse(JSON.stringify(templateSettings));
- const fullNewDesign = {
- ...design,
- ...newSettings,
- bannerSettings: {
- ...design.bannerSettings,
- ...newSettings.bannerSettings,
- },
- sideNavSettings: {
- ...design.sideNavSettings,
- ...newSettings.sideNavSettings,
- },
- };
-
- if (!fullNewDesign.bannerSettings.height) fullNewDesign.bannerSettings.height = 300;
- if (!fullNewDesign.bannerSettings.width) fullNewDesign.bannerSettings.width = 'contained';
- setDesign(fullNewDesign);
- };
- const handleUploadImage = () => {
- const newImage: MediaSource = {
- type: 'file',
- value: {
- name: `user_upload_${Date.now()}.jpg`,
- size: 123456,
- previewUrl: `https://picsum.photos/seed/user-bg-${Date.now()}/400/300`
- }
- };
- const updatedImages = [...(design.userBackgroundImages || []), newImage];
- setDesign({ ...design, userBackgroundImages: updatedImages });
- };
- return (
- <div className="space-y-1">
- <Accordion title={t('design_editor.templates')} defaultOpen={true}>
- <div className="grid grid-cols-2 gap-3">
- {designTemplates.map(template => (
- <button key={template.name} onMouseDown={(e) => e.preventDefault()} onClick={() => applyThemeTemplate(template.settings)} className="p-2 rounded-lg border-2 border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-brand-primary transition-colors text-left">
- <div className="h-12 w-full rounded-md mb-2 bg-cover bg-center" style={{backgroundImage: `url(${(template.settings.bannerSettings as any)?.imageSource?.value})`}} />
- <p className="font-semibold text-sm">{template.name}</p>
- </button>
- ))}
- <button onClick={() => setIsThemeModalOpen(true)} className="col-span-2 p-4 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600 hover:border-brand-primary hover:bg-gray-100 dark:hover:bg-gray-900/50 transition-colors flex items-center justify-center">
- <span className="font-semibold text-gray-500 dark:text-gray-300">{t('design_editor.more_themes')}</span>
- </button>
- </div>
- </Accordion>
-
- <Accordion title={t('design_editor.profile')}>
- <div className="space-y-2 p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
- <h4 className="font-semibold mb-2">{t('design_editor.avatar')}</h4>
- <div className="flex items-center gap-2">
- <label className="text-xs text-gray-500 dark:text-gray-400">{t('design_editor.source')}:</label>
- <button onMouseDown={(e) => e.preventDefault()} onClick={() => {
- const newSource: MediaSource = { type: 'url', value: '' };
- updateDesign('avatarSource', newSource);
- }} className={`px-3 py-1 text-xs rounded-md ${design.avatarSource?.type === 'url' ? 'bg-brand-primary text-white' : 'bg-gray-200 dark:bg-gray-600'}`}>URL</button>
- <button onMouseDown={(e) => e.preventDefault()} onClick={() => {
- const newSource: MediaSource = { type: 'file', value: { name: 'avatar.jpg', size: 12345, previewUrl: `https://api.dicebear.com/8.x/adventurer/svg?seed=NewAvatar` } };
- updateDesign('avatarSource', newSource);
- }} className={`px-3 py-1 text-xs rounded-md ${design.avatarSource?.type === 'file' ? 'bg-brand-primary text-white' : 'bg-gray-200 dark:bg-gray-600'}`}>{t('link_editor.upload')}</button>
- </div>
- {design.avatarSource?.type === 'url' ?
- <input type="url" placeholder="Enter image URL..." value={design.avatarSource.value} onChange={e => {
- const newSource: MediaSource = { type: 'url', value: e.target.value };
- updateDesign('avatarSource', newSource);
- }} className="w-full bg-gray-200 dark:bg-gray-700 p-2 rounded-md border border-gray-300 dark:border-gray-600" />
- :
- design.avatarSource?.type === 'file' ?
- <div className="text-sm p-2 bg-gray-200 dark:bg-gray-700 rounded-md">Simulated Upload: {design.avatarSource.value.name}</div>
- : <div className="text-sm p-2 text-gray-500">No avatar set.</div>
- }
- </div>
- </Accordion>
- <Accordion title={t('design_editor.appearance')}>
- <>
- <div className="grid grid-cols-2 gap-4">
- {themes.map(th => <button key={th.value} onMouseDown={(e) => e.preventDefault()} onClick={() => updateDesign('theme', th.value)} className={`p-4 rounded-lg border-2 transition-colors ${design.theme === th.value ? 'border-brand-primary bg-gray-200 dark:bg-gray-700' : 'border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-brand-primary'}`}><p className="font-semibold">{th.name}</p></button>)}
- <button onMouseDown={(e) => e.preventDefault()} onClick={() => updateDesign('theme', 'custom')} className={`p-4 rounded-lg border-2 transition-colors ${design.theme === 'custom' ? 'border-brand-primary bg-gray-200 dark:bg-gray-700' : 'border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-brand-primary'}`}><p className="font-semibold">{t('design_editor.custom')}</p></button>
- </div>
- {design.theme === 'custom' && (
- <div className="p-4 bg-gray-100 dark:bg-gray-800 rounded-lg space-y-3 mt-4">
- <ColorPalettePicker label={t('design_editor.background_color')} color={design.customThemeColors.background} onChange={c => updateNested('customThemeColors', 'background', c)} />
- <ColorPalettePicker label={t('design_editor.text_color')} color={design.customThemeColors.text} onChange={c => updateNested('customThemeColors', 'text', c)} />
- <ColorPalettePicker label={t('design_editor.button_color')} color={design.customThemeColors.button} onChange={c => updateNested('customThemeColors', 'button', c)} />
- <ColorPalettePicker label={t('design_editor.button_text')} color={design.customThemeColors.buttonText} onChange={c => updateNested('customThemeColors', 'buttonText', c)} />
- </div>
- )}
- </>
- </Accordion>
-
- <Accordion title={t('design_editor.typography')}>
- <div className="space-y-4">
- <ColorPalettePicker label={t('design_editor.font_color')} color={design.fontColor} onChange={c => updateDesign('fontColor', c)} />
- <div>
- <label className="text-sm text-gray-600 dark:text-gray-300">{t('design_editor.font_size')}</label>
- <select value={design.fontSize} onChange={e => updateDesign('fontSize', e.target.value)} className="w-full bg-gray-100 dark:bg-gray-700 p-2 rounded-md border border-gray-300 dark:border-gray-600 mt-1">
- {fontSizes.map(fs => <option key={fs.value} value={fs.value}>{fs.name}</option>)}
- </select>
- </div>
- <div>
- <label className="text-sm text-gray-600 dark:text-gray-300">{t('design_editor.font_family')}</label>
- <select value={design.fontFamily} onChange={e => updateDesign('fontFamily', e.target.value as FontFamily)} className="w-full bg-gray-100 dark:bg-gray-700 p-2 rounded-md border border-gray-300 dark:border-gray-600 mt-1">
- <option value="sans">Sans-Serif</option><option value="serif">Serif</option><option value="mono">Monospaced</option>
- </select>
- </div>
- </div>
- </Accordion>
- <Accordion title={t('design_editor.buttons')}>
- <div className="space-y-4">
- <div>
- <h4 className="text-sm font-semibold mb-2">{t('design_editor.button_style')}</h4>
- <div className="flex gap-2">
- {(['filled', 'outline'] as ButtonStyle[]).map(style => <button key={style} onMouseDown={(e) => e.preventDefault()} onClick={() => updateDesign('buttonStyle', style)} className={`px-4 py-2 text-sm rounded-md capitalize flex-1 border-2 ${design.buttonStyle === style ? 'bg-brand-primary border-brand-primary text-white' : 'bg-transparent border-gray-300 dark:border-gray-600'}`}>{style}</button>)}
- </div>
- </div>
- <div>
- <h4 className="text-sm font-semibold mb-2">{t('design_editor.button_shape')}</h4>
- <div className="flex gap-2">
- {(['rounded', 'pill', 'square'] as ButtonShape[]).map(shape => <button key={shape} onMouseDown={(e) => e.preventDefault()} onClick={() => updateDesign('buttonShape', shape)} className={`px-4 py-2 text-sm flex-1 border-2 ${design.buttonShape === shape ? 'bg-brand-primary border-brand-primary text-white' : 'bg-transparent border-gray-300 dark:border-gray-600'} ${shape === 'rounded' ? 'rounded-md' : shape === 'pill' ? 'rounded-full' : ''}`}>{shape}</button>)}
- </div>
- </div>
- </div>
- </Accordion>
-
- <Accordion title={t('design_editor.background')}>
- <div>
- <div className="flex gap-2 mb-4">
- {(['color', 'gradient', 'image'] as BackgroundType[]).map(type =>
- <button key={type} onMouseDown={(e) => e.preventDefault()} onClick={() => {
- let newValue = design.backgroundValue;
- if (type === 'color' && design.backgroundType !== 'color') newValue = '#1f2937';
- if (type === 'gradient' && design.backgroundType !== 'gradient') newValue = predefinedGradients[0].value;
- if (type === 'image' && design.backgroundType !== 'image') {
- const allImages = [...predefinedBackgrounds, ...(design.userBackgroundImages || [])];
- newValue = allImages.length > 0 ? getImageUrl(allImages[0]) : '';
- };
- setDesign({ ...design, backgroundType: type, backgroundValue: newValue });
- }}
- className={`px-4 py-2 text-sm rounded-md capitalize flex-1 border-2 ${design.backgroundType === type ? 'bg-brand-primary border-brand-primary text-white' : 'bg-transparent border-gray-300 dark:border-gray-600'}`}>{type}</button>
- )}
- </div>
- {design.backgroundType === 'color' && <ColorPalettePicker label={t('design_editor.background_color')} color={design.backgroundValue} onChange={c => updateDesign('backgroundValue', c)} />}
- {design.backgroundType === 'gradient' && (<div className="grid grid-cols-2 gap-4">{predefinedGradients.map(bg => <button key={bg.name} onMouseDown={(e) => e.preventDefault()} onClick={() => updateDesign('backgroundValue', bg.value)} className={`h-16 rounded-lg ${bg.value} flex items-center justify-center text-white font-semibold transition-all ${design.backgroundValue === bg.value ? 'ring-2 ring-brand-primary ring-offset-2 ring-offset-gray-50 dark:ring-offset-gray-900' : ''}`}>{bg.name}</button>)}</div>)}
- {design.backgroundType === 'image' && (
- <div className="space-y-4">
- <div className="grid grid-cols-2 gap-3">
- {[...predefinedBackgrounds, ...(design.userBackgroundImages || [])].map(bg => {
- const url = getImageUrl(bg);
- const isSelected = design.backgroundValue === url;
- return (
- <button key={url} onMouseDown={(e) => e.preventDefault()} onClick={() => updateDesign('backgroundValue', url)} className={`relative group rounded-lg overflow-hidden border-2 transition-all h-24 ${isSelected ? 'border-brand-primary' : 'border-transparent'}`}>
- <img src={url} className="w-full h-full object-cover" alt="Background option"/>
- {isSelected && <div className="absolute inset-0 bg-black/50 flex items-center justify-center"><Icon className="h-6 w-6 text-white"><path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7"/></Icon></div>}
- </button>
- )
- })}
- </div>
- <button onClick={handleUploadImage} className="w-full p-3 bg-gray-200 dark:bg-gray-700 rounded-lg text-sm font-semibold hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
- {t('design_editor.upload_image')}
- </button>
- </div>
- )}
- </div>
- </Accordion>
-
- <Accordion title={t('design_editor.enterprise_layout')} defaultOpen={false}>
- <div className="space-y-6">
- <div>
- <h4 className="font-semibold mb-3">{t('design_editor.banner')}</h4>
- <div className="space-y-4 p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
- <div>
- <label className="text-sm text-gray-600 dark:text-gray-300 block mb-2">{t('design_editor.banner_type')}</label>
- <div className="flex gap-2">
- {(['color', 'gradient', 'image', 'none'] as BannerType[]).map(type =>
- <button key={type} onMouseDown={(e) => e.preventDefault()} onClick={() => {
- let newSettings = { ...design.bannerSettings, type };
- if (type === 'color' && design.bannerSettings.type !== 'color') newSettings.value = '#374151';
- if (type === 'gradient' && design.bannerSettings.type !== 'gradient') newSettings.value = predefinedGradients[0].value;
- if (type === 'image' && design.bannerSettings.type !== 'image') {
- if (!newSettings.imageSource) newSettings.imageSource = { type: 'url', value: '' };
- }
- updateDesign('bannerSettings', newSettings);
- }} className={`px-4 py-2 text-sm rounded-md capitalize flex-1 border-2 ${design.bannerSettings.type === type ? 'bg-brand-primary border-brand-primary text-white' : 'bg-transparent border-gray-300 dark:border-gray-600'}`}>{type}</button>
- )}
- </div>
- </div>
- {design.bannerSettings.type !== 'none' && (
- <>
- <div>
- <label className="text-sm text-gray-600 dark:text-gray-300 block mb-2">{t('design_editor.banner_width')}</label>
- <div className="flex gap-2">
- <button onMouseDown={(e) => e.preventDefault()} onClick={() => updateNested('bannerSettings', 'width', 'contained')} className={`px-4 py-2 text-sm rounded-md capitalize flex-1 border-2 ${design.bannerSettings.width === 'contained' ? 'bg-brand-primary border-brand-primary text-white' : 'bg-transparent border-gray-300 dark:border-gray-600'}`}>{t('design_editor.contained')}</button>
- <button onMouseDown={(e) => e.preventDefault()} onClick={() => updateNested('bannerSettings', 'width', 'full')} className={`px-4 py-2 text-sm rounded-md capitalize flex-1 border-2 ${design.bannerSettings.width === 'full' ? 'bg-brand-primary border-brand-primary text-white' : 'bg-transparent border-gray-300 dark:border-gray-600'}`}>{t('design_editor.full_width')}</button>
- </div>
- </div>
- <div>
- <label htmlFor="bannerHeight" className="text-sm text-gray-600 dark:text-gray-300 block mb-1">{t('design_editor.banner_height')}</label>
- <input id="bannerHeight" type="number" value={design.bannerSettings.height} onChange={e => updateNested('bannerSettings', 'height', parseInt(e.target.value, 10) || 0)} className="w-full bg-gray-200 dark:bg-gray-700 p-2 rounded-md border border-gray-300 dark:border-gray-600"/>
- </div>
- {design.bannerSettings.type === 'color' && <ColorPalettePicker label={t('design_editor.banner_color')} color={design.bannerSettings.value} onChange={c => updateNested('bannerSettings', 'value', c)} />}
- {design.bannerSettings.type === 'gradient' && (<div className="grid grid-cols-2 gap-4">{predefinedGradients.map(bg => <button key={bg.name} onMouseDown={(e) => e.preventDefault()} onClick={() => updateNested('bannerSettings', 'value', bg.value)} className={`h-16 rounded-lg ${bg.value} flex items-center justify-center text-white font-semibold transition-all ${design.bannerSettings.value === bg.value ? 'ring-2 ring-brand-primary ring-offset-2 ring-offset-gray-50 dark:ring-offset-gray-900' : ''}`}>{bg.name}</button>)}</div>)}
- {design.bannerSettings.type === 'image' && (
- <div className="space-y-2">
- <div className="flex items-center gap-2">
- <label className="text-xs text-gray-500 dark:text-gray-400">{t('design_editor.source')}:</label>
- <button onMouseDown={(e) => e.preventDefault()} onClick={() => {
- const newSource: MediaSource = { type: 'url', value: '' };
- updateNested('bannerSettings', 'imageSource', newSource);
- }} className={`px-3 py-1 text-xs rounded-md ${design.bannerSettings.imageSource?.type === 'url' ? 'bg-brand-primary text-white' : 'bg-gray-200 dark:bg-gray-600'}`}>URL</button>
- <button onMouseDown={(e) => e.preventDefault()} onClick={() => {
- const newSource: MediaSource = { type: 'file', value: { name: 'banner.jpg', size: 12345, previewUrl: `https://picsum.photos/seed/upload-${Date.now()}/1200/300` } };
- updateNested('bannerSettings', 'imageSource', newSource);
- }} className={`px-3 py-1 text-xs rounded-md ${design.bannerSettings.imageSource?.type === 'file' ? 'bg-brand-primary text-white' : 'bg-gray-200 dark:bg-gray-600'}`}>{t('link_editor.upload')}</button>
- </div>
- {design.bannerSettings.imageSource?.type === 'url' ?
- <input type="url" placeholder="Enter image URL..." value={design.bannerSettings.imageSource.value} onChange={e => {
- const newSource: MediaSource = { type: 'url', value: e.target.value };
- updateNested('bannerSettings', 'imageSource', newSource);
- }} className="w-full bg-gray-200 dark:bg-gray-700 p-2 rounded-md border border-gray-300 dark:border-gray-600" />
- :
- design.bannerSettings.imageSource?.type === 'file' ?
- <div className="text-sm p-2 bg-gray-200 dark:bg-gray-700 rounded-md">Simulated Upload: {design.bannerSettings.imageSource.value.name}</div>
- : null
- }
- </div>
- )}
- </>
- )}
- </div>
- </div>
- <div>
- <h4 className="font-semibold mb-3">{t('design_editor.side_nav')}</h4>
- <div className="space-y-3 p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
- <div className="grid grid-cols-2 gap-4">
- <div>
- <label className="text-sm text-gray-600 dark:text-gray-300">{t('design_editor.font_family')}</label>
- <select value={design.sideNavSettings.fontFamily} onChange={e => updateNested('sideNavSettings', 'fontFamily', e.target.value as FontFamily)} className="w-full bg-gray-200 dark:bg-gray-700 p-2 rounded-md border border-gray-300 dark:border-gray-600 mt-1">
- <option value="sans">Sans-Serif</option><option value="serif">Serif</option><option value="mono">Monospaced</option>
- </select>
- </div>
- <div>
- <label className="text-sm text-gray-600 dark:text-gray-300">{t('design_editor.font_size')}</label>
- <input type="text" placeholder="e.g., 14px" value={design.sideNavSettings.fontSize} onChange={e => updateNested('sideNavSettings', 'fontSize', e.target.value)} className="w-full bg-gray-200 dark:bg-gray-700 p-2 rounded-md border border-gray-300 dark:border-gray-600 mt-1" />
- </div>
- </div>
- <div>
- <label className="text-sm text-gray-600 dark:text-gray-300 block mb-2">{t('design_editor.position')}</label>
- <div className="flex gap-2">
- {(['normal', 'top', 'center'] as const).map(style =>
- <button key={style} onMouseDown={(e) => e.preventDefault()} onClick={() => updateNested('sideNavSettings', 'navFloatStyle', style)}
- className={`px-4 py-2 text-sm rounded-md capitalize flex-1 border-2 ${(design.sideNavSettings.navFloatStyle || 'normal') === style ? 'bg-brand-primary border-brand-primary text-white' : 'bg-transparent border-gray-300 dark:border-gray-600'}`}>
- {style === 'top' ? t('design_editor.float_top') : style === 'center' ? t('design_editor.float_center') : t('design_editor.normal')}
- </button>
- )}
- </div>
- </div>
- <div>
- <label className="text-sm text-gray-600 dark:text-gray-300 block mb-2">{t('design_editor.background_style')}</label>
- <div className="flex gap-2">
- {(['compact', 'full'] as const).map(style =>
- <button key={style} onMouseDown={(e) => e.preventDefault()} onClick={() => updateNested('sideNavSettings', 'navBackgroundStyle', style)}
- className={`px-4 py-2 text-sm rounded-md capitalize flex-1 border-2 ${(design.sideNavSettings.navBackgroundStyle || 'compact') === style ? 'bg-brand-primary border-brand-primary text-white' : 'bg-transparent border-gray-300 dark:border-gray-600'}`}>
- {style === 'full' ? t('design_editor.full_height') : t('design_editor.compact')}
- </button>
- )}
- </div>
- </div>
- <ColorPalettePicker label={t('design_editor.background_color')} color={design.sideNavSettings.backgroundColor} onChange={c => updateNested('sideNavSettings', 'backgroundColor', c)} />
- <ColorPalettePicker label={t('design_editor.text_color')} color={design.sideNavSettings.textColor} onChange={c => updateNested('sideNavSettings', 'textColor', c)} />
- <ColorPalettePicker label={t('design_editor.active_link')} color={design.sideNavSettings.activeLinkColor} onChange={c => updateNested('sideNavSettings', 'activeLinkColor', c)} />
- <ColorPalettePicker label={t('design_editor.hover_bg')} color={design.sideNavSettings.hoverBackgroundColor} onChange={c => updateNested('sideNavSettings', 'hoverBackgroundColor', c)} />
- <ColorPalettePicker label={t('design_editor.hover_text')} color={design.sideNavSettings.hoverTextColor} onChange={c => updateNested('sideNavSettings', 'hoverTextColor', c)} />
- </div>
- </div>
- <div>
- <h4 className="font-semibold mb-3">{t('design_editor.chat_widget')}</h4>
- <div className="space-y-3 p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
- <ColorPalettePicker label={t('design_editor.icon_color')} color={design.chatWidgetSettings.iconColor} onChange={c => updateNested('chatWidgetSettings', 'iconColor', c)} />
- <ColorPalettePicker label={t('design_editor.header_bg')} color={design.chatWidgetSettings.headerBackgroundColor} onChange={c => updateNested('chatWidgetSettings', 'headerBackgroundColor', c)} />
- <ColorPalettePicker label={t('design_editor.header_text')} color={design.chatWidgetSettings.headerTextColor} onChange={c => updateNested('chatWidgetSettings', 'headerTextColor', c)} />
- <ColorPalettePicker label={t('design_editor.panel_bg')} color={design.chatWidgetSettings.panelBackgroundColor} onChange={c => updateNested('chatWidgetSettings', 'panelBackgroundColor', c)} />
- <ColorPalettePicker label={t('design_editor.user_message')} color={design.chatWidgetSettings.userMessageBackgroundColor} onChange={c => updateNested('chatWidgetSettings', 'userMessageBackgroundColor', c)} />
- <ColorPalettePicker label={t('design_editor.user_text')} color={design.chatWidgetSettings.userMessageTextColor} onChange={c => updateNested('chatWidgetSettings', 'userMessageTextColor', c)} />
- <ColorPalettePicker label={t('design_editor.ai_message')} color={design.chatWidgetSettings.aiMessageBackgroundColor} onChange={c => updateNested('chatWidgetSettings', 'aiMessageBackgroundColor', c)} />
- <ColorPalettePicker label={t('design_editor.ai_text')} color={design.chatWidgetSettings.aiMessageTextColor} onChange={c => updateNested('chatWidgetSettings', 'aiMessageTextColor', c)} />
- </div>
- </div>
- </div>
- </Accordion>
- {isThemeModalOpen && (
- <div className="fixed inset-0 bg-black/70 z-50 flex items-center justify-center backdrop-blur-sm" onClick={() => setIsThemeModalOpen(false)}>
- <div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-5xl h-[90vh] p-8 border border-gray-200 dark:border-gray-700 flex flex-col" onClick={e => e.stopPropagation()}>
- <div className="flex justify-between items-center mb-6 flex-shrink-0">
- <h2 className="text-3xl font-bold text-gray-900 dark:text-white">{t('design_editor.more_themes')}</h2>
- <button onClick={() => setIsThemeModalOpen(false)} className="text-gray-400 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white">
- <Icon className="h-8 w-8"><path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /></Icon>
- </button>
- </div>
- <div className="flex-1 overflow-y-auto pr-4 -mr-4">
- <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
- {extendedDesignTemplates.map(template => (
- <button key={template.name} onMouseDown={(e) => e.preventDefault()} onClick={() => { applyThemeTemplate(template.settings); setIsThemeModalOpen(false); }} className="p-2 rounded-lg border-2 border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-brand-primary transition-colors text-left">
- <div className="h-20 w-full rounded-md mb-2 bg-cover bg-center" style={{backgroundImage: `url(${(template.settings.bannerSettings as any)?.imageSource?.value})`}} />
- <p className="font-semibold text-sm">{template.name}</p>
- </button>
- ))}
- </div>
- </div>
- </div>
- </div>
- )}
- </div>
- );
- };
- export default DesignEditor;
|