From 8776e2d128fba0e3bce5d0e1d82219d982b44dd3 Mon Sep 17 00:00:00 2001 From: PEDZEO Date: Wed, 21 Jan 2026 03:56:21 +0300 Subject: [PATCH] Refactor AdminSettings into modular components Split 1123-line monolithic AdminSettings.tsx into 11 modules: Components: - icons.tsx (85 lines) - All SVG icons - Toggle.tsx (21 lines) - Reusable toggle component - SettingInput.tsx (89 lines) - Text/select input for settings - SettingRow.tsx (104 lines) - Single setting card - BrandingTab.tsx (212 lines) - Logo, name, animation settings - ThemeTab.tsx (277 lines) - Theme presets and custom colors - FavoritesTab.tsx (59 lines) - Starred settings display - SettingsTab.tsx (144 lines) - Accordion categories view Utilities: - constants.ts (55 lines) - MENU_SECTIONS, THEME_PRESETS - utils.ts (35 lines) - formatSettingKey, stripHtml - index.ts (13 lines) - Barrel exports Result: AdminSettings.tsx reduced from 1123 to 251 lines --- src/components/admin/BrandingTab.tsx | 212 ++++++ src/components/admin/FavoritesTab.tsx | 59 ++ src/components/admin/SettingCard.tsx | 259 ------- src/components/admin/SettingInput.tsx | 89 +++ src/components/admin/SettingRow.tsx | 104 +++ src/components/admin/SettingsTab.tsx | 144 ++++ src/components/admin/ThemeTab.tsx | 277 ++++++++ src/components/admin/Toggle.tsx | 21 + src/components/admin/constants.ts | 55 ++ src/components/admin/icons.tsx | 85 +++ src/components/admin/index.ts | 13 + src/components/admin/utils.ts | 35 + src/pages/AdminSettings.tsx | 972 ++------------------------ 13 files changed, 1144 insertions(+), 1181 deletions(-) create mode 100644 src/components/admin/BrandingTab.tsx create mode 100644 src/components/admin/FavoritesTab.tsx delete mode 100644 src/components/admin/SettingCard.tsx create mode 100644 src/components/admin/SettingInput.tsx create mode 100644 src/components/admin/SettingRow.tsx create mode 100644 src/components/admin/SettingsTab.tsx create mode 100644 src/components/admin/ThemeTab.tsx create mode 100644 src/components/admin/Toggle.tsx create mode 100644 src/components/admin/constants.ts create mode 100644 src/components/admin/icons.tsx create mode 100644 src/components/admin/index.ts create mode 100644 src/components/admin/utils.ts diff --git a/src/components/admin/BrandingTab.tsx b/src/components/admin/BrandingTab.tsx new file mode 100644 index 0000000..82b45f1 --- /dev/null +++ b/src/components/admin/BrandingTab.tsx @@ -0,0 +1,212 @@ +import { useState, useRef } from 'react' +import { useTranslation } from 'react-i18next' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { brandingApi, setCachedBranding } from '../../api/branding' +import { setCachedAnimationEnabled } from '../AnimatedBackground' +import { setCachedFullscreenEnabled } from '../../hooks/useTelegramWebApp' +import { UploadIcon, TrashIcon, PencilIcon, CheckIcon, CloseIcon } from './icons' +import { Toggle } from './Toggle' + +interface BrandingTabProps { + accentColor?: string +} + +export function BrandingTab({ accentColor = '#3b82f6' }: BrandingTabProps) { + const { t } = useTranslation() + const queryClient = useQueryClient() + const fileInputRef = useRef(null) + + const [editingName, setEditingName] = useState(false) + const [newName, setNewName] = useState('') + + // Queries + const { data: branding } = useQuery({ + queryKey: ['branding'], + queryFn: brandingApi.getBranding, + }) + + const { data: animationSettings } = useQuery({ + queryKey: ['animation-enabled'], + queryFn: brandingApi.getAnimationEnabled, + }) + + const { data: fullscreenSettings } = useQuery({ + queryKey: ['fullscreen-enabled'], + queryFn: brandingApi.getFullscreenEnabled, + }) + + // Mutations + const updateBrandingMutation = useMutation({ + mutationFn: brandingApi.updateName, + onSuccess: (data) => { + setCachedBranding(data) + queryClient.invalidateQueries({ queryKey: ['branding'] }) + setEditingName(false) + }, + }) + + const uploadLogoMutation = useMutation({ + mutationFn: brandingApi.uploadLogo, + onSuccess: (data) => { + setCachedBranding(data) + queryClient.invalidateQueries({ queryKey: ['branding'] }) + }, + }) + + const deleteLogoMutation = useMutation({ + mutationFn: brandingApi.deleteLogo, + onSuccess: (data) => { + setCachedBranding(data) + queryClient.invalidateQueries({ queryKey: ['branding'] }) + }, + }) + + const updateAnimationMutation = useMutation({ + mutationFn: (enabled: boolean) => brandingApi.updateAnimationEnabled(enabled), + onSuccess: (data) => { + setCachedAnimationEnabled(data.enabled) + queryClient.invalidateQueries({ queryKey: ['animation-enabled'] }) + }, + }) + + const updateFullscreenMutation = useMutation({ + mutationFn: (enabled: boolean) => brandingApi.updateFullscreenEnabled(enabled), + onSuccess: (data) => { + setCachedFullscreenEnabled(data.enabled) + queryClient.invalidateQueries({ queryKey: ['fullscreen-enabled'] }) + }, + }) + + const handleLogoUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (file) { + uploadLogoMutation.mutate(file) + } + } + + return ( +
+ {/* Logo & Name */} +
+

{t('admin.settings.logoAndName')}

+ +
+ {/* Logo */} +
+
+ {branding?.has_custom_logo ? ( + Logo + ) : ( + branding?.logo_letter || 'V' + )} +
+ +
+ + + {branding?.has_custom_logo && ( + + )} +
+
+ + {/* Name */} +
+ + {editingName ? ( +
+ 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" + maxLength={50} + /> + + +
+ ) : ( +
+ {branding?.name || t('admin.settings.notSpecified')} + +
+ )} +
+
+
+ + {/* Animation & Fullscreen toggles */} +
+

{t('admin.settings.interfaceOptions')}

+ +
+
+
+ {t('admin.settings.animatedBackground')} +

{t('admin.settings.animatedBackgroundDesc')}

+
+ updateAnimationMutation.mutate(!(animationSettings?.enabled ?? true))} + disabled={updateAnimationMutation.isPending} + /> +
+ +
+
+ {t('admin.settings.autoFullscreen')} +

{t('admin.settings.autoFullscreenDesc')}

+
+ updateFullscreenMutation.mutate(!(fullscreenSettings?.enabled ?? false))} + disabled={updateFullscreenMutation.isPending} + /> +
+
+
+
+ ) +} diff --git a/src/components/admin/FavoritesTab.tsx b/src/components/admin/FavoritesTab.tsx new file mode 100644 index 0000000..c70709d --- /dev/null +++ b/src/components/admin/FavoritesTab.tsx @@ -0,0 +1,59 @@ +import { useTranslation } from 'react-i18next' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { SettingDefinition, adminSettingsApi } from '../../api/adminSettings' +import { StarIcon } from './icons' +import { SettingRow } from './SettingRow' + +interface FavoritesTabProps { + settings: SettingDefinition[] + isFavorite: (key: string) => boolean + toggleFavorite: (key: string) => void +} + +export function FavoritesTab({ settings, isFavorite, toggleFavorite }: FavoritesTabProps) { + const { t } = useTranslation() + const queryClient = useQueryClient() + + const updateSettingMutation = useMutation({ + mutationFn: ({ key, value }: { key: string; value: string }) => adminSettingsApi.updateSetting(key, value), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin-settings'] }) + }, + }) + + const resetSettingMutation = useMutation({ + mutationFn: (key: string) => adminSettingsApi.resetSetting(key), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin-settings'] }) + }, + }) + + if (settings.length === 0) { + return ( +
+
+ +
+

{t('admin.settings.favoritesEmpty')}

+

{t('admin.settings.favoritesHint')}

+
+ ) + } + + return ( +
+ {settings.map((setting) => ( + toggleFavorite(setting.key)} + onUpdate={(value) => updateSettingMutation.mutate({ key: setting.key, value })} + onReset={() => resetSettingMutation.mutate(setting.key)} + isUpdating={updateSettingMutation.isPending} + isResetting={resetSettingMutation.isPending} + /> + ))} +
+ ) +} diff --git a/src/components/admin/SettingCard.tsx b/src/components/admin/SettingCard.tsx deleted file mode 100644 index f2b7f56..0000000 --- a/src/components/admin/SettingCard.tsx +++ /dev/null @@ -1,259 +0,0 @@ -import { useState, useCallback } from 'react' - -interface SettingChoice { - value: string - label: string - description?: string -} - -interface SettingDefinition { - key: string - name: string - description?: string - category: string - type: string - is_optional: boolean - current: string | number | boolean | null - original: string | number | boolean | null - has_override: boolean - read_only: boolean - choices?: SettingChoice[] - hint?: string -} - -interface SettingCardProps { - setting: SettingDefinition - translatedName?: string - translatedDescription?: string - isFavorite: boolean - onToggleFavorite: () => void - onUpdate: (value: string) => void - onReset: () => void - disabled?: boolean -} - -// Icons -const StarIcon = ({ filled }: { filled: boolean }) => ( - - - -) - -const RefreshIcon = () => ( - - - -) - -const LockIcon = () => ( - - - -) - -const CheckIcon = () => ( - - - -) - -const CloseIcon = () => ( - - - -) - -export function SettingCard({ - setting, - translatedName, - translatedDescription, - isFavorite, - onToggleFavorite, - onUpdate, - onReset, - disabled -}: SettingCardProps) { - const [isEditing, setIsEditing] = useState(false) - const [editValue, setEditValue] = useState('') - - const displayName = translatedName || setting.name || setting.key - const displayDescription = translatedDescription || setting.description - - const handleStartEdit = useCallback(() => { - if (setting.read_only) return - setEditValue(String(setting.current ?? '')) - setIsEditing(true) - }, [setting.current, setting.read_only]) - - const handleSave = useCallback(() => { - onUpdate(editValue) - setIsEditing(false) - }, [editValue, onUpdate]) - - const handleCancel = useCallback(() => { - setIsEditing(false) - setEditValue('') - }, []) - - const handleKeyDown = useCallback((e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - handleSave() - } else if (e.key === 'Escape') { - handleCancel() - } - }, [handleSave, handleCancel]) - - const handleToggle = useCallback(() => { - if (setting.read_only) return - const newValue = setting.current === true || setting.current === 'true' ? 'false' : 'true' - onUpdate(newValue) - }, [setting.current, setting.read_only, onUpdate]) - - const handleSelectChange = useCallback((e: React.ChangeEvent) => { - onUpdate(e.target.value) - }, [onUpdate]) - - // Render input based on type - const renderInput = () => { - // Read-only display - if (setting.read_only) { - return ( -
- - {String(setting.current ?? '-')} -
- ) - } - - // Boolean toggle - if (setting.type === 'bool') { - const isChecked = setting.current === true || setting.current === 'true' - return ( - - ) - } - - // Select dropdown - if (setting.choices && setting.choices.length > 0) { - return ( - - ) - } - - // Text/number input - if (isEditing) { - return ( -
- setEditValue(e.target.value)} - onKeyDown={handleKeyDown} - autoFocus - className="bg-dark-700 border border-accent-500 rounded-lg px-3 py-1.5 text-sm text-dark-100 focus:outline-none w-40" - /> - - -
- ) - } - - return ( - - ) - } - - return ( -
-
- {/* Left side - info */} -
-
- {displayName} - {setting.has_override && ( - - Изменено - - )} -
- {displayDescription && ( -

{displayDescription}

- )} - {setting.hint && ( -

{setting.hint}

- )} -
- - {/* Right side - controls */} -
- {/* Favorite button */} - - - {/* Input control */} - {renderInput()} - - {/* Reset button */} - {setting.has_override && !setting.read_only && ( - - )} -
-
-
- ) -} diff --git a/src/components/admin/SettingInput.tsx b/src/components/admin/SettingInput.tsx new file mode 100644 index 0000000..4279fd8 --- /dev/null +++ b/src/components/admin/SettingInput.tsx @@ -0,0 +1,89 @@ +import { useState } from 'react' +import { SettingDefinition } from '../../api/adminSettings' +import { CheckIcon, CloseIcon } from './icons' + +interface SettingInputProps { + setting: SettingDefinition + onUpdate: (value: string) => void + disabled?: boolean +} + +export function SettingInput({ setting, onUpdate, disabled }: SettingInputProps) { + const [isEditing, setIsEditing] = useState(false) + const [value, setValue] = useState('') + + const handleStart = () => { + setValue(String(setting.current ?? '')) + setIsEditing(true) + } + + const handleSave = () => { + onUpdate(value) + setIsEditing(false) + } + + const handleCancel = () => { + setIsEditing(false) + setValue('') + } + + // Dropdown for choices + if (setting.choices && setting.choices.length > 0) { + return ( + + ) + } + + // Editing mode + if (isEditing) { + return ( +
+ setValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleSave() + if (e.key === 'Escape') handleCancel() + }} + autoFocus + className="bg-dark-700 border border-accent-500 rounded-lg px-3 py-1.5 text-sm text-dark-100 focus:outline-none w-32" + /> + + +
+ ) + } + + // Display mode + return ( + + ) +} diff --git a/src/components/admin/SettingRow.tsx b/src/components/admin/SettingRow.tsx new file mode 100644 index 0000000..a607f81 --- /dev/null +++ b/src/components/admin/SettingRow.tsx @@ -0,0 +1,104 @@ +import { useTranslation } from 'react-i18next' +import { SettingDefinition } from '../../api/adminSettings' +import { StarIcon, LockIcon, RefreshIcon } from './icons' +import { SettingInput } from './SettingInput' +import { Toggle } from './Toggle' +import { formatSettingKey, stripHtml } from './utils' + +interface SettingRowProps { + setting: SettingDefinition + isFavorite: boolean + onToggleFavorite: () => void + onUpdate: (value: string) => void + onReset: () => void + isUpdating?: boolean + isResetting?: boolean +} + +export function SettingRow({ + setting, + isFavorite, + onToggleFavorite, + onUpdate, + onReset, + isUpdating, + isResetting +}: SettingRowProps) { + const { t } = useTranslation() + + const formattedKey = formatSettingKey(setting.name || setting.key) + const displayName = t(`admin.settings.settingNames.${formattedKey}`, formattedKey) + const description = setting.hint?.description ? stripHtml(setting.hint.description) : null + + return ( +
+ {/* Top row - name and badge */} +
+
+
+ {displayName} + {setting.has_override && ( + + {t('admin.settings.modified')} + + )} +
+ {description && ( +

{description}

+ )} +
+ + {/* Favorite button */} + +
+ + {/* Bottom row - control */} +
+ {setting.key} + +
+ {/* Setting control */} + {setting.read_only ? ( +
+ + {String(setting.current ?? '-')} +
+ ) : setting.type === 'bool' ? ( + onUpdate(setting.current === true || setting.current === 'true' ? 'false' : 'true')} + disabled={isUpdating} + /> + ) : ( + + )} + + {/* Reset button */} + {setting.has_override && !setting.read_only && ( + + )} +
+
+
+ ) +} diff --git a/src/components/admin/SettingsTab.tsx b/src/components/admin/SettingsTab.tsx new file mode 100644 index 0000000..44d74ee --- /dev/null +++ b/src/components/admin/SettingsTab.tsx @@ -0,0 +1,144 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { SettingDefinition, adminSettingsApi } from '../../api/adminSettings' +import { ChevronDownIcon } from './icons' +import { SettingRow } from './SettingRow' + +interface CategoryGroup { + key: string + label: string + settings: SettingDefinition[] +} + +interface SettingsTabProps { + categories: CategoryGroup[] + searchQuery: string + filteredSettings: SettingDefinition[] + isFavorite: (key: string) => boolean + toggleFavorite: (key: string) => void +} + +export function SettingsTab({ + categories, + searchQuery, + filteredSettings, + isFavorite, + toggleFavorite +}: SettingsTabProps) { + const { t } = useTranslation() + const queryClient = useQueryClient() + + const [expandedSections, setExpandedSections] = useState>(new Set()) + + const toggleSection = (key: string) => { + setExpandedSections(prev => { + const next = new Set(prev) + if (next.has(key)) { + next.delete(key) + } else { + next.add(key) + } + return next + }) + } + + const updateSettingMutation = useMutation({ + mutationFn: ({ key, value }: { key: string; value: string }) => adminSettingsApi.updateSetting(key, value), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin-settings'] }) + }, + }) + + const resetSettingMutation = useMutation({ + mutationFn: (key: string) => adminSettingsApi.resetSetting(key), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin-settings'] }) + }, + }) + + // If searching, show flat list + if (searchQuery) { + return ( +
+ {filteredSettings.length === 0 ? ( +
+

{t('admin.settings.noSettings')}

+
+ ) : ( +
+ {filteredSettings.map((setting) => ( + toggleFavorite(setting.key)} + onUpdate={(value) => updateSettingMutation.mutate({ key: setting.key, value })} + onReset={() => resetSettingMutation.mutate(setting.key)} + isUpdating={updateSettingMutation.isPending} + isResetting={resetSettingMutation.isPending} + /> + ))} +
+ )} +
+ ) + } + + // Show accordion for subcategories + return ( +
+ {categories.map((cat) => { + const isExpanded = expandedSections.has(cat.key) + return ( +
+ {/* Accordion header */} + + + {/* Accordion content */} + {isExpanded && ( +
+
+ {cat.settings.map((setting) => ( + toggleFavorite(setting.key)} + onUpdate={(value) => updateSettingMutation.mutate({ key: setting.key, value })} + onReset={() => resetSettingMutation.mutate(setting.key)} + isUpdating={updateSettingMutation.isPending} + isResetting={resetSettingMutation.isPending} + /> + ))} +
+
+ )} +
+ ) + })} + + {categories.length === 0 && ( +
+

{t('admin.settings.noSettings')}

+
+ )} +
+ ) +} diff --git a/src/components/admin/ThemeTab.tsx b/src/components/admin/ThemeTab.tsx new file mode 100644 index 0000000..1f797e7 --- /dev/null +++ b/src/components/admin/ThemeTab.tsx @@ -0,0 +1,277 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { themeColorsApi } from '../../api/themeColors' +import { DEFAULT_THEME_COLORS } from '../../types/theme' +import { ColorPicker } from '../ColorPicker' +import { applyThemeColors } from '../../hooks/useThemeColors' +import { updateEnabledThemesCache } from '../../hooks/useTheme' +import { MoonIcon, SunIcon, ChevronDownIcon } from './icons' +import { Toggle } from './Toggle' +import { THEME_PRESETS } from './constants' + +export function ThemeTab() { + const { t } = useTranslation() + const queryClient = useQueryClient() + + const [expandedSections, setExpandedSections] = useState>(new Set(['presets'])) + + const toggleSection = (section: string) => { + setExpandedSections(prev => { + const next = new Set(prev) + if (next.has(section)) { + next.delete(section) + } else { + next.add(section) + } + return next + }) + } + + // Queries + const { data: themeColors } = useQuery({ + queryKey: ['theme-colors'], + queryFn: themeColorsApi.getColors, + }) + + const { data: enabledThemes } = useQuery({ + queryKey: ['enabled-themes'], + queryFn: themeColorsApi.getEnabledThemes, + }) + + // Mutations + const updateColorsMutation = useMutation({ + mutationFn: themeColorsApi.updateColors, + onSuccess: (data) => { + applyThemeColors(data) + queryClient.invalidateQueries({ queryKey: ['theme-colors'] }) + }, + }) + + const resetColorsMutation = useMutation({ + mutationFn: themeColorsApi.resetColors, + onSuccess: (data) => { + applyThemeColors(data) + queryClient.invalidateQueries({ queryKey: ['theme-colors'] }) + }, + }) + + const updateEnabledThemesMutation = useMutation({ + mutationFn: themeColorsApi.updateEnabledThemes, + onSuccess: (data) => { + updateEnabledThemesCache(data) + queryClient.invalidateQueries({ queryKey: ['enabled-themes'] }) + }, + }) + + return ( +
+ {/* Theme toggles */} +
+

{t('admin.settings.availableThemes')}

+ +
+
+
+ + {t('admin.settings.darkTheme')} +
+ { + if ((enabledThemes?.dark ?? true) && !(enabledThemes?.light ?? true)) return + updateEnabledThemesMutation.mutate({ dark: !(enabledThemes?.dark ?? true) }) + }} + disabled={updateEnabledThemesMutation.isPending} + /> +
+ +
+
+ + {t('admin.settings.lightTheme')} +
+ { + if ((enabledThemes?.light ?? true) && !(enabledThemes?.dark ?? true)) return + updateEnabledThemesMutation.mutate({ light: !(enabledThemes?.light ?? true) }) + }} + disabled={updateEnabledThemesMutation.isPending} + /> +
+
+
+ + {/* Quick Presets */} +
+ + + {expandedSections.has('presets') && ( +
+ {THEME_PRESETS.map((preset) => ( + + ))} +
+ )} +
+ + {/* Custom Colors */} +
+ + + {expandedSections.has('colors') && ( +
+ {/* Accent */} +
+

{t('admin.settings.accentColor')}

+ updateColorsMutation.mutate({ accent: color })} + disabled={updateColorsMutation.isPending} + /> +
+ + {/* Dark theme */} +
+

+ {t('admin.settings.darkTheme')} +

+
+ updateColorsMutation.mutate({ darkBackground: color })} + disabled={updateColorsMutation.isPending} + /> + updateColorsMutation.mutate({ darkSurface: color })} + disabled={updateColorsMutation.isPending} + /> + updateColorsMutation.mutate({ darkText: color })} + disabled={updateColorsMutation.isPending} + /> + updateColorsMutation.mutate({ darkTextSecondary: color })} + disabled={updateColorsMutation.isPending} + /> +
+
+ + {/* Light theme */} +
+

+ {t('admin.settings.lightTheme')} +

+
+ updateColorsMutation.mutate({ lightBackground: color })} + disabled={updateColorsMutation.isPending} + /> + updateColorsMutation.mutate({ lightSurface: color })} + disabled={updateColorsMutation.isPending} + /> + updateColorsMutation.mutate({ lightText: color })} + disabled={updateColorsMutation.isPending} + /> + updateColorsMutation.mutate({ lightTextSecondary: color })} + disabled={updateColorsMutation.isPending} + /> +
+
+ + {/* Status colors */} +
+

{t('admin.settings.statusColors')}

+
+ updateColorsMutation.mutate({ success: color })} + disabled={updateColorsMutation.isPending} + /> + updateColorsMutation.mutate({ warning: color })} + disabled={updateColorsMutation.isPending} + /> + updateColorsMutation.mutate({ error: color })} + disabled={updateColorsMutation.isPending} + /> +
+
+ + {/* Reset button */} + +
+ )} +
+
+ ) +} diff --git a/src/components/admin/Toggle.tsx b/src/components/admin/Toggle.tsx new file mode 100644 index 0000000..7159cb0 --- /dev/null +++ b/src/components/admin/Toggle.tsx @@ -0,0 +1,21 @@ +interface ToggleProps { + checked: boolean + onChange: () => void + disabled?: boolean +} + +export function Toggle({ checked, onChange, disabled }: ToggleProps) { + return ( + + ) +} diff --git a/src/components/admin/constants.ts b/src/components/admin/constants.ts new file mode 100644 index 0000000..f686581 --- /dev/null +++ b/src/components/admin/constants.ts @@ -0,0 +1,55 @@ +import { ThemeColors, DEFAULT_THEME_COLORS } from '../../types/theme' + +// Menu item types +export interface MenuItem { + id: string + iconType?: 'star' | null + categories?: string[] +} + +export interface MenuSection { + id: string + items: MenuItem[] +} + +// Sidebar menu configuration +export const MENU_SECTIONS: MenuSection[] = [ + { + id: 'main', + items: [ + { id: 'favorites', iconType: 'star' }, + { id: 'branding', iconType: null }, + { id: 'theme', iconType: null }, + ] + }, + { + id: 'settings', + items: [ + { id: 'payments', iconType: null, categories: ['PAYMENT', 'PAYMENT_VERIFICATION', 'YOOKASSA', 'CRYPTOBOT', 'HELEKET', 'PLATEGA', 'TRIBUTE', 'MULENPAY', 'PAL24', 'WATA', 'TELEGRAM'] }, + { id: 'subscriptions', iconType: null, categories: ['SUBSCRIPTIONS_CORE', 'SIMPLE_SUBSCRIPTION', 'PERIODS', 'SUBSCRIPTION_PRICES', 'TRAFFIC', 'TRAFFIC_PACKAGES', 'TRIAL', 'AUTOPAY'] }, + { id: 'interface', iconType: null, categories: ['INTERFACE', 'INTERFACE_BRANDING', 'INTERFACE_SUBSCRIPTION', 'CONNECT_BUTTON', 'MINIAPP', 'HAPP', 'SKIP', 'ADDITIONAL'] }, + { id: 'notifications', iconType: null, categories: ['NOTIFICATIONS', 'ADMIN_NOTIFICATIONS', 'ADMIN_REPORTS'] }, + { id: 'database', iconType: null, categories: ['DATABASE', 'POSTGRES', 'SQLITE', 'REDIS'] }, + { id: 'system', iconType: null, categories: ['CORE', 'REMNAWAVE', 'SERVER_STATUS', 'MONITORING', 'MAINTENANCE', 'BACKUP', 'VERSION', 'WEB_API', 'WEBHOOK', 'LOG', 'DEBUG', 'EXTERNAL_ADMIN'] }, + { id: 'users', iconType: null, categories: ['SUPPORT', 'LOCALIZATION', 'CHANNEL', 'TIMEZONE', 'REFERRAL', 'MODERATION'] }, + ] + } +] + +// Theme preset type +export interface ThemePreset { + id: string + colors: ThemeColors +} + +// Theme presets +export const THEME_PRESETS: ThemePreset[] = [ + { id: 'standard', colors: DEFAULT_THEME_COLORS }, + { 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' } }, + { 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' } }, + { 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' } }, + { 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' } }, + { 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' } }, + { 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' } }, + { 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' } }, +] diff --git a/src/components/admin/icons.tsx b/src/components/admin/icons.tsx new file mode 100644 index 0000000..d26e3fc --- /dev/null +++ b/src/components/admin/icons.tsx @@ -0,0 +1,85 @@ +// Admin Settings Icons + +export const BackIcon = () => ( + + + +) + +export const SearchIcon = () => ( + + + +) + +export const StarIcon = ({ filled }: { filled?: boolean }) => ( + + + +) + +export const ChevronDownIcon = () => ( + + + +) + +export const UploadIcon = () => ( + + + +) + +export const TrashIcon = () => ( + + + +) + +export const PencilIcon = () => ( + + + +) + +export const RefreshIcon = () => ( + + + +) + +export const LockIcon = () => ( + + + +) + +export const CheckIcon = () => ( + + + +) + +export const CloseIcon = () => ( + + + +) + +export const SunIcon = () => ( + + + +) + +export const MoonIcon = () => ( + + + +) + +export const MenuIcon = () => ( + + + +) diff --git a/src/components/admin/index.ts b/src/components/admin/index.ts new file mode 100644 index 0000000..82a4f6a --- /dev/null +++ b/src/components/admin/index.ts @@ -0,0 +1,13 @@ +// Components +export * from './icons' +export * from './Toggle' +export * from './SettingInput' +export * from './SettingRow' +export * from './BrandingTab' +export * from './ThemeTab' +export * from './FavoritesTab' +export * from './SettingsTab' + +// Constants and utils +export * from './constants' +export * from './utils' diff --git a/src/components/admin/utils.ts b/src/components/admin/utils.ts new file mode 100644 index 0000000..1d99712 --- /dev/null +++ b/src/components/admin/utils.ts @@ -0,0 +1,35 @@ +// Format setting key from Snake_Case / CamelCase to readable text +export function formatSettingKey(name: string): string { + if (!name) return '' + + return name + // CamelCase -> spaces + .replace(/([a-z])([A-Z])/g, '$1 $2') + // snake_case -> spaces + .replace(/_/g, ' ') + // Remove extra spaces + .replace(/\s+/g, ' ') + .trim() + // Capitalize first letter + .replace(/^./, c => c.toUpperCase()) +} + +// Strip HTML tags and template descriptions from setting descriptions +export function stripHtml(html: string): string { + if (!html) return '' + let cleaned = html + .replace(/<[^>]*>/g, '') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .trim() + + // Remove template descriptions like "Параметр X управляет категорией Y" + if (cleaned.match(/^Параметр .+ управляет категорией/)) { + return '' + } + + return cleaned +} diff --git a/src/pages/AdminSettings.tsx b/src/pages/AdminSettings.tsx index 096beb2..2b25a28 100644 --- a/src/pages/AdminSettings.tsx +++ b/src/pages/AdminSettings.tsx @@ -1,339 +1,50 @@ -import { useState, useMemo, useRef } from 'react' -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { useState, useMemo } from 'react' +import { useQuery } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import { adminSettingsApi, SettingDefinition } from '../api/adminSettings' -import { brandingApi, setCachedBranding } from '../api/branding' -import { setCachedAnimationEnabled } from '../components/AnimatedBackground' -import { setCachedFullscreenEnabled } from '../hooks/useTelegramWebApp' import { themeColorsApi } from '../api/themeColors' -import { ThemeColors, DEFAULT_THEME_COLORS } from '../types/theme' -import { ColorPicker } from '../components/ColorPicker' -import { applyThemeColors } from '../hooks/useThemeColors' -import { updateEnabledThemesCache } from '../hooks/useTheme' import { useFavoriteSettings } from '../hooks/useFavoriteSettings' +import { + BackIcon, + SearchIcon, + StarIcon, + CloseIcon, + MenuIcon, + MENU_SECTIONS, + MenuItem +} from '../components/admin' +import { BrandingTab } from '../components/admin/BrandingTab' +import { ThemeTab } from '../components/admin/ThemeTab' +import { FavoritesTab } from '../components/admin/FavoritesTab' +import { SettingsTab } from '../components/admin/SettingsTab' -// ============ ICONS ============ -const BackIcon = () => ( - - - -) - -const SearchIcon = () => ( - - - -) - -const StarIcon = ({ filled }: { filled?: boolean }) => ( - - - -) - -const ChevronDownIcon = () => ( - - - -) - -const UploadIcon = () => ( - - - -) - -const TrashIcon = () => ( - - - -) - -const PencilIcon = () => ( - - - -) - -const RefreshIcon = () => ( - - - -) - -const LockIcon = () => ( - - - -) - -const CheckIcon = () => ( - - - -) - -const CloseIcon = () => ( - - - -) - -const SunIcon = () => ( - - - -) - -const MoonIcon = () => ( - - - -) - -const MenuIcon = () => ( - - - -) - -// ============ HELPER FUNCTIONS ============ -// Форматирование названия настройки (Snake_Case / CamelCase -> читаемый текст) -function formatSettingKey(name: string): string { - if (!name) return '' - - return name - // CamelCase -> пробелы - .replace(/([a-z])([A-Z])/g, '$1 $2') - // snake_case -> пробелы - .replace(/_/g, ' ') - // Убираем лишние пробелы - .replace(/\s+/g, ' ') - .trim() - // Первая буква заглавная - .replace(/^./, c => c.toUpperCase()) -} - -// Очистка HTML тегов и шаблонных описаний -function stripHtml(html: string): string { - if (!html) return '' - let cleaned = html - .replace(/<[^>]*>/g, '') - .replace(/ /g, ' ') - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .trim() - - // Убираем шаблонные описания типа "Параметр X управляет категорией Y" - if (cleaned.match(/^Параметр .+ управляет категорией/)) { - return '' - } - - return cleaned -} - -// ============ SIDEBAR MENU ITEMS ============ -interface MenuItem { - id: string - icon: ((props: { filled?: boolean }) => JSX.Element) | null - categories?: string[] -} - -interface MenuSection { - id: string - items: MenuItem[] -} - -const MENU_SECTIONS: MenuSection[] = [ - { - id: 'main', - items: [ - { 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'] }, - ] - } -] - -// ============ THEME PRESETS ============ -const THEME_PRESETS: { id: string; colors: ThemeColors }[] = [ - { id: 'standard', colors: DEFAULT_THEME_COLORS }, - { 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' } }, - { 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' } }, - { 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' } }, - { 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' } }, - { 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' } }, - { 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' } }, - { 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' } }, -] - -// ============ MAIN COMPONENT ============ export default function AdminSettings() { const { t } = useTranslation() - const queryClient = useQueryClient() - const fileInputRef = useRef(null) // State const [activeSection, setActiveSection] = useState('branding') const [searchQuery, setSearchQuery] = useState('') - const [editingName, setEditingName] = useState(false) - const [newName, setNewName] = useState('') - const [expandedSections, setExpandedSections] = useState>(new Set(['presets'])) const [mobileMenuOpen, setMobileMenuOpen] = useState(false) // Favorites hook const { favorites, toggleFavorite, isFavorite } = useFavoriteSettings() - // ============ QUERIES ============ - const { data: branding } = useQuery({ - queryKey: ['branding'], - queryFn: brandingApi.getBranding, - }) - - const { data: animationSettings } = useQuery({ - queryKey: ['animation-enabled'], - queryFn: brandingApi.getAnimationEnabled, - }) - - const { data: fullscreenSettings } = useQuery({ - queryKey: ['fullscreen-enabled'], - queryFn: brandingApi.getFullscreenEnabled, - }) - + // Queries const { data: themeColors } = useQuery({ queryKey: ['theme-colors'], queryFn: themeColorsApi.getColors, }) - const { data: enabledThemes } = useQuery({ - queryKey: ['enabled-themes'], - queryFn: themeColorsApi.getEnabledThemes, - }) - const { data: allSettings } = useQuery({ queryKey: ['admin-settings'], queryFn: () => adminSettingsApi.getSettings(), }) - // ============ MUTATIONS ============ - const updateBrandingMutation = useMutation({ - mutationFn: brandingApi.updateName, - onSuccess: (data) => { - setCachedBranding(data) - queryClient.invalidateQueries({ queryKey: ['branding'] }) - setEditingName(false) - }, - }) - - const uploadLogoMutation = useMutation({ - mutationFn: brandingApi.uploadLogo, - onSuccess: (data) => { - setCachedBranding(data) - queryClient.invalidateQueries({ queryKey: ['branding'] }) - }, - }) - - const deleteLogoMutation = useMutation({ - mutationFn: brandingApi.deleteLogo, - onSuccess: (data) => { - setCachedBranding(data) - queryClient.invalidateQueries({ queryKey: ['branding'] }) - }, - }) - - const updateAnimationMutation = useMutation({ - mutationFn: (enabled: boolean) => brandingApi.updateAnimationEnabled(enabled), - onSuccess: (data) => { - setCachedAnimationEnabled(data.enabled) - queryClient.invalidateQueries({ queryKey: ['animation-enabled'] }) - }, - }) - - const updateFullscreenMutation = useMutation({ - mutationFn: (enabled: boolean) => brandingApi.updateFullscreenEnabled(enabled), - onSuccess: (data) => { - setCachedFullscreenEnabled(data.enabled) - queryClient.invalidateQueries({ queryKey: ['fullscreen-enabled'] }) - }, - }) - - const updateColorsMutation = useMutation({ - mutationFn: themeColorsApi.updateColors, - onSuccess: (data) => { - applyThemeColors(data) - queryClient.invalidateQueries({ queryKey: ['theme-colors'] }) - }, - }) - - const resetColorsMutation = useMutation({ - mutationFn: themeColorsApi.resetColors, - onSuccess: (data) => { - applyThemeColors(data) - queryClient.invalidateQueries({ queryKey: ['theme-colors'] }) - }, - }) - - const updateEnabledThemesMutation = useMutation({ - mutationFn: themeColorsApi.updateEnabledThemes, - onSuccess: (data) => { - updateEnabledThemesCache(data) - queryClient.invalidateQueries({ queryKey: ['enabled-themes'] }) - }, - }) - - const updateSettingMutation = useMutation({ - mutationFn: ({ key, value }: { key: string; value: string }) => adminSettingsApi.updateSetting(key, value), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['admin-settings'] }) - }, - }) - - const resetSettingMutation = useMutation({ - mutationFn: (key: string) => adminSettingsApi.resetSetting(key), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['admin-settings'] }) - }, - }) - - // ============ HANDLERS ============ - const handleLogoUpload = (e: React.ChangeEvent) => { - const file = e.target.files?.[0] - if (file) { - uploadLogoMutation.mutate(file) - } - } - - const toggleSection = (section: string) => { - setExpandedSections(prev => { - const next = new Set(prev) - if (next.has(section)) { - next.delete(section) - } else { - next.add(section) - } - return next - }) - } - // Get current menu item configuration const currentMenuItem = useMemo(() => { for (const section of MENU_SECTIONS) { - const item = section.items.find(i => i.id === activeSection) + const item = section.items.find((i: MenuItem) => i.id === activeSection) if (item) return item } return null @@ -382,529 +93,37 @@ export default function AdminSettings() { return allSettings.filter((s: SettingDefinition) => favorites.includes(s.key)) }, [allSettings, favorites]) - // ============ RENDER HELPERS ============ - const renderToggle = (checked: boolean, onChange: () => void, disabled?: boolean) => ( - - ) - - const renderSettingRow = (setting: SettingDefinition) => { - const isFav = isFavorite(setting.key) - // Форматируем ключ в читаемый вид и ищем перевод - const formattedKey = formatSettingKey(setting.name || setting.key) - const displayName = t(`admin.settings.settingNames.${formattedKey}`, formattedKey) - const description = setting.hint?.description ? stripHtml(setting.hint.description) : null - - return ( -
- {/* Top row - name and badge */} -
-
-
- {displayName} - {setting.has_override && ( - - {t('admin.settings.modified')} - - )} -
- {description && ( -

{description}

- )} -
- - {/* Favorite button */} - -
- - {/* Bottom row - control */} -
- {setting.key} - -
- {/* Setting control */} - {setting.read_only ? ( -
- - {String(setting.current ?? '-')} -
- ) : setting.type === 'bool' ? ( - renderToggle( - setting.current === true || setting.current === 'true', - () => updateSettingMutation.mutate({ - key: setting.key, - value: setting.current === true || setting.current === 'true' ? 'false' : 'true' - }), - updateSettingMutation.isPending - ) - ) : ( - updateSettingMutation.mutate({ key: setting.key, value })} - disabled={updateSettingMutation.isPending} - /> - )} - - {/* Reset button */} - {setting.has_override && !setting.read_only && ( - - )} -
-
-
- ) - } - - // ============ CONTENT RENDERERS ============ - const renderBrandingContent = () => ( -
- {/* Logo & Name */} -
-

{t('admin.settings.logoAndName')}

- -
- {/* Logo */} -
-
- {branding?.has_custom_logo ? ( - Logo - ) : ( - branding?.logo_letter || 'V' - )} -
- -
- - - {branding?.has_custom_logo && ( - - )} -
-
- - {/* Name */} -
- - {editingName ? ( -
- 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" - maxLength={50} - /> - - -
- ) : ( -
- {branding?.name || t('admin.settings.notSpecified')} - -
- )} -
-
-
- - {/* Animation & Fullscreen toggles */} -
-

{t('admin.settings.interfaceOptions')}

- -
-
-
- {t('admin.settings.animatedBackground')} -

{t('admin.settings.animatedBackgroundDesc')}

-
- {renderToggle( - animationSettings?.enabled ?? true, - () => updateAnimationMutation.mutate(!(animationSettings?.enabled ?? true)), - updateAnimationMutation.isPending - )} -
- -
-
- {t('admin.settings.autoFullscreen')} -

{t('admin.settings.autoFullscreenDesc')}

-
- {renderToggle( - fullscreenSettings?.enabled ?? false, - () => updateFullscreenMutation.mutate(!(fullscreenSettings?.enabled ?? false)), - updateFullscreenMutation.isPending - )} -
-
-
-
- ) - - const renderThemeContent = () => ( -
- {/* Theme toggles */} -
-

{t('admin.settings.availableThemes')}

- -
-
-
- - {t('admin.settings.darkTheme')} -
- {renderToggle( - enabledThemes?.dark ?? true, - () => { - if ((enabledThemes?.dark ?? true) && !(enabledThemes?.light ?? true)) return - updateEnabledThemesMutation.mutate({ dark: !(enabledThemes?.dark ?? true) }) - }, - updateEnabledThemesMutation.isPending - )} -
- -
-
- - {t('admin.settings.lightTheme')} -
- {renderToggle( - enabledThemes?.light ?? true, - () => { - if ((enabledThemes?.light ?? true) && !(enabledThemes?.dark ?? true)) return - updateEnabledThemesMutation.mutate({ light: !(enabledThemes?.light ?? true) }) - }, - updateEnabledThemesMutation.isPending - )} -
-
-
- - {/* Quick Presets */} -
- - - {expandedSections.has('presets') && ( -
- {THEME_PRESETS.map((preset) => ( - - ))} -
- )} -
- - {/* Custom Colors */} -
- - - {expandedSections.has('colors') && ( -
- {/* Accent */} -
-

{t('admin.settings.accentColor')}

- updateColorsMutation.mutate({ accent: color })} - disabled={updateColorsMutation.isPending} - /> -
- - {/* Dark theme */} -
-

- {t('admin.settings.darkTheme')} -

-
- updateColorsMutation.mutate({ darkBackground: color })} - disabled={updateColorsMutation.isPending} - /> - updateColorsMutation.mutate({ darkSurface: color })} - disabled={updateColorsMutation.isPending} - /> - updateColorsMutation.mutate({ darkText: color })} - disabled={updateColorsMutation.isPending} - /> - updateColorsMutation.mutate({ darkTextSecondary: color })} - disabled={updateColorsMutation.isPending} - /> -
-
- - {/* Light theme */} -
-

- {t('admin.settings.lightTheme')} -

-
- updateColorsMutation.mutate({ lightBackground: color })} - disabled={updateColorsMutation.isPending} - /> - updateColorsMutation.mutate({ lightSurface: color })} - disabled={updateColorsMutation.isPending} - /> - updateColorsMutation.mutate({ lightText: color })} - disabled={updateColorsMutation.isPending} - /> - updateColorsMutation.mutate({ lightTextSecondary: color })} - disabled={updateColorsMutation.isPending} - /> -
-
- - {/* Status colors */} -
-

{t('admin.settings.statusColors')}

-
- updateColorsMutation.mutate({ success: color })} - disabled={updateColorsMutation.isPending} - /> - updateColorsMutation.mutate({ warning: color })} - disabled={updateColorsMutation.isPending} - /> - updateColorsMutation.mutate({ error: color })} - disabled={updateColorsMutation.isPending} - /> -
-
- - {/* Reset button */} - -
- )} -
-
- ) - - const renderFavoritesContent = () => ( -
- {favoriteSettings.length === 0 ? ( -
-
- -
-

{t('admin.settings.favoritesEmpty')}

-

{t('admin.settings.favoritesHint')}

-
- ) : ( -
- {favoriteSettings.map(renderSettingRow)} -
- )} -
- ) - - const renderSettingsContent = () => { - // If searching, show flat list - if (searchQuery) { - return ( -
- {filteredSettings.length === 0 ? ( -
-

{t('admin.settings.noSettings')}

-
- ) : ( -
- {filteredSettings.map(renderSettingRow)} -
- )} -
- ) - } - - // Show accordion for subcategories - return ( -
- {currentCategories.map((cat) => { - const isExpanded = expandedSections.has(cat.key) + // Render content based on active section + const renderContent = () => { + switch (activeSection) { + case 'branding': + return + case 'theme': + return + case 'favorites': + return ( + + ) + default: + if (['payments', 'subscriptions', 'interface', 'notifications', 'database', 'system', 'users'].includes(activeSection)) { return ( -
- {/* Accordion header */} - - - {/* Accordion content */} - {isExpanded && ( -
-
- {cat.settings.map(renderSettingRow)} -
-
- )} -
+ ) - })} - - {currentCategories.length === 0 && ( -
-

{t('admin.settings.noSettings')}

-
- )} -
- ) + } + return null + } } - // ============ RENDER ============ return (
{/* Mobile overlay */} @@ -915,7 +134,7 @@ export default function AdminSettings() { /> )} - {/* Sidebar - drawer on mobile, static on desktop */} + {/* Sidebar */}
- ) - } - - return ( - - ) -}