Redesign AdminSettings with sidebar navigation and localization

- Replace top tabs with sidebar menu navigation
- Add sub-categories for settings (payments, subscriptions, etc.)
- Implement full Russian localization for all UI elements
- Add category translations for all setting groups
- Collapsible presets and custom colors sections
- Favorites functionality with localStorage persistence
- Search functionality in settings
- Responsive layout with collapsible sidebar
This commit is contained in:
PEDZEO
2026-01-21 03:02:23 +03:00
parent d7b387927f
commit 329d063387
2 changed files with 373 additions and 226 deletions

View File

@@ -674,7 +674,108 @@
"searchPlaceholder": "Поиск настроек...", "searchPlaceholder": "Поиск настроек...",
"searchCategoriesPlaceholder": "Поиск категорий...", "searchCategoriesPlaceholder": "Поиск категорий...",
"noSearchResults": "Ничего не найдено", "noSearchResults": "Ничего не найдено",
"example": "Пример" "example": "Пример",
"favorites": "Избранное",
"favoritesEmpty": "Нет избранных настроек",
"favoritesHint": "Нажмите ⭐ рядом с настройкой, чтобы добавить её сюда",
"branding": "Брендинг",
"theme": "Тема",
"payments": "Платежи",
"subscriptions": "Подписки",
"interface": "Интерфейс",
"notifications": "Уведомления",
"database": "База данных",
"system": "Система",
"users": "Пользователи",
"logoAndName": "Логотип и название",
"projectName": "Название проекта",
"interfaceOptions": "Опции интерфейса",
"animatedBackground": "Анимированный фон",
"animatedBackgroundDesc": "Волны на фоне приложения",
"autoFullscreen": "Авто-Fullscreen",
"autoFullscreenDesc": "В Telegram WebApp",
"availableThemes": "Доступные темы",
"darkTheme": "Тёмная",
"lightTheme": "Светлая",
"quickPresets": "Быстрые пресеты",
"customColors": "Ручная настройка",
"accentColor": "Акцентный цвет",
"statusColors": "Статусные цвета",
"resetAllColors": "Сбросить все цвета",
"notSpecified": "Не указано",
"presets": {
"standard": "Стандарт",
"ocean": "Океан",
"forest": "Лес",
"sunset": "Закат",
"violet": "Фиолет",
"rose": "Роза",
"midnight": "Полночь",
"turquoise": "Бирюза"
},
"colors": {
"background": "Фон",
"surface": "Поверхность",
"text": "Текст",
"textSecondary": "Вторичный текст",
"success": "Успех",
"warning": "Предупреждение",
"error": "Ошибка"
},
"categories": {
"PAYMENT": "Платежи",
"PAYMENT_VERIFICATION": "Верификация платежей",
"YOOKASSA": "YooKassa",
"CRYPTOBOT": "CryptoBot",
"HELEKET": "Heleket",
"PLATEGA": "Platega",
"TRIBUTE": "Tribute",
"MULENPAY": "MulenPay",
"PAL24": "PAL24",
"WATA": "Wata",
"TELEGRAM": "Telegram",
"SUBSCRIPTIONS_CORE": "Основные",
"SIMPLE_SUBSCRIPTION": "Простая подписка",
"PERIODS": "Периоды",
"SUBSCRIPTION_PRICES": "Цены",
"TRAFFIC": "Трафик",
"TRAFFIC_PACKAGES": "Пакеты трафика",
"TRIAL": "Пробный период",
"AUTOPAY": "Автоплатёж",
"INTERFACE": "Интерфейс",
"INTERFACE_BRANDING": "Брендинг",
"INTERFACE_SUBSCRIPTION": "Подписка",
"CONNECT_BUTTON": "Кнопка подключения",
"MINIAPP": "MiniApp",
"HAPP": "HAPP",
"SKIP": "Пропуск",
"ADDITIONAL": "Дополнительно",
"NOTIFICATIONS": "Уведомления",
"ADMIN_NOTIFICATIONS": "Уведомления админов",
"ADMIN_REPORTS": "Отчёты админов",
"DATABASE": "База данных",
"POSTGRES": "PostgreSQL",
"SQLITE": "SQLite",
"REDIS": "Redis",
"CORE": "Ядро",
"REMNAWAVE": "RemnaWave",
"SERVER_STATUS": "Статус сервера",
"MONITORING": "Мониторинг",
"MAINTENANCE": "Обслуживание",
"BACKUP": "Бэкап",
"VERSION": "Версия",
"WEB_API": "Web API",
"WEBHOOK": "Webhook",
"LOG": "Логи",
"DEBUG": "Отладка",
"EXTERNAL_ADMIN": "Внешний админ",
"SUPPORT": "Поддержка",
"LOCALIZATION": "Локализация",
"CHANNEL": "Канал",
"TIMEZONE": "Часовой пояс",
"REFERRAL": "Реферальная система",
"MODERATION": "Модерация"
}
}, },
"apps": { "apps": {
"title": "Управление приложениями", "title": "Управление приложениями",

View File

@@ -38,6 +38,12 @@ const ChevronDownIcon = () => (
</svg> </svg>
) )
const ChevronRightIcon = () => (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
)
const UploadIcon = () => ( const UploadIcon = () => (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" /> <path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
@@ -92,83 +98,52 @@ const MoonIcon = () => (
</svg> </svg>
) )
// ============ TAB DEFINITIONS ============ // ============ SIDEBAR MENU ITEMS ============
const TABS = [ interface MenuItem {
{ id: 'favorites', label: 'Избранное', icon: StarIcon }, id: string
{ id: 'branding', label: 'Брендинг', icon: null }, icon: ((props: { filled?: boolean }) => JSX.Element) | null
{ id: 'theme', label: 'Тема', icon: null }, categories?: string[]
{ id: 'payments', label: 'Платежи', icon: null },
{ id: 'subscriptions', label: 'Подписки', icon: null },
{ id: 'interface', label: 'Интерфейс', icon: null },
{ id: 'more', label: 'Ещё', icon: ChevronDownIcon, isDropdown: true },
] as const
const MORE_TABS = [
{ id: 'notifications', label: 'Уведомления' },
{ id: 'database', label: 'База данных' },
{ id: 'system', label: 'Система' },
{ id: 'users', label: 'Пользователи' },
] as const
// ============ META CATEGORIES ============
const META_CATEGORIES: Record<string, { label: string; categories: string[] }> = {
payments: {
label: 'Платежные системы',
categories: ['PAYMENT', 'PAYMENT_VERIFICATION', 'YOOKASSA', 'CRYPTOBOT', 'HELEKET', 'PLATEGA', 'TRIBUTE', 'MULENPAY', 'PAL24', 'WATA', 'TELEGRAM'],
},
subscriptions: {
label: 'Подписки и тарифы',
categories: ['SUBSCRIPTIONS_CORE', 'SIMPLE_SUBSCRIPTION', 'PERIODS', 'SUBSCRIPTION_PRICES', 'TRAFFIC', 'TRAFFIC_PACKAGES', 'TRIAL', 'AUTOPAY'],
},
interface: {
label: 'Интерфейс',
categories: ['INTERFACE', 'INTERFACE_BRANDING', 'INTERFACE_SUBSCRIPTION', 'CONNECT_BUTTON', 'MINIAPP', 'HAPP', 'SKIP', 'ADDITIONAL'],
},
notifications: {
label: 'Уведомления',
categories: ['NOTIFICATIONS', 'ADMIN_NOTIFICATIONS', 'ADMIN_REPORTS'],
},
database: {
label: 'База данных',
categories: ['DATABASE', 'POSTGRES', 'SQLITE', 'REDIS'],
},
system: {
label: 'Система',
categories: ['CORE', 'REMNAWAVE', 'SERVER_STATUS', 'MONITORING', 'MAINTENANCE', 'BACKUP', 'VERSION', 'WEB_API', 'WEBHOOK', 'LOG', 'DEBUG', 'EXTERNAL_ADMIN'],
},
users: {
label: 'Пользователи',
categories: ['SUPPORT', 'LOCALIZATION', 'CHANNEL', 'TIMEZONE', 'REFERRAL', 'MODERATION'],
},
} }
// ============ THEME PRESETS ============ interface MenuSection {
const THEME_PRESETS: { name: string; colors: ThemeColors }[] = [ id: string
{ name: 'Стандарт', colors: DEFAULT_THEME_COLORS }, items: MenuItem[]
{ name: 'Океан', colors: { accent: '#0ea5e9', darkBackground: '#0c1222', darkSurface: '#1e293b', darkText: '#f1f5f9', darkTextSecondary: '#94a3b8', lightBackground: '#e0f2fe', lightSurface: '#f0f9ff', lightText: '#0c4a6e', lightTextSecondary: '#0369a1', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' } }, }
{ name: 'Лес', colors: { accent: '#22c55e', darkBackground: '#0a1a0f', darkSurface: '#14532d', darkText: '#f0fdf4', darkTextSecondary: '#86efac', lightBackground: '#dcfce7', lightSurface: '#f0fdf4', lightText: '#14532d', lightTextSecondary: '#166534', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' } },
{ name: 'Закат', colors: { accent: '#f97316', darkBackground: '#1c1009', darkSurface: '#2d1a0e', darkText: '#fff7ed', darkTextSecondary: '#fdba74', lightBackground: '#ffedd5', lightSurface: '#fff7ed', lightText: '#7c2d12', lightTextSecondary: '#c2410c', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' } }, const MENU_SECTIONS: MenuSection[] = [
{ name: 'Фиолет', colors: { accent: '#a855f7', darkBackground: '#0f0a1a', darkSurface: '#1e1b2e', darkText: '#faf5ff', darkTextSecondary: '#c4b5fd', lightBackground: '#f3e8ff', lightSurface: '#faf5ff', lightText: '#581c87', lightTextSecondary: '#7e22ce', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' } }, {
{ name: 'Роза', colors: { accent: '#f43f5e', darkBackground: '#1a0a10', darkSurface: '#2d1520', darkText: '#fff1f2', darkTextSecondary: '#fda4af', lightBackground: '#ffe4e6', lightSurface: '#fff1f2', lightText: '#881337', lightTextSecondary: '#be123c', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' } }, id: 'main',
{ name: 'Полночь', colors: { accent: '#6366f1', darkBackground: '#030712', darkSurface: '#111827', darkText: '#f9fafb', darkTextSecondary: '#9ca3af', lightBackground: '#e5e7eb', lightSurface: '#f3f4f6', lightText: '#111827', lightTextSecondary: '#4b5563', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' } }, items: [
{ name: 'Бирюза', colors: { accent: '#14b8a6', darkBackground: '#0a1614', darkSurface: '#134e4a', darkText: '#f0fdfa', darkTextSecondary: '#5eead4', lightBackground: '#ccfbf1', lightSurface: '#f0fdfa', lightText: '#134e4a', lightTextSecondary: '#0f766e', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' } }, { id: 'favorites', icon: StarIcon },
{ id: 'branding', icon: null },
{ id: 'theme', icon: null },
]
},
{
id: 'settings',
items: [
{ id: 'payments', icon: null, categories: ['PAYMENT', 'PAYMENT_VERIFICATION', 'YOOKASSA', 'CRYPTOBOT', 'HELEKET', 'PLATEGA', 'TRIBUTE', 'MULENPAY', 'PAL24', 'WATA', 'TELEGRAM'] },
{ id: 'subscriptions', icon: null, categories: ['SUBSCRIPTIONS_CORE', 'SIMPLE_SUBSCRIPTION', 'PERIODS', 'SUBSCRIPTION_PRICES', 'TRAFFIC', 'TRAFFIC_PACKAGES', 'TRIAL', 'AUTOPAY'] },
{ id: 'interface', icon: null, categories: ['INTERFACE', 'INTERFACE_BRANDING', 'INTERFACE_SUBSCRIPTION', 'CONNECT_BUTTON', 'MINIAPP', 'HAPP', 'SKIP', 'ADDITIONAL'] },
{ id: 'notifications', icon: null, categories: ['NOTIFICATIONS', 'ADMIN_NOTIFICATIONS', 'ADMIN_REPORTS'] },
{ id: 'database', icon: null, categories: ['DATABASE', 'POSTGRES', 'SQLITE', 'REDIS'] },
{ id: 'system', icon: null, categories: ['CORE', 'REMNAWAVE', 'SERVER_STATUS', 'MONITORING', 'MAINTENANCE', 'BACKUP', 'VERSION', 'WEB_API', 'WEBHOOK', 'LOG', 'DEBUG', 'EXTERNAL_ADMIN'] },
{ id: 'users', icon: null, categories: ['SUPPORT', 'LOCALIZATION', 'CHANNEL', 'TIMEZONE', 'REFERRAL', 'MODERATION'] },
]
}
] ]
// ============ SETTING TRANSLATIONS ============ // ============ THEME PRESETS ============
const SETTING_TRANSLATIONS: Record<string, { name: string; description: string }> = { const THEME_PRESETS: { id: string; colors: ThemeColors }[] = [
BOT_USERNAME: { name: 'Username бота', description: 'Имя бота в Telegram' }, { id: 'standard', colors: DEFAULT_THEME_COLORS },
SUPPORT_USERNAME: { name: 'Username поддержки', description: 'Telegram @username поддержки' }, { id: 'ocean', colors: { accent: '#0ea5e9', darkBackground: '#0c1222', darkSurface: '#1e293b', darkText: '#f1f5f9', darkTextSecondary: '#94a3b8', lightBackground: '#e0f2fe', lightSurface: '#f0f9ff', lightText: '#0c4a6e', lightTextSecondary: '#0369a1', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' } },
SUPPORT_MENU_ENABLED: { name: 'Меню поддержки', description: 'Показывать раздел поддержки' }, { id: 'forest', colors: { accent: '#22c55e', darkBackground: '#0a1a0f', darkSurface: '#14532d', darkText: '#f0fdf4', darkTextSecondary: '#86efac', lightBackground: '#dcfce7', lightSurface: '#f0fdf4', lightText: '#14532d', lightTextSecondary: '#166534', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' } },
DEFAULT_LANGUAGE: { name: 'Язык по умолчанию', description: 'Язык интерфейса' }, { id: 'sunset', colors: { accent: '#f97316', darkBackground: '#1c1009', darkSurface: '#2d1a0e', darkText: '#fff7ed', darkTextSecondary: '#fdba74', lightBackground: '#ffedd5', lightSurface: '#fff7ed', lightText: '#7c2d12', lightTextSecondary: '#c2410c', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' } },
TIMEZONE: { name: 'Часовой пояс', description: 'Временная зона системы' }, { id: 'violet', colors: { accent: '#a855f7', darkBackground: '#0f0a1a', darkSurface: '#1e1b2e', darkText: '#faf5ff', darkTextSecondary: '#c4b5fd', lightBackground: '#f3e8ff', lightSurface: '#faf5ff', lightText: '#581c87', lightTextSecondary: '#7e22ce', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' } },
REFERRAL_PROGRAM_ENABLED: { name: 'Реферальная программа', description: 'Включить рефералку' }, { id: 'rose', colors: { accent: '#f43f5e', darkBackground: '#1a0a10', darkSurface: '#2d1520', darkText: '#fff1f2', darkTextSecondary: '#fda4af', lightBackground: '#ffe4e6', lightSurface: '#fff1f2', lightText: '#881337', lightTextSecondary: '#be123c', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' } },
YOOKASSA_ENABLED: { name: 'YooKassa', description: 'Приём платежей через YooKassa' }, { id: 'midnight', colors: { accent: '#6366f1', darkBackground: '#030712', darkSurface: '#111827', darkText: '#f9fafb', darkTextSecondary: '#9ca3af', lightBackground: '#e5e7eb', lightSurface: '#f3f4f6', lightText: '#111827', lightTextSecondary: '#4b5563', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' } },
CRYPTOBOT_ENABLED: { name: 'CryptoBot', description: 'Приём криптовалюты' }, { id: 'turquoise', colors: { accent: '#14b8a6', darkBackground: '#0a1614', darkSurface: '#134e4a', darkText: '#f0fdfa', darkTextSecondary: '#5eead4', lightBackground: '#ccfbf1', lightSurface: '#f0fdfa', lightText: '#134e4a', lightTextSecondary: '#0f766e', success: '#22c55e', warning: '#f59e0b', error: '#ef4444' } },
TELEGRAM_STARS_ENABLED: { name: 'Telegram Stars', description: 'Оплата звёздами' }, ]
TRIAL_DURATION_DAYS: { name: 'Триал (дней)', description: 'Длительность пробного периода' },
DEFAULT_DEVICE_LIMIT: { name: 'Устройств по умолчанию', description: 'Лимит устройств' },
// Add more as needed...
}
// ============ MAIN COMPONENT ============ // ============ MAIN COMPONENT ============
export default function AdminSettings() { export default function AdminSettings() {
@@ -177,12 +152,13 @@ export default function AdminSettings() {
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
// State // State
const [activeTab, setActiveTab] = useState('branding') const [activeSection, setActiveSection] = useState('branding')
const [activeSubCategory, setActiveSubCategory] = useState<string | null>(null)
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [showMoreDropdown, setShowMoreDropdown] = useState(false)
const [editingName, setEditingName] = useState(false) const [editingName, setEditingName] = useState(false)
const [newName, setNewName] = useState('') const [newName, setNewName] = useState('')
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set(['presets'])) const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set(['presets']))
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
// Favorites hook // Favorites hook
const { favorites, toggleFavorite, isFavorite } = useFavoriteSettings() const { favorites, toggleFavorite, isFavorite } = useFavoriteSettings()
@@ -318,24 +294,61 @@ export default function AdminSettings() {
}) })
} }
// Filter settings by tab // Get current menu item configuration
const currentMenuItem = useMemo(() => {
for (const section of MENU_SECTIONS) {
const item = section.items.find(i => i.id === activeSection)
if (item) return item
}
return null
}, [activeSection])
// Get categories for current section
const currentCategories = useMemo(() => {
if (!currentMenuItem?.categories || !allSettings || !Array.isArray(allSettings)) return []
const categoryMap = new Map<string, SettingDefinition[]>()
for (const setting of allSettings) {
if (currentMenuItem.categories.includes(setting.category.key)) {
if (!categoryMap.has(setting.category.key)) {
categoryMap.set(setting.category.key, [])
}
categoryMap.get(setting.category.key)!.push(setting)
}
}
return Array.from(categoryMap.entries()).map(([key, settings]) => ({
key,
label: t(`admin.settings.categories.${key}`, key),
settings
}))
}, [currentMenuItem, allSettings, t])
// Filter settings for current view
const filteredSettings = useMemo(() => { const filteredSettings = useMemo(() => {
if (!allSettings || !Array.isArray(allSettings)) return [] if (!allSettings || !Array.isArray(allSettings)) return []
const metaCategories = META_CATEGORIES[activeTab]?.categories || [] let settings: SettingDefinition[] = []
let filtered = allSettings.filter((s: SettingDefinition) => metaCategories.includes(s.category.key))
if (searchQuery) { if (activeSubCategory) {
const q = searchQuery.toLowerCase() settings = allSettings.filter((s: SettingDefinition) => s.category.key === activeSubCategory)
filtered = filtered.filter((s: SettingDefinition) => } else if (currentMenuItem?.categories) {
s.key.toLowerCase().includes(q) || settings = allSettings.filter((s: SettingDefinition) =>
s.name?.toLowerCase().includes(q) || currentMenuItem.categories!.includes(s.category.key)
SETTING_TRANSLATIONS[s.key]?.name.toLowerCase().includes(q)
) )
} }
return filtered if (searchQuery) {
}, [allSettings, activeTab, searchQuery]) const q = searchQuery.toLowerCase()
settings = settings.filter((s: SettingDefinition) =>
s.key.toLowerCase().includes(q) ||
s.name?.toLowerCase().includes(q)
)
}
return settings
}, [allSettings, activeSection, activeSubCategory, currentMenuItem, searchQuery])
// Favorite settings // Favorite settings
const favoriteSettings = useMemo(() => { const favoriteSettings = useMemo(() => {
@@ -359,9 +372,6 @@ export default function AdminSettings() {
) )
const renderSettingRow = (setting: SettingDefinition) => { const renderSettingRow = (setting: SettingDefinition) => {
const trans = SETTING_TRANSLATIONS[setting.key]
const name = trans?.name || setting.name || setting.key
const desc = trans?.description || setting.hint?.description
const isFav = isFavorite(setting.key) const isFav = isFavorite(setting.key)
return ( return (
@@ -371,14 +381,16 @@ export default function AdminSettings() {
> >
<div className="flex-1 min-w-0 mr-4"> <div className="flex-1 min-w-0 mr-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-medium text-dark-100 truncate">{name}</span> <span className="font-medium text-dark-100 truncate">{setting.name || setting.key}</span>
{setting.has_override && ( {setting.has_override && (
<span className="px-1.5 py-0.5 text-xs rounded bg-warning-500/20 text-warning-400"> <span className="px-1.5 py-0.5 text-xs rounded bg-warning-500/20 text-warning-400">
Изменено {t('admin.settings.modified')}
</span> </span>
)} )}
</div> </div>
{desc && <p className="text-sm text-dark-400 truncate mt-0.5">{desc}</p>} {setting.hint?.description && (
<p className="text-sm text-dark-400 truncate mt-0.5">{setting.hint.description}</p>
)}
</div> </div>
<div className="flex items-center gap-2 flex-shrink-0"> <div className="flex items-center gap-2 flex-shrink-0">
@@ -423,7 +435,7 @@ export default function AdminSettings() {
onClick={() => resetSettingMutation.mutate(setting.key)} onClick={() => resetSettingMutation.mutate(setting.key)}
disabled={resetSettingMutation.isPending} disabled={resetSettingMutation.isPending}
className="p-1.5 rounded-lg text-dark-400 hover:text-dark-200 hover:bg-dark-700 transition-colors" className="p-1.5 rounded-lg text-dark-400 hover:text-dark-200 hover:bg-dark-700 transition-colors"
title="Сбросить" title={t('admin.settings.reset')}
> >
<RefreshIcon /> <RefreshIcon />
</button> </button>
@@ -433,12 +445,12 @@ export default function AdminSettings() {
) )
} }
// ============ TAB CONTENT ============ // ============ CONTENT RENDERERS ============
const renderBrandingTab = () => ( const renderBrandingContent = () => (
<div className="space-y-6"> <div className="space-y-6">
{/* Logo & Name */} {/* Logo & Name */}
<div className="p-6 rounded-2xl bg-dark-800/50 border border-dark-700/50"> <div className="p-6 rounded-2xl bg-dark-800/50 border border-dark-700/50">
<h3 className="text-lg font-semibold text-dark-100 mb-4">Логотип и название</h3> <h3 className="text-lg font-semibold text-dark-100 mb-4">{t('admin.settings.logoAndName')}</h3>
<div className="flex items-start gap-6"> <div className="flex items-start gap-6">
{/* Logo */} {/* Logo */}
@@ -485,7 +497,7 @@ export default function AdminSettings() {
{/* Name */} {/* Name */}
<div className="flex-1"> <div className="flex-1">
<label className="block text-sm font-medium text-dark-300 mb-2">Название проекта</label> <label className="block text-sm font-medium text-dark-300 mb-2">{t('admin.settings.projectName')}</label>
{editingName ? ( {editingName ? (
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
@@ -493,7 +505,6 @@ export default function AdminSettings() {
value={newName} value={newName}
onChange={(e) => setNewName(e.target.value)} onChange={(e) => setNewName(e.target.value)}
className="flex-1 px-4 py-2 rounded-xl bg-dark-700 border border-dark-600 text-dark-100 focus:outline-none focus:border-accent-500" className="flex-1 px-4 py-2 rounded-xl bg-dark-700 border border-dark-600 text-dark-100 focus:outline-none focus:border-accent-500"
placeholder="Название"
maxLength={50} maxLength={50}
/> />
<button <button
@@ -512,7 +523,7 @@ export default function AdminSettings() {
</div> </div>
) : ( ) : (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-lg text-dark-100">{branding?.name || 'Не указано'}</span> <span className="text-lg text-dark-100">{branding?.name || t('admin.settings.notSpecified')}</span>
<button <button
onClick={() => { onClick={() => {
setNewName(branding?.name ?? '') setNewName(branding?.name ?? '')
@@ -530,13 +541,13 @@ export default function AdminSettings() {
{/* Animation & Fullscreen toggles */} {/* Animation & Fullscreen toggles */}
<div className="p-6 rounded-2xl bg-dark-800/50 border border-dark-700/50"> <div className="p-6 rounded-2xl bg-dark-800/50 border border-dark-700/50">
<h3 className="text-lg font-semibold text-dark-100 mb-4">Опции интерфейса</h3> <h3 className="text-lg font-semibold text-dark-100 mb-4">{t('admin.settings.interfaceOptions')}</h3>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between p-4 rounded-xl bg-dark-700/30"> <div className="flex items-center justify-between p-4 rounded-xl bg-dark-700/30">
<div> <div>
<span className="font-medium text-dark-100">Анимированный фон</span> <span className="font-medium text-dark-100">{t('admin.settings.animatedBackground')}</span>
<p className="text-sm text-dark-400">Волны на фоне приложения</p> <p className="text-sm text-dark-400">{t('admin.settings.animatedBackgroundDesc')}</p>
</div> </div>
{renderToggle( {renderToggle(
animationSettings?.enabled ?? true, animationSettings?.enabled ?? true,
@@ -547,8 +558,8 @@ export default function AdminSettings() {
<div className="flex items-center justify-between p-4 rounded-xl bg-dark-700/30"> <div className="flex items-center justify-between p-4 rounded-xl bg-dark-700/30">
<div> <div>
<span className="font-medium text-dark-100">Авто-Fullscreen</span> <span className="font-medium text-dark-100">{t('admin.settings.autoFullscreen')}</span>
<p className="text-sm text-dark-400">В Telegram WebApp</p> <p className="text-sm text-dark-400">{t('admin.settings.autoFullscreenDesc')}</p>
</div> </div>
{renderToggle( {renderToggle(
fullscreenSettings?.enabled ?? false, fullscreenSettings?.enabled ?? false,
@@ -561,17 +572,17 @@ export default function AdminSettings() {
</div> </div>
) )
const renderThemeTab = () => ( const renderThemeContent = () => (
<div className="space-y-6"> <div className="space-y-6">
{/* Theme toggles */} {/* Theme toggles */}
<div className="p-6 rounded-2xl bg-dark-800/50 border border-dark-700/50"> <div className="p-6 rounded-2xl bg-dark-800/50 border border-dark-700/50">
<h3 className="text-lg font-semibold text-dark-100 mb-4">Доступные темы</h3> <h3 className="text-lg font-semibold text-dark-100 mb-4">{t('admin.settings.availableThemes')}</h3>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="flex items-center justify-between p-4 rounded-xl bg-dark-700/30"> <div className="flex items-center justify-between p-4 rounded-xl bg-dark-700/30">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<MoonIcon /> <MoonIcon />
<span className="font-medium text-dark-200">Тёмная</span> <span className="font-medium text-dark-200">{t('admin.settings.darkTheme')}</span>
</div> </div>
{renderToggle( {renderToggle(
enabledThemes?.dark ?? true, enabledThemes?.dark ?? true,
@@ -586,7 +597,7 @@ export default function AdminSettings() {
<div className="flex items-center justify-between p-4 rounded-xl bg-dark-700/30"> <div className="flex items-center justify-between p-4 rounded-xl bg-dark-700/30">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<SunIcon /> <SunIcon />
<span className="font-medium text-dark-200">Светлая</span> <span className="font-medium text-dark-200">{t('admin.settings.lightTheme')}</span>
</div> </div>
{renderToggle( {renderToggle(
enabledThemes?.light ?? true, enabledThemes?.light ?? true,
@@ -606,7 +617,7 @@ export default function AdminSettings() {
onClick={() => toggleSection('presets')} onClick={() => toggleSection('presets')}
className="w-full flex items-center justify-between" className="w-full flex items-center justify-between"
> >
<h3 className="text-lg font-semibold text-dark-100">Быстрые пресеты</h3> <h3 className="text-lg font-semibold text-dark-100">{t('admin.settings.quickPresets')}</h3>
<div className={`transition-transform ${expandedSections.has('presets') ? 'rotate-180' : ''}`}> <div className={`transition-transform ${expandedSections.has('presets') ? 'rotate-180' : ''}`}>
<ChevronDownIcon /> <ChevronDownIcon />
</div> </div>
@@ -616,7 +627,7 @@ export default function AdminSettings() {
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mt-4"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mt-4">
{THEME_PRESETS.map((preset) => ( {THEME_PRESETS.map((preset) => (
<button <button
key={preset.name} key={preset.id}
onClick={() => updateColorsMutation.mutate(preset.colors)} onClick={() => updateColorsMutation.mutate(preset.colors)}
disabled={updateColorsMutation.isPending} disabled={updateColorsMutation.isPending}
className="p-3 rounded-xl border border-dark-600 hover:border-dark-500 transition-all hover:scale-[1.02]" className="p-3 rounded-xl border border-dark-600 hover:border-dark-500 transition-all hover:scale-[1.02]"
@@ -628,7 +639,7 @@ export default function AdminSettings() {
style={{ backgroundColor: preset.colors.accent }} style={{ backgroundColor: preset.colors.accent }}
/> />
<span className="text-xs font-medium" style={{ color: preset.colors.darkText }}> <span className="text-xs font-medium" style={{ color: preset.colors.darkText }}>
{preset.name} {t(`admin.settings.presets.${preset.id}`)}
</span> </span>
</div> </div>
<div className="flex gap-1"> <div className="flex gap-1">
@@ -648,7 +659,7 @@ export default function AdminSettings() {
onClick={() => toggleSection('colors')} onClick={() => toggleSection('colors')}
className="w-full flex items-center justify-between" className="w-full flex items-center justify-between"
> >
<h3 className="text-lg font-semibold text-dark-100">Ручная настройка</h3> <h3 className="text-lg font-semibold text-dark-100">{t('admin.settings.customColors')}</h3>
<div className={`transition-transform ${expandedSections.has('colors') ? 'rotate-180' : ''}`}> <div className={`transition-transform ${expandedSections.has('colors') ? 'rotate-180' : ''}`}>
<ChevronDownIcon /> <ChevronDownIcon />
</div> </div>
@@ -658,7 +669,7 @@ export default function AdminSettings() {
<div className="mt-4 space-y-6"> <div className="mt-4 space-y-6">
{/* Accent */} {/* Accent */}
<div> <div>
<h4 className="text-sm font-medium text-dark-300 mb-3">Акцентный цвет</h4> <h4 className="text-sm font-medium text-dark-300 mb-3">{t('admin.settings.accentColor')}</h4>
<ColorPicker <ColorPicker
label={t('theme.accent')} label={t('theme.accent')}
value={themeColors?.accent || DEFAULT_THEME_COLORS.accent} value={themeColors?.accent || DEFAULT_THEME_COLORS.accent}
@@ -670,29 +681,29 @@ export default function AdminSettings() {
{/* Dark theme */} {/* Dark theme */}
<div> <div>
<h4 className="text-sm font-medium text-dark-300 mb-3 flex items-center gap-2"> <h4 className="text-sm font-medium text-dark-300 mb-3 flex items-center gap-2">
<MoonIcon /> Тёмная тема <MoonIcon /> {t('admin.settings.darkTheme')}
</h4> </h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<ColorPicker <ColorPicker
label="Фон" label={t('admin.settings.colors.background')}
value={themeColors?.darkBackground || DEFAULT_THEME_COLORS.darkBackground} value={themeColors?.darkBackground || DEFAULT_THEME_COLORS.darkBackground}
onChange={(color) => updateColorsMutation.mutate({ darkBackground: color })} onChange={(color) => updateColorsMutation.mutate({ darkBackground: color })}
disabled={updateColorsMutation.isPending} disabled={updateColorsMutation.isPending}
/> />
<ColorPicker <ColorPicker
label="Поверхность" label={t('admin.settings.colors.surface')}
value={themeColors?.darkSurface || DEFAULT_THEME_COLORS.darkSurface} value={themeColors?.darkSurface || DEFAULT_THEME_COLORS.darkSurface}
onChange={(color) => updateColorsMutation.mutate({ darkSurface: color })} onChange={(color) => updateColorsMutation.mutate({ darkSurface: color })}
disabled={updateColorsMutation.isPending} disabled={updateColorsMutation.isPending}
/> />
<ColorPicker <ColorPicker
label="Текст" label={t('admin.settings.colors.text')}
value={themeColors?.darkText || DEFAULT_THEME_COLORS.darkText} value={themeColors?.darkText || DEFAULT_THEME_COLORS.darkText}
onChange={(color) => updateColorsMutation.mutate({ darkText: color })} onChange={(color) => updateColorsMutation.mutate({ darkText: color })}
disabled={updateColorsMutation.isPending} disabled={updateColorsMutation.isPending}
/> />
<ColorPicker <ColorPicker
label="Вторичный текст" label={t('admin.settings.colors.textSecondary')}
value={themeColors?.darkTextSecondary || DEFAULT_THEME_COLORS.darkTextSecondary} value={themeColors?.darkTextSecondary || DEFAULT_THEME_COLORS.darkTextSecondary}
onChange={(color) => updateColorsMutation.mutate({ darkTextSecondary: color })} onChange={(color) => updateColorsMutation.mutate({ darkTextSecondary: color })}
disabled={updateColorsMutation.isPending} disabled={updateColorsMutation.isPending}
@@ -703,29 +714,29 @@ export default function AdminSettings() {
{/* Light theme */} {/* Light theme */}
<div> <div>
<h4 className="text-sm font-medium text-dark-300 mb-3 flex items-center gap-2"> <h4 className="text-sm font-medium text-dark-300 mb-3 flex items-center gap-2">
<SunIcon /> Светлая тема <SunIcon /> {t('admin.settings.lightTheme')}
</h4> </h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<ColorPicker <ColorPicker
label="Фон" label={t('admin.settings.colors.background')}
value={themeColors?.lightBackground || DEFAULT_THEME_COLORS.lightBackground} value={themeColors?.lightBackground || DEFAULT_THEME_COLORS.lightBackground}
onChange={(color) => updateColorsMutation.mutate({ lightBackground: color })} onChange={(color) => updateColorsMutation.mutate({ lightBackground: color })}
disabled={updateColorsMutation.isPending} disabled={updateColorsMutation.isPending}
/> />
<ColorPicker <ColorPicker
label="Поверхность" label={t('admin.settings.colors.surface')}
value={themeColors?.lightSurface || DEFAULT_THEME_COLORS.lightSurface} value={themeColors?.lightSurface || DEFAULT_THEME_COLORS.lightSurface}
onChange={(color) => updateColorsMutation.mutate({ lightSurface: color })} onChange={(color) => updateColorsMutation.mutate({ lightSurface: color })}
disabled={updateColorsMutation.isPending} disabled={updateColorsMutation.isPending}
/> />
<ColorPicker <ColorPicker
label="Текст" label={t('admin.settings.colors.text')}
value={themeColors?.lightText || DEFAULT_THEME_COLORS.lightText} value={themeColors?.lightText || DEFAULT_THEME_COLORS.lightText}
onChange={(color) => updateColorsMutation.mutate({ lightText: color })} onChange={(color) => updateColorsMutation.mutate({ lightText: color })}
disabled={updateColorsMutation.isPending} disabled={updateColorsMutation.isPending}
/> />
<ColorPicker <ColorPicker
label="Вторичный текст" label={t('admin.settings.colors.textSecondary')}
value={themeColors?.lightTextSecondary || DEFAULT_THEME_COLORS.lightTextSecondary} value={themeColors?.lightTextSecondary || DEFAULT_THEME_COLORS.lightTextSecondary}
onChange={(color) => updateColorsMutation.mutate({ lightTextSecondary: color })} onChange={(color) => updateColorsMutation.mutate({ lightTextSecondary: color })}
disabled={updateColorsMutation.isPending} disabled={updateColorsMutation.isPending}
@@ -735,22 +746,22 @@ export default function AdminSettings() {
{/* Status colors */} {/* Status colors */}
<div> <div>
<h4 className="text-sm font-medium text-dark-300 mb-3">Статусные цвета</h4> <h4 className="text-sm font-medium text-dark-300 mb-3">{t('admin.settings.statusColors')}</h4>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<ColorPicker <ColorPicker
label="Успех" label={t('admin.settings.colors.success')}
value={themeColors?.success || DEFAULT_THEME_COLORS.success} value={themeColors?.success || DEFAULT_THEME_COLORS.success}
onChange={(color) => updateColorsMutation.mutate({ success: color })} onChange={(color) => updateColorsMutation.mutate({ success: color })}
disabled={updateColorsMutation.isPending} disabled={updateColorsMutation.isPending}
/> />
<ColorPicker <ColorPicker
label="Предупреждение" label={t('admin.settings.colors.warning')}
value={themeColors?.warning || DEFAULT_THEME_COLORS.warning} value={themeColors?.warning || DEFAULT_THEME_COLORS.warning}
onChange={(color) => updateColorsMutation.mutate({ warning: color })} onChange={(color) => updateColorsMutation.mutate({ warning: color })}
disabled={updateColorsMutation.isPending} disabled={updateColorsMutation.isPending}
/> />
<ColorPicker <ColorPicker
label="Ошибка" label={t('admin.settings.colors.error')}
value={themeColors?.error || DEFAULT_THEME_COLORS.error} value={themeColors?.error || DEFAULT_THEME_COLORS.error}
onChange={(color) => updateColorsMutation.mutate({ error: color })} onChange={(color) => updateColorsMutation.mutate({ error: color })}
disabled={updateColorsMutation.isPending} disabled={updateColorsMutation.isPending}
@@ -764,7 +775,7 @@ export default function AdminSettings() {
disabled={resetColorsMutation.isPending} disabled={resetColorsMutation.isPending}
className="px-4 py-2 rounded-xl bg-dark-700 text-dark-300 hover:bg-dark-600 transition-colors disabled:opacity-50" className="px-4 py-2 rounded-xl bg-dark-700 text-dark-300 hover:bg-dark-600 transition-colors disabled:opacity-50"
> >
Сбросить все цвета {t('admin.settings.resetAllColors')}
</button> </button>
</div> </div>
)} )}
@@ -772,15 +783,15 @@ export default function AdminSettings() {
</div> </div>
) )
const renderFavoritesTab = () => ( const renderFavoritesContent = () => (
<div className="space-y-4"> <div className="space-y-4">
{favoriteSettings.length === 0 ? ( {favoriteSettings.length === 0 ? (
<div className="p-12 rounded-2xl bg-dark-800/30 border border-dark-700/30 text-center"> <div className="p-12 rounded-2xl bg-dark-800/30 border border-dark-700/30 text-center">
<div className="flex justify-center mb-4 text-dark-500">
<StarIcon filled={false} /> <StarIcon filled={false} />
<p className="text-dark-400 mt-4">Нет избранных настроек</p> </div>
<p className="text-dark-500 text-sm mt-1"> <p className="text-dark-400">{t('admin.settings.favoritesEmpty')}</p>
Нажмите рядом с настройкой, чтобы добавить её сюда <p className="text-dark-500 text-sm mt-1">{t('admin.settings.favoritesHint')}</p>
</p>
</div> </div>
) : ( ) : (
favoriteSettings.map(renderSettingRow) favoriteSettings.map(renderSettingRow)
@@ -788,11 +799,42 @@ export default function AdminSettings() {
</div> </div>
) )
const renderSettingsTab = () => ( const renderSettingsContent = () => (
<div className="space-y-4"> <div className="space-y-4">
{/* Sub-categories navigation */}
{currentCategories.length > 1 && !activeSubCategory && (
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 mb-6">
{currentCategories.map((cat) => (
<button
key={cat.key}
onClick={() => setActiveSubCategory(cat.key)}
className="p-4 rounded-xl bg-dark-800/50 border border-dark-700/50 hover:border-dark-600 transition-all text-left"
>
<div className="flex items-center justify-between">
<span className="font-medium text-dark-200">{cat.label}</span>
<ChevronRightIcon />
</div>
<span className="text-sm text-dark-500">{cat.settings.length} {t('admin.settings.settingsCount')}</span>
</button>
))}
</div>
)}
{/* Back button if in sub-category */}
{activeSubCategory && (
<button
onClick={() => setActiveSubCategory(null)}
className="flex items-center gap-2 text-dark-400 hover:text-dark-200 transition-colors mb-4"
>
<BackIcon />
<span>{t('common.back')}</span>
</button>
)}
{/* Settings list */}
{filteredSettings.length === 0 ? ( {filteredSettings.length === 0 ? (
<div className="p-12 rounded-2xl bg-dark-800/30 border border-dark-700/30 text-center"> <div className="p-12 rounded-2xl bg-dark-800/30 border border-dark-700/30 text-center">
<p className="text-dark-400">Нет настроек в этой категории</p> <p className="text-dark-400">{t('admin.settings.noSettings')}</p>
</div> </div>
) : ( ) : (
filteredSettings.map(renderSettingRow) filteredSettings.map(renderSettingRow)
@@ -802,87 +844,50 @@ export default function AdminSettings() {
// ============ RENDER ============ // ============ RENDER ============
return ( return (
<div className="space-y-6"> <div className="flex h-full min-h-[600px]">
{/* Sidebar */}
<div className={`flex-shrink-0 bg-dark-900/50 border-r border-dark-700/50 transition-all ${
sidebarCollapsed ? 'w-16' : 'w-64'
}`}>
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="p-4 border-b border-dark-700/50">
<div className="flex items-center gap-4"> <div className="flex items-center gap-3">
<Link to="/admin" className="p-2 rounded-xl bg-dark-800 hover:bg-dark-700 transition-colors"> <Link to="/admin" className="p-2 rounded-xl bg-dark-800 hover:bg-dark-700 transition-colors">
<BackIcon /> <BackIcon />
</Link> </Link>
<h1 className="text-2xl font-bold text-dark-100">Настройки</h1> {!sidebarCollapsed && (
</div> <h1 className="text-lg font-bold text-dark-100">{t('admin.settings.title')}</h1>
{/* Search */}
<div className="relative">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Поиск..."
className="w-64 pl-10 pr-4 py-2 rounded-xl bg-dark-800 border border-dark-700 text-dark-100 placeholder-dark-500 focus:outline-none focus:border-accent-500"
/>
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-dark-500">
<SearchIcon />
</div>
</div>
</div>
{/* Tabs */}
<div className="flex items-center gap-1 p-1 rounded-2xl bg-dark-800/50 border border-dark-700/50 overflow-x-auto">
{TABS.map((tab) => {
if (tab.id === 'more') {
return (
<div key={tab.id} className="relative">
<button
onClick={() => setShowMoreDropdown(!showMoreDropdown)}
className={`flex items-center gap-1 px-4 py-2.5 rounded-xl text-sm font-medium transition-all whitespace-nowrap ${
MORE_TABS.some(t => t.id === activeTab)
? 'bg-accent-500 text-white'
: 'text-dark-400 hover:text-dark-200 hover:bg-dark-700/50'
}`}
>
{tab.label}
<ChevronDownIcon />
</button>
{showMoreDropdown && (
<div className="absolute top-full right-0 mt-2 py-2 min-w-[160px] rounded-xl bg-dark-800 border border-dark-700 shadow-xl z-50">
{MORE_TABS.map((moreTab) => (
<button
key={moreTab.id}
onClick={() => {
setActiveTab(moreTab.id)
setShowMoreDropdown(false)
}}
className={`w-full px-4 py-2 text-left text-sm transition-colors ${
activeTab === moreTab.id
? 'text-accent-400 bg-accent-500/10'
: 'text-dark-300 hover:text-dark-100 hover:bg-dark-700/50'
}`}
>
{moreTab.label}
</button>
))}
</div>
)} )}
</div> </div>
) </div>
}
{/* Menu */}
<nav className="p-2 space-y-1">
{MENU_SECTIONS.map((section, sectionIdx) => (
<div key={section.id}>
{sectionIdx > 0 && <div className="my-3 border-t border-dark-700/50" />}
{section.items.map((item) => {
const isActive = activeSection === item.id
const Icon = item.icon
return ( return (
<button <button
key={tab.id} key={item.id}
onClick={() => setActiveTab(tab.id)} onClick={() => {
className={`flex items-center gap-2 px-4 py-2.5 rounded-xl text-sm font-medium transition-all whitespace-nowrap ${ setActiveSection(item.id)
activeTab === tab.id setActiveSubCategory(null)
? 'bg-accent-500 text-white' }}
: 'text-dark-400 hover:text-dark-200 hover:bg-dark-700/50' className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all ${
isActive
? 'bg-accent-500/10 text-accent-400'
: 'text-dark-400 hover:text-dark-200 hover:bg-dark-800/50'
}`} }`}
> >
{tab.id === 'favorites' && <StarIcon filled={activeTab === 'favorites'} />} {Icon && <Icon filled={isActive && item.id === 'favorites'} />}
{tab.label} {!sidebarCollapsed && (
{tab.id === 'favorites' && favorites.length > 0 && ( <span className="font-medium">{t(`admin.settings.${item.id}`)}</span>
<span className="ml-1 px-1.5 py-0.5 text-xs rounded-full bg-white/20"> )}
{item.id === 'favorites' && favorites.length > 0 && !sidebarCollapsed && (
<span className="ml-auto px-2 py-0.5 text-xs rounded-full bg-warning-500/20 text-warning-400">
{favorites.length} {favorites.length}
</span> </span>
)} )}
@@ -890,13 +895,54 @@ export default function AdminSettings() {
) )
})} })}
</div> </div>
))}
</nav>
{/* Tab Content */} {/* Collapse toggle */}
<div className="min-h-[400px]"> <button
{activeTab === 'favorites' && renderFavoritesTab()} onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
{activeTab === 'branding' && renderBrandingTab()} className="absolute bottom-4 left-4 p-2 rounded-lg text-dark-500 hover:text-dark-300 hover:bg-dark-800 transition-colors"
{activeTab === 'theme' && renderThemeTab()} >
{['payments', 'subscriptions', 'interface', 'notifications', 'database', 'system', 'users'].includes(activeTab) && renderSettingsTab()} <div className={`transition-transform ${sidebarCollapsed ? 'rotate-180' : ''}`}>
<ChevronRightIcon />
</div>
</button>
</div>
{/* Main content */}
<div className="flex-1 overflow-auto">
{/* Search header */}
<div className="sticky top-0 z-10 bg-dark-900/80 backdrop-blur-xl border-b border-dark-700/50 p-4">
<div className="flex items-center gap-4">
<h2 className="text-xl font-semibold text-dark-100">
{t(`admin.settings.${activeSection}`)}
{activeSubCategory && (
<span className="text-dark-400 font-normal"> / {t(`admin.settings.categories.${activeSubCategory}`, activeSubCategory)}</span>
)}
</h2>
<div className="flex-1" />
<div className="relative">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={t('admin.settings.searchPlaceholder')}
className="w-64 pl-10 pr-4 py-2 rounded-xl bg-dark-800 border border-dark-700 text-dark-100 placeholder-dark-500 focus:outline-none focus:border-accent-500"
/>
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-dark-500">
<SearchIcon />
</div>
</div>
</div>
</div>
{/* Content */}
<div className="p-6">
{activeSection === 'favorites' && renderFavoritesContent()}
{activeSection === 'branding' && renderBrandingContent()}
{activeSection === 'theme' && renderThemeContent()}
{['payments', 'subscriptions', 'interface', 'notifications', 'database', 'system', 'users'].includes(activeSection) && renderSettingsContent()}
</div>
</div> </div>
</div> </div>
) )