Initial commit

This commit is contained in:
2025-12-04 22:24:47 +01:00
commit 453ce10494
106 changed files with 17145 additions and 0 deletions

View File

@@ -0,0 +1,217 @@
import { useState, useEffect, useRef } from 'react';
import { useAuth } from '../../contexts/AuthContext';
import { useTranslation } from '../../contexts/LanguageContext';
import { useSidebar } from '../../contexts/SidebarContext';
import { useModules } from '../../contexts/ModulesContext';
import type { ModuleId } from '../../contexts/ModulesContext';
import Feature1Tab from '../../components/admin/Feature1Tab';
import '../../styles/AdminPanel.css';
type TabId = 'feature1' | 'feature2' | 'feature3';
export default function Features() {
const { user: currentUser } = useAuth();
const { t } = useTranslation();
const { toggleMobileMenu } = useSidebar();
const { moduleStates, setModuleEnabled, saveModulesToBackend, hasInitialized } = useModules();
const [activeTab, setActiveTab] = useState<TabId>('feature1');
const hasUserMadeChanges = useRef(false);
const saveRef = useRef(saveModulesToBackend);
const [tooltip, setTooltip] = useState<{ text: string; left: number; visible: boolean }>({
text: '',
left: 0,
visible: false,
});
const handleTabMouseEnter = (text: string, e: React.MouseEvent) => {
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
setTooltip({
text,
left: rect.left + rect.width / 2,
visible: true,
});
};
const handleTabMouseLeave = () => {
setTooltip((prev) => ({ ...prev, visible: false }));
};
// Keep saveRef updated with latest function
useEffect(() => {
saveRef.current = saveModulesToBackend;
}, [saveModulesToBackend]);
if (!currentUser?.is_superuser) {
return null;
}
const handleModuleToggle = async (moduleId: ModuleId, type: 'admin' | 'user', enabled: boolean) => {
hasUserMadeChanges.current = true;
setModuleEnabled(moduleId, type, enabled);
};
// Save changes when moduleStates change, but debounce to avoid too many requests
// Only save if: 1) Backend data has been loaded, and 2) User has made changes
useEffect(() => {
if (!hasInitialized || !hasUserMadeChanges.current) {
return;
}
const timeoutId = setTimeout(() => {
saveRef.current().catch(console.error);
}, 300);
return () => clearTimeout(timeoutId);
}, [moduleStates, hasInitialized]);
// Save on unmount if there are pending changes (empty deps = only on unmount)
useEffect(() => {
return () => {
if (hasUserMadeChanges.current) {
saveRef.current().catch(console.error);
}
};
}, []);
const getModuleDescription = (moduleId: string): string => {
const key = `${moduleId}Desc` as keyof typeof t.admin;
return t.admin[key] || t.admin.moduleDefaultDesc;
};
const renderModuleToggle = (moduleId: ModuleId) => {
const state = moduleStates[moduleId];
const adminEnabled = state?.admin ?? true;
const userEnabled = state?.user ?? true;
return (
<div className="feature-header">
<div className="feature-header-info">
<p>{getModuleDescription(moduleId)}</p>
</div>
<div className="feature-header-actions">
<div className={`feature-status-badge ${adminEnabled ? 'active' : ''}`}>
{adminEnabled ? t.admin.active : t.admin.inactive}
</div>
<div className="toggle-group">
<span className="toggle-label">{t.admin.adminRole}</span>
<label className="toggle-modern">
<input
type="checkbox"
checked={adminEnabled}
onChange={(e) => handleModuleToggle(moduleId, 'admin', e.target.checked)}
/>
<span className="toggle-slider-modern"></span>
</label>
</div>
<div className="toggle-group">
<span className="toggle-label">{t.admin.userRole}</span>
<label className={`toggle-modern ${!adminEnabled ? 'disabled' : ''}`}>
<input
type="checkbox"
checked={userEnabled}
disabled={!adminEnabled}
onChange={(e) => handleModuleToggle(moduleId, 'user', e.target.checked)}
/>
<span className="toggle-slider-modern"></span>
</label>
</div>
</div>
</div>
);
};
const renderTabContent = () => {
switch (activeTab) {
case 'feature1':
return (
<>
{renderModuleToggle('feature1')}
<Feature1Tab />
</>
);
case 'feature2':
return (
<>
{renderModuleToggle('feature2')}
<div className="tab-content-placeholder">
<div className="placeholder-icon">
<span className="material-symbols-outlined">download</span>
</div>
<h3>{t.features.feature2}</h3>
<p>{t.features.comingSoon}</p>
</div>
</>
);
case 'feature3':
return (
<>
{renderModuleToggle('feature3')}
<div className="tab-content-placeholder">
<div className="placeholder-icon">
<span className="material-symbols-outlined">cast</span>
</div>
<h3>{t.features.feature3}</h3>
<p>{t.features.comingSoon}</p>
</div>
</>
);
default:
return null;
}
};
return (
<main className="main-content admin-panel-root">
{/* Tab Tooltip */}
{tooltip.visible && (
<div
className="admin-tab-tooltip"
style={{ left: tooltip.left }}
>
{tooltip.text}
</div>
)}
<div className="admin-tabs-container">
<div className="admin-tabs-slider">
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
<span className="material-symbols-outlined">menu</span>
</button>
<div className="admin-title-section">
<span className="material-symbols-outlined">extension</span>
<span className="admin-title-text">{t.featuresPage.title}</span>
</div>
<div className="admin-tabs-divider"></div>
<button
className={`admin-tab-btn ${activeTab === 'feature1' ? 'active' : ''}`}
onClick={() => setActiveTab('feature1')}
onMouseEnter={(e) => handleTabMouseEnter(t.sidebar.feature1, e)}
onMouseLeave={handleTabMouseLeave}
>
<span className="material-symbols-outlined">playlist_play</span>
<span>{t.sidebar.feature1}</span>
</button>
<button
className={`admin-tab-btn ${activeTab === 'feature2' ? 'active' : ''}`}
onClick={() => setActiveTab('feature2')}
onMouseEnter={(e) => handleTabMouseEnter(t.sidebar.feature2, e)}
onMouseLeave={handleTabMouseLeave}
>
<span className="material-symbols-outlined">download</span>
<span>{t.sidebar.feature2}</span>
</button>
<button
className={`admin-tab-btn ${activeTab === 'feature3' ? 'active' : ''}`}
onClick={() => setActiveTab('feature3')}
onMouseEnter={(e) => handleTabMouseEnter(t.sidebar.feature3, e)}
onMouseLeave={handleTabMouseLeave}
>
<span className="material-symbols-outlined">cast</span>
<span>{t.sidebar.feature3}</span>
</button>
</div>
</div>
<div className="admin-tab-content">
{renderTabContent()}
</div>
</main>
);
}

View File

@@ -0,0 +1,136 @@
import { useState, useEffect } from 'react';
import { useTranslation } from '../../contexts/LanguageContext';
import { useSidebar } from '../../contexts/SidebarContext';
import Sidebar from '../../components/Sidebar';
import '../../styles/Settings.css';
interface Settings {
registration_enabled?: boolean;
show_logo?: boolean;
}
export default function Settings() {
const { t } = useTranslation();
const { toggleMobileMenu } = useSidebar();
const [settings, setSettings] = useState<Settings>({});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
useEffect(() => {
fetchSettings();
}, []);
const fetchSettings = async () => {
try {
const token = localStorage.getItem('token');
const response = await fetch('/api/v1/settings', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (response.ok) {
const data = await response.json();
setSettings(data);
}
} catch (error) {
console.error('Failed to fetch settings:', error);
} finally {
setLoading(false);
}
};
const updateSetting = async (key: string, value: any) => {
setSaving(true);
try {
const token = localStorage.getItem('token');
const response = await fetch(`/api/v1/settings/${key}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ value }),
});
if (response.ok) {
const updatedSetting = await response.json();
setSettings(prev => ({
...prev,
[key]: updatedSetting.value
}));
}
} catch (error) {
console.error('Failed to update setting:', error);
} finally {
setSaving(false);
}
};
const handleRegistrationToggle = (checked: boolean) => {
updateSetting('registration_enabled', checked);
};
if (loading) {
return <div className="loading">{t.common.loading}</div>;
}
return (
<div className="app-layout">
<Sidebar />
<main className="main-content settings-root">
<div className="page-tabs-container">
<div className="page-tabs-slider">
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
<span className="material-symbols-outlined">menu</span>
</button>
<div className="page-title-section">
<span className="material-symbols-outlined">settings</span>
<span className="page-title-text">{t.settings.title}</span>
</div>
</div>
</div>
<div className="page-content">
<div className="settings-card">
<div className="settings-section">
<h2>{t.settings.authentication}</h2>
<div className="setting-item">
<div className="setting-info">
<h3>{t.settings.allowRegistration}</h3>
<p>{t.settings.allowRegistrationDesc}</p>
</div>
<label className="toggle-switch">
<input
type="checkbox"
checked={settings.registration_enabled !== false}
onChange={(e) => handleRegistrationToggle(e.target.checked)}
disabled={saving}
/>
<span className="toggle-slider"></span>
</label>
</div>
<div className="setting-item">
<div className="setting-info">
<h3>{t.settings.showLogo}</h3>
<p>{t.settings.showLogoDesc}</p>
</div>
<label className="toggle-switch">
<input
type="checkbox"
checked={settings.show_logo === true}
onChange={(e) => updateSetting('show_logo', e.target.checked)}
disabled={saving}
/>
<span className="toggle-slider"></span>
</label>
</div>
</div>
</div>
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,40 @@
import { useAuth } from '../../contexts/AuthContext';
import { useTranslation } from '../../contexts/LanguageContext';
import { useSidebar } from '../../contexts/SidebarContext';
import '../../styles/AdminPanel.css';
export default function Sources() {
const { user: currentUser } = useAuth();
const { t } = useTranslation();
const { toggleMobileMenu } = useSidebar();
if (!currentUser?.is_superuser) {
return null;
}
return (
<main className="main-content admin-panel-root">
<div className="admin-tabs-container">
<div className="admin-tabs-slider">
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
<span className="material-symbols-outlined">menu</span>
</button>
<div className="admin-title-section">
<span className="material-symbols-outlined">database</span>
<span className="admin-title-text">{t.sourcesPage.title}</span>
</div>
</div>
</div>
<div className="admin-tab-content">
<div className="tab-content-placeholder">
<div className="placeholder-icon">
<span className="material-symbols-outlined">database</span>
</div>
<h3>{t.sourcesPage.title}</h3>
<p>{t.sourcesPage.comingSoon}</p>
</div>
</div>
</main>
);
}

View File

@@ -0,0 +1,808 @@
import { useState } from 'react';
import { useTheme, COLOR_PALETTES } from '../../contexts/ThemeContext';
import type { AccentColor, BorderRadius, SidebarStyle, Density, FontFamily, ColorPalette } from '../../contexts/ThemeContext';
import { useTranslation } from '../../contexts/LanguageContext';
import { useAuth } from '../../contexts/AuthContext';
import { useSidebar } from '../../contexts/SidebarContext';
import type { SidebarMode } from '../../contexts/SidebarContext';
import { ChromePicker, HuePicker } from 'react-color';
import '../../styles/ThemeSettings.css';
type ThemeTab = 'colors' | 'appearance' | 'preview' | 'advanced';
type ColorPickerState = {
isOpen: boolean;
theme: 'light' | 'dark';
property: string;
value: string;
} | null;
export default function ThemeSettings() {
const [activeTab, setActiveTab] = useState<ThemeTab>('colors');
const {
accentColor,
borderRadius,
sidebarStyle,
density,
fontFamily,
colorPalette,
setAccentColor,
setBorderRadius,
setSidebarStyle,
setDensity,
setFontFamily,
setColorPalette,
saveThemeToBackend
} = useTheme();
const { t } = useTranslation();
const { user } = useAuth();
const { toggleMobileMenu, sidebarMode, setSidebarMode } = useSidebar();
const isAdmin = user?.is_superuser || false;
const [tooltip, setTooltip] = useState<{ text: string; left: number; visible: boolean }>({
text: '',
left: 0,
visible: false,
});
const handleTabMouseEnter = (text: string, e: React.MouseEvent) => {
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
setTooltip({
text,
left: rect.left + rect.width / 2,
visible: true,
});
};
const handleTabMouseLeave = () => {
setTooltip((prev) => ({ ...prev, visible: false }));
};
// Handlers that save to backend after setting
const handleAccentColorChange = async (color: AccentColor) => {
setAccentColor(color);
saveThemeToBackend({ accentColor: color }).catch(console.error);
};
const handleBorderRadiusChange = async (radius: BorderRadius) => {
setBorderRadius(radius);
saveThemeToBackend({ borderRadius: radius }).catch(console.error);
};
const handleSidebarStyleChange = async (style: SidebarStyle) => {
setSidebarStyle(style);
saveThemeToBackend({ sidebarStyle: style }).catch(console.error);
};
const handleDensityChange = async (d: Density) => {
setDensity(d);
saveThemeToBackend({ density: d }).catch(console.error);
};
const handleFontFamilyChange = async (font: FontFamily) => {
setFontFamily(font);
saveThemeToBackend({ fontFamily: font }).catch(console.error);
};
const handleColorPaletteChange = async (palette: ColorPalette) => {
setColorPalette(palette);
saveThemeToBackend({ colorPalette: palette }).catch(console.error);
};
// Advanced color states
const [customColors, setCustomColors] = useState({
light: {
bgMain: '#ffffff',
bgCard: '#f9fafb',
bgElevated: '#ffffff',
textPrimary: '#111827',
textSecondary: '#6b7280',
border: '#e5e7eb',
sidebarBg: '#1f2937',
sidebarText: '#f9fafb'
},
dark: {
bgMain: '#0f172a',
bgCard: '#1e293b',
bgElevated: '#334155',
textPrimary: '#f1f5f9',
textSecondary: '#94a3b8',
border: '#334155',
sidebarBg: '#0c1222',
sidebarText: '#f9fafb'
}
});
// Color picker popup state
const [colorPickerState, setColorPickerState] = useState<ColorPickerState>(null);
const colors: { id: AccentColor; label: string; value: string; description: string }[] = [
{ id: 'auto', label: t.theme.colors.auto, value: '#374151', description: t.theme.colors.autoDesc },
{ id: 'blue', label: t.theme.colors.blue, value: '#3b82f6', description: t.theme.colors.blueDesc },
{ id: 'purple', label: t.theme.colors.purple, value: '#8b5cf6', description: t.theme.colors.purpleDesc },
{ id: 'green', label: t.theme.colors.green, value: '#10b981', description: t.theme.colors.greenDesc },
{ id: 'orange', label: t.theme.colors.orange, value: '#f97316', description: t.theme.colors.orangeDesc },
{ id: 'pink', label: t.theme.colors.pink, value: '#ec4899', description: t.theme.colors.pinkDesc },
{ id: 'red', label: t.theme.colors.red, value: '#ef4444', description: t.theme.colors.redDesc },
{ id: 'teal', label: t.theme.colors.teal, value: '#14b8a6', description: t.theme.colors.tealDesc },
{ id: 'amber', label: t.theme.colors.amber, value: '#f59e0b', description: t.theme.colors.amberDesc },
{ id: 'indigo', label: t.theme.colors.indigo, value: '#6366f1', description: t.theme.colors.indigoDesc },
{ id: 'cyan', label: t.theme.colors.cyan, value: '#06b6d4', description: t.theme.colors.cyanDesc },
{ id: 'rose', label: t.theme.colors.rose, value: '#f43f5e', description: t.theme.colors.roseDesc },
];
const radii: { id: BorderRadius; label: string; description: string; preview: string }[] = [
{ id: 'large', label: t.theme.radius.large, description: t.theme.radius.largeDesc, preview: '16px' },
{ id: 'medium', label: t.theme.radius.medium, description: t.theme.radius.mediumDesc, preview: '8px' },
{ id: 'small', label: t.theme.radius.small, description: t.theme.radius.smallDesc, preview: '4px' },
];
const sidebarStyles: { id: SidebarStyle; label: string; description: string }[] = [
{ id: 'default', label: t.theme.sidebarOptions.default, description: t.theme.sidebarOptions.defaultDesc },
{ id: 'dark', label: t.theme.sidebarOptions.dark, description: t.theme.sidebarOptions.darkDesc },
{ id: 'light', label: t.theme.sidebarOptions.light, description: t.theme.sidebarOptions.lightDesc },
];
const sidebarModes: { id: SidebarMode; label: string; description: string }[] = [
{ id: 'toggle', label: t.admin.sidebarModeToggle, description: t.admin.sidebarModeToggleDesc },
{ id: 'dynamic', label: t.admin.sidebarModeDynamic, description: t.admin.sidebarModeDynamicDesc },
{ id: 'collapsed', label: t.admin.sidebarModeCollapsed, description: t.admin.sidebarModeCollapsedDesc },
];
const densities: { id: Density; label: string; description: string }[] = [
{ id: 'compact', label: t.theme.densityOptions.compact, description: t.theme.densityOptions.compactDesc },
{ id: 'comfortable', label: t.theme.densityOptions.comfortable, description: t.theme.densityOptions.comfortableDesc },
{ id: 'spacious', label: t.theme.densityOptions.spacious, description: t.theme.densityOptions.spaciousDesc },
];
const fonts: { id: FontFamily; label: string; description: string; fontStyle: string }[] = [
{ id: 'sans', label: t.theme.fontOptions.system, description: t.theme.fontOptions.systemDesc, fontStyle: 'system-ui' },
{ id: 'inter', label: t.theme.fontOptions.inter, description: t.theme.fontOptions.interDesc, fontStyle: 'Inter' },
{ id: 'roboto', label: t.theme.fontOptions.roboto, description: t.theme.fontOptions.robotoDesc, fontStyle: 'Roboto' },
];
const palettes: { id: ColorPalette; label: string; description: string }[] = [
{ id: 'monochrome', label: t.theme.palettes.monochrome, description: t.theme.palettes.monochromeDesc },
{ id: 'default', label: t.theme.palettes.default, description: t.theme.palettes.defaultDesc },
{ id: 'monochromeBlue', label: t.theme.palettes.monochromeBlue, description: t.theme.palettes.monochromeBlueDesc },
{ id: 'sepia', label: t.theme.palettes.sepia, description: t.theme.palettes.sepiaDesc },
{ id: 'nord', label: t.theme.palettes.nord, description: t.theme.palettes.nordDesc },
{ id: 'dracula', label: t.theme.palettes.dracula, description: t.theme.palettes.draculaDesc },
{ id: 'solarized', label: t.theme.palettes.solarized, description: t.theme.palettes.solarizedDesc },
{ id: 'github', label: t.theme.palettes.github, description: t.theme.palettes.githubDesc },
{ id: 'ocean', label: t.theme.palettes.ocean, description: t.theme.palettes.oceanDesc },
{ id: 'forest', label: t.theme.palettes.forest, description: t.theme.palettes.forestDesc },
{ id: 'midnight', label: t.theme.palettes.midnight, description: t.theme.palettes.midnightDesc },
{ id: 'sunset', label: t.theme.palettes.sunset, description: t.theme.palettes.sunsetDesc },
];
// Helper component for color control
const ColorControl = ({ theme, property, label, value }: {
theme: 'light' | 'dark';
property: string;
label: string;
value: string
}) => {
return (
<div className="color-control-item">
<label className="color-control-label">
<span>{label}</span>
<span className="color-value-display selectable">{value}</span>
</label>
<div className="color-control-actions">
<div className="color-input-group">
<button
className="btn-color-picker"
style={{ backgroundColor: value }}
title={t.theme.pickColor}
onClick={() => {
setColorPickerState({
isOpen: true,
theme,
property,
value
});
}}
>
{/* Color preview via background color */}
</button>
<input
type="text"
value={value.toUpperCase()}
onChange={(e) => handleHexInput(theme, property, e.target.value)}
className="color-hex-input"
placeholder="#FFFFFF"
maxLength={7}
/>
</div>
</div>
</div>
);
};
// Color picker handlers
const handleColorChange = (theme: 'light' | 'dark', property: string, value: string) => {
setCustomColors(prev => ({
...prev,
[theme]: {
...prev[theme],
[property]: value
}
}));
// Apply to CSS variables immediately
const root = document.documentElement;
const varName = `--color-${property.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
root.style.setProperty(varName, value);
};
const handleHexInput = (theme: 'light' | 'dark', property: string, value: string) => {
// Validate hex color format
const hexRegex = /^#?[0-9A-Fa-f]{6}$/;
const formattedValue = value.startsWith('#') ? value : `#${value}`;
if (hexRegex.test(formattedValue)) {
handleColorChange(theme, property, formattedValue);
}
};
const resetToDefaults = () => {
setCustomColors({
light: {
bgMain: '#ffffff',
bgCard: '#f9fafb',
bgElevated: '#ffffff',
textPrimary: '#111827',
textSecondary: '#6b7280',
border: '#e5e7eb',
sidebarBg: '#1f2937',
sidebarText: '#f9fafb'
},
dark: {
bgMain: '#0f172a',
bgCard: '#1e293b',
bgElevated: '#334155',
textPrimary: '#f1f5f9',
textSecondary: '#94a3b8',
border: '#334155',
sidebarBg: '#0c1222',
sidebarText: '#f9fafb'
}
});
// Reset CSS variables
const root = document.documentElement;
root.style.removeProperty('--color-bg-main');
root.style.removeProperty('--color-bg-card');
root.style.removeProperty('--color-bg-elevated');
root.style.removeProperty('--color-text-primary');
root.style.removeProperty('--color-text-secondary');
root.style.removeProperty('--color-border');
root.style.removeProperty('--color-sidebar-bg');
root.style.removeProperty('--color-sidebar-text');
};
return (
<main className="main-content theme-settings-root">
{/* Tab Tooltip */}
{tooltip.visible && (
<div
className="admin-tab-tooltip"
style={{ left: tooltip.left }}
>
{tooltip.text}
</div>
)}
{/* Modern Tab Navigation */}
<div className="page-tabs-container">
<div className="page-tabs-slider">
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
<span className="material-symbols-outlined">menu</span>
</button>
<div className="page-title-section">
<span className="material-symbols-outlined">brush</span>
<span className="page-title-text">{t.theme.title}</span>
</div>
<div className="admin-tabs-divider"></div>
<button
className={`admin-tab-btn ${activeTab === 'colors' ? 'active' : ''}`}
onClick={() => setActiveTab('colors')}
onMouseEnter={(e) => handleTabMouseEnter(t.theme.colorsTab, e)}
onMouseLeave={handleTabMouseLeave}
>
<span className="material-symbols-outlined">color_lens</span>
<span>{t.theme.colorsTab}</span>
</button>
<button
className={`admin-tab-btn ${activeTab === 'appearance' ? 'active' : ''}`}
onClick={() => setActiveTab('appearance')}
onMouseEnter={(e) => handleTabMouseEnter(t.theme.appearanceTab, e)}
onMouseLeave={handleTabMouseLeave}
>
<span className="material-symbols-outlined">tune</span>
<span>{t.theme.appearanceTab}</span>
</button>
<button
className={`admin-tab-btn ${activeTab === 'preview' ? 'active' : ''}`}
onClick={() => setActiveTab('preview')}
onMouseEnter={(e) => handleTabMouseEnter(t.theme.previewTab, e)}
onMouseLeave={handleTabMouseLeave}
>
<span className="material-symbols-outlined">visibility</span>
<span>{t.theme.previewTab}</span>
</button>
{isAdmin && (
<button
className={`admin-tab-btn ${activeTab === 'advanced' ? 'active' : ''}`}
onClick={() => setActiveTab('advanced')}
onMouseEnter={(e) => handleTabMouseEnter(t.theme.advancedTab, e)}
onMouseLeave={handleTabMouseLeave}
>
<span className="material-symbols-outlined">code</span>
<span>{t.theme.advancedTab}</span>
</button>
)}
</div>
</div>
<div className="admin-tab-content">
{activeTab === 'colors' && (
<div className="theme-tab-content">
<div className="theme-section">
<div className="section-header">
<h3 className="section-title">{t.theme.accentColor}</h3>
</div>
<div className="color-grid-enhanced">
{colors.map((color) => (
<div
key={color.id}
className={`color-card ${accentColor === color.id ? 'active' : ''}`}
onClick={() => handleAccentColorChange(color.id)}
>
<div className="color-swatch-large" style={{ backgroundColor: color.value }}>
{accentColor === color.id && (
<span className="material-symbols-outlined">check</span>
)}
</div>
<div className="color-info">
<span className="color-name">{color.label}</span>
<span className="color-description">{color.description}</span>
</div>
</div>
))}
</div>
</div>
<div className="theme-section">
<div className="section-header">
<h3 className="section-title">{t.theme.colorPalette}</h3>
</div>
<div className="palette-grid">
{palettes.map((palette) => {
const paletteColors = COLOR_PALETTES[palette.id];
return (
<div
key={palette.id}
className={`palette-card ${colorPalette === palette.id ? 'active' : ''}`}
onClick={() => handleColorPaletteChange(palette.id)}
>
<div className="palette-preview">
<div className="palette-swatch-row">
<div className="palette-swatch" style={{ backgroundColor: paletteColors.light.bgMain }} title="Light BG" />
<div className="palette-swatch" style={{ backgroundColor: paletteColors.light.bgCard }} title="Light Card" />
<div className="palette-swatch" style={{ backgroundColor: paletteColors.light.textPrimary }} title="Light Text" />
<div className="palette-swatch" style={{ backgroundColor: paletteColors.light.sidebarBg }} title="Light Sidebar" />
</div>
<div className="palette-swatch-row">
<div className="palette-swatch" style={{ backgroundColor: paletteColors.dark.bgMain }} title="Dark BG" />
<div className="palette-swatch" style={{ backgroundColor: paletteColors.dark.bgCard }} title="Dark Card" />
<div className="palette-swatch" style={{ backgroundColor: paletteColors.dark.textPrimary }} title="Dark Text" />
<div className="palette-swatch" style={{ backgroundColor: paletteColors.dark.sidebarBg }} title="Dark Sidebar" />
</div>
{colorPalette === palette.id && (
<div className="palette-check">
<span className="material-symbols-outlined">check</span>
</div>
)}
</div>
<div className="palette-info">
<span className="palette-name">{palette.label}</span>
<span className="palette-description">{palette.description}</span>
</div>
</div>
);
})}
</div>
</div>
</div>
)}
{activeTab === 'appearance' && (
<div className="theme-tab-content">
<div className="appearance-grid">
{/* Border Radius Section */}
<div className="theme-section">
<div className="section-header">
<h3 className="section-title">{t.theme.borderRadius}</h3>
</div>
<div className="option-cards">
{radii.map((radius) => (
<div
key={radius.id}
className={`option-card ${borderRadius === radius.id ? 'active' : ''}`}
onClick={() => handleBorderRadiusChange(radius.id)}
>
<div className="option-preview">
<div
className="radius-preview-box"
style={{ borderRadius: radius.preview }}
></div>
</div>
<div className="option-info">
<span className="option-name">{radius.label}</span>
<span className="option-description">{radius.description}</span>
</div>
</div>
))}
</div>
</div>
{/* Sidebar Style Section */}
<div className="theme-section">
<div className="section-header">
<h3 className="section-title">{t.theme.sidebarStyle}</h3>
</div>
<div className="option-cards">
{sidebarStyles.map((style) => (
<div
key={style.id}
className={`option-card ${sidebarStyle === style.id ? 'active' : ''}`}
onClick={() => handleSidebarStyleChange(style.id)}
>
<div className="option-preview">
<div className={`sidebar-preview sidebar-preview-${style.id}`}>
<div className="sidebar-part"></div>
<div className="content-part"></div>
</div>
</div>
<div className="option-info">
<span className="option-name">{style.label}</span>
<span className="option-description">{style.description}</span>
</div>
</div>
))}
</div>
</div>
{/* Sidebar Mode Section */}
<div className="theme-section">
<div className="section-header">
<h3 className="section-title">{t.admin.sidebarMode}</h3>
</div>
<div className="option-cards">
{sidebarModes.map((mode) => (
<div
key={mode.id}
className={`option-card ${sidebarMode === mode.id ? 'active' : ''}`}
onClick={() => setSidebarMode(mode.id)}
>
<div className="option-preview">
<div className={`sidebar-mode-preview sidebar-mode-${mode.id}`}>
<div className="sidebar-line"></div>
<div className="sidebar-line"></div>
</div>
</div>
<div className="option-info">
<span className="option-name">{mode.label}</span>
<span className="option-description">{mode.description}</span>
</div>
</div>
))}
</div>
</div>
{/* Density Section */}
<div className="theme-section">
<div className="section-header">
<h3 className="section-title">{t.theme.density}</h3>
</div>
<div className="option-cards">
{densities.map((d) => (
<div
key={d.id}
className={`option-card ${density === d.id ? 'active' : ''}`}
onClick={() => handleDensityChange(d.id)}
>
<div className="option-preview">
<div className={`density-preview density-preview-${d.id}`}>
<div className="density-line"></div>
<div className="density-line"></div>
<div className="density-line"></div>
</div>
</div>
<div className="option-info">
<span className="option-name">{d.label}</span>
<span className="option-description">{d.description}</span>
</div>
</div>
))}
</div>
</div>
{/* Font Family Section */}
<div className="theme-section">
<div className="section-header">
<h3 className="section-title">{t.theme.fontFamily}</h3>
</div>
<div className="option-cards">
{fonts.map((f) => (
<div
key={f.id}
className={`option-card ${fontFamily === f.id ? 'active' : ''}`}
onClick={() => handleFontFamilyChange(f.id)}
>
<div className="option-preview">
<div className="font-preview" style={{ fontFamily: f.fontStyle }}>
Aa
</div>
</div>
<div className="option-info">
<span className="option-name">{f.label}</span>
<span className="option-description">{f.description}</span>
</div>
</div>
))}
</div>
</div>
</div>
</div>
)}
{activeTab === 'preview' && (
<div className="theme-tab-content">
<div className="theme-section">
<div className="section-header">
<h3 className="section-title">{t.theme.preview}</h3>
</div>
<div className="preview-container">
<div className="preview-card">
<div className="preview-header">
<h3>{t.theme.previewCard}</h3>
<span className="badge badge-accent">{t.theme.badge}</span>
</div>
<p>{t.theme.previewDescription}</p>
<div className="preview-actions">
<button className="btn-primary">{t.theme.primaryButton}</button>
<button className="btn-ghost">{t.theme.ghostButton}</button>
</div>
<div className="preview-inputs">
<input type="text" placeholder={t.theme.inputPlaceholder} />
</div>
</div>
<div className="preview-card">
<div className="preview-header">
<h3>{t.theme.sampleCard}</h3>
<span className="badge badge-success">{t.dashboard.active}</span>
</div>
<p>{t.theme.sampleCardDesc}</p>
<div className="preview-stats">
<div className="stat-item">
<span className="stat-value">142</span>
<span className="stat-label">{t.theme.totalItems}</span>
</div>
<div className="stat-item">
<span className="stat-value">89%</span>
<span className="stat-label">{t.theme.successRate}</span>
</div>
</div>
</div>
</div>
</div>
</div>
)}
{activeTab === 'advanced' && isAdmin && (
<div className="theme-tab-content">
<div className="theme-section">
<div className="section-header">
<h3 className="section-title">{t.theme.advancedColors}</h3>
<button className="btn-ghost" onClick={resetToDefaults} style={{ marginTop: '1rem' }}>
<span className="material-symbols-outlined">refresh</span>
{t.theme.resetColors}
</button>
</div>
<div className="advanced-colors-grid" style={{ marginTop: '2rem' }}>
{/* Light Theme Colors */}
<div className="color-theme-section">
<h3 className="color-theme-title">{t.theme.lightThemeColors}</h3>
<div className="color-controls-list">
<ColorControl
theme="light"
property="bgMain"
label={t.theme.background}
value={customColors.light.bgMain}
/>
<ColorControl
theme="light"
property="bgCard"
label={t.theme.backgroundCard}
value={customColors.light.bgCard}
/>
<ColorControl
theme="light"
property="bgElevated"
label={t.theme.backgroundElevated}
value={customColors.light.bgElevated}
/>
<ColorControl
theme="light"
property="textPrimary"
label={t.theme.textPrimary}
value={customColors.light.textPrimary}
/>
<ColorControl
theme="light"
property="textSecondary"
label={t.theme.textSecondary}
value={customColors.light.textSecondary}
/>
<ColorControl
theme="light"
property="border"
label={t.theme.border}
value={customColors.light.border}
/>
<ColorControl
theme="light"
property="sidebarBg"
label={t.theme.sidebarBackground}
value={customColors.light.sidebarBg}
/>
<ColorControl
theme="light"
property="sidebarText"
label={t.theme.sidebarText}
value={customColors.light.sidebarText}
/>
</div>
</div>
{/* Dark Theme Colors */}
<div className="color-theme-section">
<h3 className="color-theme-title">{t.theme.darkThemeColors}</h3>
<div className="color-controls-list">
<ColorControl
theme="dark"
property="bgMain"
label={t.theme.background}
value={customColors.dark.bgMain}
/>
<ColorControl
theme="dark"
property="bgCard"
label={t.theme.backgroundCard}
value={customColors.dark.bgCard}
/>
<ColorControl
theme="dark"
property="bgElevated"
label={t.theme.backgroundElevated}
value={customColors.dark.bgElevated}
/>
<ColorControl
theme="dark"
property="textPrimary"
label={t.theme.textPrimary}
value={customColors.dark.textPrimary}
/>
<ColorControl
theme="dark"
property="textSecondary"
label={t.theme.textSecondary}
value={customColors.dark.textSecondary}
/>
<ColorControl
theme="dark"
property="border"
label={t.theme.border}
value={customColors.dark.border}
/>
<ColorControl
theme="dark"
property="sidebarBg"
label={t.theme.sidebarBackground}
value={customColors.dark.sidebarBg}
/>
<ColorControl
theme="dark"
property="sidebarText"
label={t.theme.sidebarText}
value={customColors.dark.sidebarText}
/>
</div>
</div>
</div>
</div>
</div>
)}
</div>
{/* Color Picker Popup */}
{colorPickerState && (
<div
className="color-picker-overlay"
onClick={() => setColorPickerState(null)}
>
<div
className="color-picker-popup"
onClick={(e) => e.stopPropagation()}
>
<div className="color-picker-header">
<h3>{t.theme.pickColor}</h3>
<button
className="btn-close-picker"
onClick={() => setColorPickerState(null)}
>
<span className="material-symbols-outlined">close</span>
</button>
</div>
<div className="color-picker-content">
<div className="color-picker-preview-section">
<div
className="color-preview-box"
style={{ backgroundColor: colorPickerState.value }}
/>
<div className="color-preview-info">
<span className="color-preview-hex">{colorPickerState.value.toUpperCase()}</span>
<span className="color-preview-label">{t.theme.currentColor}</span>
</div>
</div>
<div className="chrome-picker-wrapper">
<ChromePicker
color={colorPickerState.value}
onChange={(color) => {
handleColorChange(
colorPickerState.theme,
colorPickerState.property,
color.hex
);
setColorPickerState({
...colorPickerState,
value: color.hex
});
}}
disableAlpha={true}
/>
<div className="hue-picker-wrapper">
<HuePicker
color={colorPickerState.value}
onChange={(color) => {
handleColorChange(
colorPickerState.theme,
colorPickerState.property,
color.hex
);
setColorPickerState({
...colorPickerState,
value: color.hex
});
}}
width="100%"
height="16px"
/>
</div>
</div>
<div className="color-picker-actions">
<button
className="btn-primary btn-full-width"
onClick={() => setColorPickerState(null)}
>
{t.theme.apply}
</button>
</div>
</div>
</div>
</div>
)}
</main>
);
}