+ // Normal mode: QuickToggles + settings by category
+ const allCategorySettings = categories.flatMap((c) => c.settings);
- {/* 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}
- />
- ))}
-
+ if (allCategorySettings.length === 0) {
+ return (
+
+
{t('admin.settings.noSettings')}
+
+ );
+ }
+
+ return (
+
+
updateSettingMutation.mutate({ key, value })}
+ disabled={updateSettingMutation.isPending}
+ />
+ {categories.map((category) => {
+ if (category.settings.length === 0) return null;
+ return (
+
+ {categories.length > 1 && (
+
+
{category.label}
+ {category.settings.length}
)}
+
+ {category.settings.map((setting, idx) => (
+ toggleFavorite(setting.key)}
+ onUpdate={(value) => updateSettingMutation.mutate({ key: setting.key, value })}
+ onReset={() => resetSettingMutation.mutate(setting.key)}
+ isUpdating={updateSettingMutation.isPending}
+ isResetting={resetSettingMutation.isPending}
+ isLast={idx === category.settings.length - 1}
+ />
+ ))}
+
);
})}
-
- {categories.length === 0 && (
-
-
{t('admin.settings.noSettings')}
-
- )}
);
}
diff --git a/src/components/admin/SettingsTableRow.tsx b/src/components/admin/SettingsTableRow.tsx
new file mode 100644
index 0000000..b25f893
--- /dev/null
+++ b/src/components/admin/SettingsTableRow.tsx
@@ -0,0 +1,162 @@
+import { useTranslation } from 'react-i18next';
+import { SettingDefinition } from '../../api/adminSettings';
+import { cn } from '../../lib/utils';
+import { StarIcon, LockIcon, RefreshIcon } from './icons';
+import { SettingInput } from './SettingInput';
+import { Toggle } from './Toggle';
+import { formatSettingKey, stripHtml } from './utils';
+
+interface SettingsTableRowProps {
+ setting: SettingDefinition;
+ isFavorite: boolean;
+ onToggleFavorite: () => void;
+ onUpdate: (value: string) => void;
+ onReset: () => void;
+ isUpdating?: boolean;
+ isResetting?: boolean;
+ isLast?: boolean;
+ className?: string;
+}
+
+export function SettingsTableRow({
+ setting,
+ isFavorite,
+ onToggleFavorite,
+ onUpdate,
+ onReset,
+ isUpdating,
+ isResetting,
+ isLast,
+ className,
+}: SettingsTableRowProps) {
+ 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;
+ const isModified = setting.has_override;
+ const isBool = setting.type === 'bool';
+ const boolChecked = setting.current === true || setting.current === 'true';
+
+ const isLongValue = (() => {
+ const val = String(setting.current ?? '');
+ const key = setting.key.toLowerCase();
+ return (
+ val.length > 50 ||
+ val.includes('\n') ||
+ val.startsWith('[') ||
+ val.startsWith('{') ||
+ key.includes('_items') ||
+ key.includes('_config') ||
+ key.includes('_keywords') ||
+ key.includes('_template') ||
+ key.includes('_packages')
+ );
+ })();
+
+ return (
+
+
+ {/* Left side: name, badges, key */}
+
+ {/* Name + badges row */}
+
+ {displayName}
+
+ {isModified && (
+
+ {t('admin.settings.modified')}
+
+ )}
+
+ {setting.has_override && !setting.read_only && (
+
+ {t('admin.settings.badgeDb')}
+
+ )}
+
+ {setting.read_only && (
+
+ {t('admin.settings.badgeEnv')}
+
+
+ )}
+
+
+ {/* Setting key */}
+
+ {setting.key}
+
+
+ {/* Description for long values */}
+ {isLongValue && description && (
+
{description}
+ )}
+
+
+ {/* Right side: control + action buttons */}
+
+ {setting.read_only ? (
+
+ {String(setting.current ?? '-')}
+
+ ) : isBool ? (
+
onUpdate(boolChecked ? 'false' : 'true')}
+ disabled={isUpdating}
+ aria-label={displayName}
+ />
+ ) : (
+
+
+
+ )}
+
+ {/* Reset button -- hover-reveal when has_override */}
+ {isModified && !setting.read_only && (
+
+
+
+ )}
+
+ {/* Favorite button -- visible if favorited, hover-reveal otherwise */}
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/admin/SettingsTreeSidebar.tsx b/src/components/admin/SettingsTreeSidebar.tsx
new file mode 100644
index 0000000..68ac48e
--- /dev/null
+++ b/src/components/admin/SettingsTreeSidebar.tsx
@@ -0,0 +1,322 @@
+import { useState, useRef, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+import { SETTINGS_TREE } from './constants';
+import { StarIcon, SearchIcon, CloseIcon, ChevronDownIcon } from './icons';
+import { SettingDefinition } from '../../api/adminSettings';
+import { formatSettingKey } from './utils';
+import { cn } from '../../lib/utils';
+
+interface SettingsTreeSidebarProps {
+ activeSection: string;
+ onSectionChange: (sectionId: string) => void;
+ favoritesCount: number;
+ searchQuery: string;
+ onSearchChange: (query: string) => void;
+ allSettings?: SettingDefinition[];
+ onSelectSetting?: (setting: SettingDefinition) => void;
+ className?: string;
+}
+
+export function SettingsTreeSidebar({
+ activeSection,
+ onSectionChange,
+ favoritesCount,
+ searchQuery,
+ onSearchChange,
+ allSettings,
+ onSelectSetting,
+ className,
+}: SettingsTreeSidebarProps) {
+ const { t } = useTranslation();
+ const [expandedGroup, setExpandedGroup] = useState
(null);
+ const [isSearchOpen, setIsSearchOpen] = useState(false);
+ const [highlightedIndex, setHighlightedIndex] = useState(0);
+ const searchContainerRef = useRef(null);
+ const inputRef = useRef(null);
+
+ // Auto-expand the group containing the active section
+ useEffect(() => {
+ for (const group of SETTINGS_TREE.groups) {
+ if (group.children.some((child) => child.id === activeSection)) {
+ setExpandedGroup(group.id);
+ return;
+ }
+ }
+ }, [activeSection]);
+
+ // Filter settings for autocomplete
+ const suggestions =
+ searchQuery.trim() && allSettings
+ ? allSettings
+ .filter((s) => {
+ const q = searchQuery.toLowerCase().trim();
+ if (s.key.toLowerCase().includes(q)) return true;
+ if (s.name?.toLowerCase().includes(q)) return true;
+ const formattedKey = formatSettingKey(s.name || s.key);
+ const translatedName = t(`admin.settings.settingNames.${formattedKey}`, formattedKey);
+ if (translatedName.toLowerCase().includes(q)) return true;
+ if (s.hint?.description?.toLowerCase().includes(q)) return true;
+ return false;
+ })
+ .slice(0, 8)
+ : [];
+
+ // Close dropdown on click outside
+ useEffect(() => {
+ const handleClickOutside = (e: MouseEvent) => {
+ if (searchContainerRef.current && !searchContainerRef.current.contains(e.target as Node)) {
+ setIsSearchOpen(false);
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ // Reset highlighted index when suggestions change
+ useEffect(() => {
+ setHighlightedIndex(0);
+ }, [suggestions.length, searchQuery]);
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (!isSearchOpen || suggestions.length === 0) return;
+
+ switch (e.key) {
+ case 'ArrowDown':
+ e.preventDefault();
+ setHighlightedIndex((i) => (i + 1) % suggestions.length);
+ break;
+ case 'ArrowUp':
+ e.preventDefault();
+ setHighlightedIndex((i) => (i - 1 + suggestions.length) % suggestions.length);
+ break;
+ case 'Enter':
+ e.preventDefault();
+ if (suggestions[highlightedIndex]) {
+ handleSelectSuggestion(suggestions[highlightedIndex]);
+ }
+ break;
+ case 'Escape':
+ setIsSearchOpen(false);
+ inputRef.current?.blur();
+ break;
+ }
+ };
+
+ const handleSelectSuggestion = (setting: SettingDefinition) => {
+ setIsSearchOpen(false);
+ onSearchChange(setting.name || setting.key);
+ onSelectSetting?.(setting);
+ };
+
+ const getSettingDisplayName = (setting: SettingDefinition) => {
+ const formattedKey = formatSettingKey(setting.name || setting.key);
+ return t(`admin.settings.settingNames.${formattedKey}`, formattedKey);
+ };
+
+ const handleGroupToggle = (groupId: string) => {
+ if (expandedGroup === groupId) {
+ setExpandedGroup(null);
+ } else {
+ setExpandedGroup(groupId);
+ // Auto-select first child when expanding
+ const group = SETTINGS_TREE.groups.find((g) => g.id === groupId);
+ if (group && group.children.length > 0) {
+ onSectionChange(group.children[0].id);
+ }
+ }
+ };
+
+ const isGroupActive = (groupId: string) => {
+ const group = SETTINGS_TREE.groups.find((g) => g.id === groupId);
+ return group?.children.some((child) => child.id === activeSection) ?? false;
+ };
+
+ // Special items excluding favorites (favorites is rendered separately)
+ const customizationItems = SETTINGS_TREE.specialItems.filter((item) => item.id !== 'favorites');
+
+ return (
+
+ {/* Search bar */}
+
+
{
+ onSearchChange(e.target.value);
+ setIsSearchOpen(true);
+ }}
+ onFocus={() => setIsSearchOpen(true)}
+ onKeyDown={handleKeyDown}
+ placeholder={t('admin.settings.searchPlaceholder')}
+ className="w-full rounded-lg border border-dark-700/50 bg-dark-800/50 py-2 pl-9 pr-8 text-sm text-dark-100 placeholder-dark-500 transition-colors focus:border-accent-500 focus:outline-none"
+ />
+
+
+
+ {searchQuery && (
+
{
+ onSearchChange('');
+ setIsSearchOpen(false);
+ }}
+ className="absolute right-6 top-1/2 -translate-y-1/2 text-dark-500 transition-colors hover:text-dark-300"
+ >
+
+
+ )}
+
+ {/* Autocomplete dropdown */}
+ {isSearchOpen && suggestions.length > 0 && (
+
+ {suggestions.map((setting, index) => (
+ handleSelectSuggestion(setting)}
+ onMouseEnter={() => setHighlightedIndex(index)}
+ className={cn(
+ 'flex w-full flex-col gap-0.5 px-3 py-2 text-left transition-colors',
+ index === highlightedIndex ? 'bg-accent-500/20' : 'hover:bg-dark-700/50',
+ )}
+ >
+
+ {getSettingDisplayName(setting)}
+
+
+ {t(`admin.settings.categories.${setting.category.key}`, setting.category.key)}
+
+
+ ))}
+
+ )}
+
+
+ {/* Favorites button */}
+
+ onSectionChange('favorites')}
+ className={cn(
+ 'flex w-full items-center gap-3 rounded-lg px-3 py-2 transition-all',
+ activeSection === 'favorites'
+ ? 'bg-accent-500/10 text-accent-400'
+ : 'text-dark-400 hover:bg-dark-800/50 hover:text-dark-200',
+ )}
+ >
+
+ {t('admin.settings.favorites', 'Favorites')}
+ {favoritesCount > 0 && (
+
+ {favoritesCount}
+
+ )}
+
+
+
+ {/* Divider */}
+
+
+ {/* Customization section label */}
+
+
+ {t('admin.settings.customization', 'Customization')}
+
+
+
+ {/* Special items (branding, theme, analytics, buttons) */}
+
+ {customizationItems.map((item) => {
+ const isActive = activeSection === item.id;
+ return (
+ onSectionChange(item.id)}
+ className={cn(
+ 'flex w-full items-center gap-3 rounded-lg px-3 py-2 transition-all',
+ isActive
+ ? 'bg-accent-500/10 text-accent-400'
+ : 'text-dark-400 hover:bg-dark-800/50 hover:text-dark-200',
+ )}
+ >
+ {item.icon && {item.icon} }
+ {t(`admin.settings.${item.id}`, item.id)}
+
+ );
+ })}
+
+
+ {/* Divider */}
+
+
+ {/* Settings section label */}
+
+
+ {t('admin.settings.settingsLabel', 'Settings')}
+
+
+
+ {/* Tree groups */}
+
+ {SETTINGS_TREE.groups.map((group) => {
+ const isExpanded = expandedGroup === group.id;
+ const hasActiveChild = isGroupActive(group.id);
+
+ return (
+
+ {/* Group header */}
+
handleGroupToggle(group.id)}
+ className={cn(
+ 'flex w-full items-center gap-3 rounded-lg px-3 py-2 transition-all',
+ hasActiveChild
+ ? 'text-accent-300'
+ : 'text-dark-400 hover:bg-dark-800/50 hover:text-dark-200',
+ )}
+ >
+ {group.icon}
+
+ {t(`admin.settings.groups.${group.id}`, group.id)}
+
+
+
+
+ {/* Children */}
+ {isExpanded && (
+
+ {group.children.map((child) => {
+ const isActive = activeSection === child.id;
+ return (
+ onSectionChange(child.id)}
+ className={cn(
+ 'flex w-full items-center rounded-lg px-3 py-1.5 text-left text-sm transition-all',
+ isActive
+ ? 'bg-accent-500/10 text-accent-400'
+ : 'text-dark-400 hover:bg-dark-800/50 hover:text-dark-200',
+ )}
+ >
+ {t(`admin.settings.tree.${child.id}`, child.id)}
+
+ );
+ })}
+
+ )}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/components/admin/constants.ts b/src/components/admin/constants.ts
index 812f33f..356de38 100644
--- a/src/components/admin/constants.ts
+++ b/src/components/admin/constants.ts
@@ -1,111 +1,160 @@
import { ThemeColors, DEFAULT_THEME_COLORS } from '../../types/theme';
-// Menu item types
-export interface MenuItem {
+// Tree sidebar types
+export interface TreeSubItem {
id: string;
+ categories: string[];
+}
+
+export interface TreeGroup {
+ id: string;
+ icon: string;
+ children: TreeSubItem[];
+}
+
+export interface SpecialItem {
+ id: string;
+ icon?: string;
iconType?: 'star' | null;
- categories?: string[];
}
-export interface MenuSection {
- id: string;
- items: MenuItem[];
+export interface SettingsTreeConfig {
+ specialItems: SpecialItem[];
+ groups: TreeGroup[];
}
-// Sidebar menu configuration
-export const MENU_SECTIONS: MenuSection[] = [
- {
- id: 'main',
- items: [
- { id: 'favorites', iconType: 'star' },
- { id: 'branding', iconType: null },
- { id: 'theme', iconType: null },
- { id: 'analytics', iconType: null },
- { id: 'buttons', 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',
- 'TELEGRAM_WIDGET',
- 'TELEGRAM_OIDC',
- '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'],
- },
- ],
- },
-];
+// Hierarchical settings tree — all 61 backend category keys mapped into 7 groups
+export const SETTINGS_TREE: SettingsTreeConfig = {
+ specialItems: [
+ { id: 'favorites', iconType: 'star' },
+ { id: 'branding', icon: '🎨' },
+ { id: 'theme', icon: '🌈' },
+ { id: 'analytics', icon: '📊' },
+ { id: 'buttons', icon: '📱' },
+ ],
+ groups: [
+ {
+ id: 'payments',
+ icon: '💳',
+ children: [
+ { id: 'payments_general', categories: ['PAYMENT', 'PAYMENT_VERIFICATION'] },
+ { id: 'payments_stars', categories: ['TELEGRAM'] },
+ { id: 'payments_yookassa', categories: ['YOOKASSA'] },
+ { id: 'payments_cryptobot', categories: ['CRYPTOBOT'] },
+ { id: 'payments_cloudpayments', categories: ['CLOUDPAYMENTS'] },
+ { id: 'payments_freekassa', categories: ['FREEKASSA'] },
+ { id: 'payments_kassa_ai', categories: ['KASSA_AI'] },
+ { id: 'payments_platega', categories: ['PLATEGA'] },
+ { id: 'payments_pal24', categories: ['PAL24'] },
+ { id: 'payments_heleket', categories: ['HELEKET'] },
+ { id: 'payments_mulenpay', categories: ['MULENPAY'] },
+ { id: 'payments_tribute', categories: ['TRIBUTE'] },
+ { id: 'payments_wata', categories: ['WATA'] },
+ { id: 'payments_riopay', categories: ['RIOPAY'] },
+ { id: 'payments_severpay', categories: ['SEVERPAY'] },
+ ],
+ },
+ {
+ id: 'subscriptions',
+ icon: '📦',
+ children: [
+ { id: 'subs_core', categories: ['SUBSCRIPTIONS_CORE'] },
+ { id: 'subs_trial', categories: ['TRIAL'] },
+ { id: 'subs_pricing', categories: ['SUBSCRIPTION_PRICES'] },
+ { id: 'subs_periods', categories: ['PERIODS'] },
+ { id: 'subs_traffic', categories: ['TRAFFIC', 'TRAFFIC_PACKAGES'] },
+ { id: 'subs_simple', categories: ['SIMPLE_SUBSCRIPTION'] },
+ { id: 'subs_autopay', categories: ['AUTOPAY'] },
+ ],
+ },
+ {
+ id: 'interface',
+ icon: '🖥️',
+ children: [
+ {
+ id: 'iface_general',
+ categories: ['INTERFACE', 'INTERFACE_BRANDING', 'INTERFACE_SUBSCRIPTION'],
+ },
+ { id: 'iface_connect', categories: ['CONNECT_BUTTON'] },
+ { id: 'iface_miniapp', categories: ['MINIAPP'] },
+ { id: 'iface_happ', categories: ['HAPP'] },
+ { id: 'iface_widget', categories: ['TELEGRAM_WIDGET'] },
+ { id: 'iface_oidc', categories: ['TELEGRAM_OIDC'] },
+ { id: 'iface_skip', categories: ['SKIP'] },
+ { id: 'iface_additional', categories: ['ADDITIONAL'] },
+ ],
+ },
+ {
+ id: 'users',
+ icon: '👥',
+ children: [
+ { id: 'users_support', categories: ['SUPPORT'] },
+ { id: 'users_referral', categories: ['REFERRAL'] },
+ { id: 'users_channel', categories: ['CHANNEL'] },
+ { id: 'users_localization', categories: ['LOCALIZATION', 'TIMEZONE'] },
+ { id: 'users_moderation', categories: ['MODERATION', 'BAN_NOTIFICATIONS'] },
+ ],
+ },
+ {
+ id: 'notifications',
+ icon: '🔔',
+ children: [
+ { id: 'notif_user', categories: ['NOTIFICATIONS', 'WEBHOOK_NOTIFICATIONS'] },
+ { id: 'notif_admin', categories: ['ADMIN_NOTIFICATIONS'] },
+ { id: 'notif_reports', categories: ['ADMIN_REPORTS'] },
+ ],
+ },
+ {
+ id: 'database',
+ icon: '🗄️',
+ children: [
+ { id: 'db_general', categories: ['DATABASE'] },
+ { id: 'db_postgres', categories: ['POSTGRES'] },
+ { id: 'db_sqlite', categories: ['SQLITE'] },
+ { id: 'db_redis', categories: ['REDIS'] },
+ ],
+ },
+ {
+ id: 'system',
+ icon: '⚙️',
+ children: [
+ { id: 'sys_core', categories: ['CORE', 'DEBUG'] },
+ { id: 'sys_remnawave', categories: ['REMNAWAVE'] },
+ { id: 'sys_webapi', categories: ['WEB_API', 'EXTERNAL_ADMIN'] },
+ { id: 'sys_webhook', categories: ['WEBHOOK'] },
+ { id: 'sys_server', categories: ['SERVER_STATUS'] },
+ { id: 'sys_monitoring', categories: ['MONITORING'] },
+ { id: 'sys_maintenance', categories: ['MAINTENANCE'] },
+ { id: 'sys_backup', categories: ['BACKUP'] },
+ { id: 'sys_version', categories: ['VERSION'] },
+ { id: 'sys_logging', categories: ['LOG'] },
+ ],
+ },
+ ],
+};
+
+// Helper: find which group and sub-item a backend category key belongs to
+export function findTreeLocation(
+ categoryKey: string,
+): { groupId: string; subItemId: string } | null {
+ for (const group of SETTINGS_TREE.groups) {
+ for (const child of group.children) {
+ if (child.categories.includes(categoryKey)) {
+ return { groupId: group.id, subItemId: child.id };
+ }
+ }
+ }
+ return null;
+}
+
+// Helper: get all backend category keys for a given sub-item id
+export function getCategoriesForSubItem(subItemId: string): string[] {
+ for (const group of SETTINGS_TREE.groups) {
+ const child = group.children.find((c) => c.id === subItemId);
+ if (child) return child.categories;
+ }
+ return [];
+}
// Theme preset type
export interface ThemePreset {
diff --git a/src/components/admin/index.ts b/src/components/admin/index.ts
index a6a7d72..90c30ad 100644
--- a/src/components/admin/index.ts
+++ b/src/components/admin/index.ts
@@ -5,6 +5,7 @@ export * from './icons';
export * from './Toggle';
export * from './SettingInput';
export * from './SettingRow';
+export * from './SettingsTableRow';
export * from './AnalyticsTab';
export * from './BrandingTab';
export * from './ButtonsTab';
@@ -13,6 +14,8 @@ export * from './FavoritesTab';
export * from './SettingsTab';
export * from './SettingsMobileTabs';
export * from './SettingsSearch';
+export * from './SettingsTreeSidebar';
+export * from './QuickToggles';
export * from './LocaleTabs';
export * from './LocalizedInput';
diff --git a/src/locales/en.json b/src/locales/en.json
index 99893f7..f0f2061 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -1836,8 +1836,11 @@
"title": "System Settings",
"allCategories": "All categories",
"noSettings": "No settings found",
+ "quickToggles": "Quick Toggles",
"modified": "Modified",
"readOnly": "Read only",
+ "badgeDb": "DB",
+ "badgeEnv": "ENV",
"reset": "Reset",
"categoriesCount": "categories",
"settingsCount": "settings",
@@ -1910,6 +1913,84 @@
"quickPresets": "Quick presets",
"resetAllColors": "Reset all colors",
"statusColors": "Status colors",
+ "favorites": "Favorites",
+ "branding": "Branding",
+ "theme": "Theme",
+ "payments": "Payments",
+ "subscriptions": "Subscriptions",
+ "interface": "Interface",
+ "notifications": "Notifications",
+ "database": "Database",
+ "system": "System",
+ "users": "Users",
+ "customization": "Customization",
+ "settingsLabel": "Settings",
+ "backToAdmin": "Back to admin",
+ "totalCount": "{{count}} total",
+ "modifiedCount": "{{count}} modified",
+ "groups": {
+ "payments": "Payments",
+ "subscriptions": "Subscriptions",
+ "interface": "Interface",
+ "users": "Users",
+ "notifications": "Notifications",
+ "database": "Database",
+ "system": "System"
+ },
+ "tree": {
+ "payments_general": "General",
+ "payments_stars": "Telegram Stars",
+ "payments_yookassa": "YooKassa",
+ "payments_cryptobot": "CryptoBot",
+ "payments_cloudpayments": "CloudPayments",
+ "payments_freekassa": "FreeKassa",
+ "payments_kassa_ai": "Kassa AI",
+ "payments_platega": "Platega",
+ "payments_pal24": "PAL24",
+ "payments_heleket": "Heleket",
+ "payments_mulenpay": "MulenPay",
+ "payments_tribute": "Tribute",
+ "payments_wata": "Wata",
+ "payments_riopay": "RioPay",
+ "payments_severpay": "SeverPay",
+ "subs_core": "Core",
+ "subs_trial": "Trial",
+ "subs_pricing": "Pricing",
+ "subs_periods": "Periods",
+ "subs_traffic": "Traffic",
+ "subs_simple": "Simple subscription",
+ "subs_autopay": "Auto-pay",
+ "iface_general": "General",
+ "iface_connect": "Connect button",
+ "iface_miniapp": "MiniApp",
+ "iface_happ": "HAPP",
+ "iface_widget": "Login Widget",
+ "iface_oidc": "OIDC",
+ "iface_skip": "Skip steps",
+ "iface_additional": "Additional",
+ "users_support": "Support",
+ "users_referral": "Referral",
+ "users_channel": "Channel",
+ "users_localization": "Localization",
+ "users_moderation": "Moderation",
+ "notif_user": "User notifications",
+ "notif_admin": "Admin notifications",
+ "notif_reports": "Reports",
+ "db_general": "General",
+ "db_postgres": "PostgreSQL",
+ "db_sqlite": "SQLite",
+ "db_redis": "Redis",
+ "sys_core": "Core & Debug",
+ "sys_remnawave": "RemnaWave",
+ "sys_webapi": "Web API",
+ "sys_webhook": "Webhook",
+ "sys_server": "Server status",
+ "sys_monitoring": "Monitoring",
+ "sys_maintenance": "Maintenance",
+ "sys_backup": "Backup",
+ "sys_version": "Version",
+ "sys_logging": "Logging"
+ },
"categories": {
"TELEGRAM_WIDGET": "Telegram Login Widget",
"TELEGRAM_OIDC": "Telegram Login (OIDC)"
diff --git a/src/locales/fa.json b/src/locales/fa.json
index def8574..ad623d6 100644
--- a/src/locales/fa.json
+++ b/src/locales/fa.json
@@ -1497,6 +1497,7 @@
"title": "تنظیمات سیستم",
"allCategories": "همه دستهها",
"noSettings": "تنظیماتی یافت نشد",
+ "quickToggles": "تغییر سریع",
"modified": "تغییر یافته",
"readOnly": "فقط خواندنی",
"reset": "بازنشانی",
@@ -1571,6 +1572,86 @@
"quickPresets": "پیشتنظیمهای سریع",
"resetAllColors": "بازنشانی همه رنگها",
"statusColors": "رنگهای وضعیت",
+ "favorites": "علاقهمندیها",
+ "branding": "برندسازی",
+ "theme": "پوسته",
+ "payments": "پرداختها",
+ "subscriptions": "اشتراکها",
+ "interface": "رابط کاربری",
+ "notifications": "اعلانها",
+ "database": "پایگاه داده",
+ "system": "سیستم",
+ "users": "کاربران",
+ "customization": "سفارشیسازی",
+ "settingsLabel": "تنظیمات",
+ "backToAdmin": "بازگشت به مدیریت",
+ "totalCount": "{{count}} مورد",
+ "modifiedCount": "{{count}} تغییر یافته",
+ "badgeDb": "پایگاه داده",
+ "badgeEnv": "متغیر محیطی",
+ "groups": {
+ "payments": "پرداختها",
+ "subscriptions": "اشتراکها",
+ "interface": "رابط کاربری",
+ "users": "کاربران",
+ "notifications": "اعلانها",
+ "database": "پایگاه داده",
+ "system": "سیستم"
+ },
+ "tree": {
+ "payments_general": "عمومی",
+ "payments_stars": "Telegram Stars",
+ "payments_yookassa": "YooKassa",
+ "payments_cryptobot": "CryptoBot",
+ "payments_cloudpayments": "CloudPayments",
+ "payments_freekassa": "FreeKassa",
+ "payments_kassa_ai": "Kassa AI",
+ "payments_platega": "Platega",
+ "payments_pal24": "PAL24",
+ "payments_heleket": "Heleket",
+ "payments_mulenpay": "MulenPay",
+ "payments_tribute": "Tribute",
+ "payments_wata": "Wata",
+ "payments_riopay": "RioPay",
+ "payments_severpay": "SeverPay",
+ "subs_core": "هسته",
+ "subs_trial": "آزمایشی",
+ "subs_pricing": "قیمتگذاری",
+ "subs_periods": "دورهها",
+ "subs_traffic": "ترافیک",
+ "subs_simple": "اشتراک ساده",
+ "subs_autopay": "پرداخت خودکار",
+ "iface_general": "عمومی",
+ "iface_connect": "دکمه اتصال",
+ "iface_miniapp": "MiniApp",
+ "iface_happ": "HAPP",
+ "iface_widget": "ویجت ورود",
+ "iface_oidc": "OIDC",
+ "iface_skip": "رد کردن مراحل",
+ "iface_additional": "اضافی",
+ "users_support": "پشتیبانی",
+ "users_referral": "معرفی",
+ "users_channel": "کانال",
+ "users_localization": "بومیسازی",
+ "users_moderation": "مدیریت محتوا",
+ "notif_user": "اعلانهای کاربر",
+ "notif_admin": "اعلانهای مدیر",
+ "notif_reports": "گزارشها",
+ "db_general": "عمومی",
+ "db_postgres": "PostgreSQL",
+ "db_sqlite": "SQLite",
+ "db_redis": "Redis",
+ "sys_core": "هسته و اشکالزدایی",
+ "sys_remnawave": "RemnaWave",
+ "sys_webapi": "Web API",
+ "sys_webhook": "Webhook",
+ "sys_server": "وضعیت سرور",
+ "sys_monitoring": "نظارت",
+ "sys_maintenance": "نگهداری",
+ "sys_backup": "پشتیبانگیری",
+ "sys_version": "نسخه",
+ "sys_logging": "لاگها"
+ },
"categories": {
"TELEGRAM_WIDGET": "Telegram Login Widget",
"TELEGRAM_OIDC": "Telegram Login (OIDC)"
diff --git a/src/locales/ru.json b/src/locales/ru.json
index adfef96..5046425 100644
--- a/src/locales/ru.json
+++ b/src/locales/ru.json
@@ -1861,8 +1861,11 @@
"title": "Настройки системы",
"allCategories": "Все категории",
"noSettings": "Настройки не найдены",
+ "quickToggles": "Быстрые переключатели",
"modified": "Изменено",
"readOnly": "Только чтение",
+ "badgeDb": "БД",
+ "badgeEnv": "ENV",
"reset": "Сбросить",
"categoriesCount": "категорий",
"settingsCount": "настроек",
@@ -1936,6 +1939,74 @@
"googleLabelPlaceholder": "Например: AbCdEfGhIjKl",
"googleLabelHint": "Метка конверсии для отслеживания событий",
"analyticsHint": "Счётчики автоматически встраиваются на все страницы кабинета. После сохранения ID скрипты подключаются при следующей загрузке страницы. Для удаления счётчика очистите поле и сохраните.",
+ "customization": "Кастомизация",
+ "settingsLabel": "Настройки",
+ "backToAdmin": "Назад к админке",
+ "totalCount": "{{count}} всего",
+ "modifiedCount": "{{count}} изменено",
+ "groups": {
+ "payments": "Платежи",
+ "subscriptions": "Подписки",
+ "interface": "Интерфейс",
+ "users": "Пользователи",
+ "notifications": "Уведомления",
+ "database": "База данных",
+ "system": "Система"
+ },
+ "tree": {
+ "payments_general": "Основные",
+ "payments_stars": "Telegram Stars",
+ "payments_yookassa": "YooKassa",
+ "payments_cryptobot": "CryptoBot",
+ "payments_cloudpayments": "CloudPayments",
+ "payments_freekassa": "FreeKassa",
+ "payments_kassa_ai": "Kassa AI",
+ "payments_platega": "Platega",
+ "payments_pal24": "PAL24",
+ "payments_heleket": "Heleket",
+ "payments_mulenpay": "MulenPay",
+ "payments_tribute": "Tribute",
+ "payments_wata": "Wata",
+ "payments_riopay": "RioPay",
+ "payments_severpay": "SeverPay",
+ "subs_core": "Основные",
+ "subs_trial": "Пробный период",
+ "subs_pricing": "Цены",
+ "subs_periods": "Периоды",
+ "subs_traffic": "Трафик",
+ "subs_simple": "Простая подписка",
+ "subs_autopay": "Автоплатёж",
+ "iface_general": "Основные",
+ "iface_connect": "Кнопка подключения",
+ "iface_miniapp": "MiniApp",
+ "iface_happ": "HAPP",
+ "iface_widget": "Виджет входа",
+ "iface_oidc": "OIDC",
+ "iface_skip": "Пропуск шагов",
+ "iface_additional": "Дополнительно",
+ "users_support": "Поддержка",
+ "users_referral": "Реферальная система",
+ "users_channel": "Канал",
+ "users_localization": "Локализация",
+ "users_moderation": "Модерация",
+ "notif_user": "Уведомления пользователей",
+ "notif_admin": "Уведомления админов",
+ "notif_reports": "Отчёты",
+ "db_general": "Основные",
+ "db_postgres": "PostgreSQL",
+ "db_sqlite": "SQLite",
+ "db_redis": "Redis",
+ "sys_core": "Ядро и отладка",
+ "sys_remnawave": "RemnaWave",
+ "sys_webapi": "Web API",
+ "sys_webhook": "Webhook",
+ "sys_server": "Статус сервера",
+ "sys_monitoring": "Мониторинг",
+ "sys_maintenance": "Обслуживание",
+ "sys_backup": "Бэкап",
+ "sys_version": "Версия",
+ "sys_logging": "Логи"
+ },
"presets": {
"standard": "Стандарт",
"ocean": "Океан",
diff --git a/src/locales/zh.json b/src/locales/zh.json
index 2b0e3fd..478f57b 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -1535,6 +1535,7 @@
"title": "系统设置",
"allCategories": "所有分类",
"noSettings": "未找到设置",
+ "quickToggles": "快速开关",
"modified": "已修改",
"readOnly": "只读",
"reset": "重置",
@@ -1609,6 +1610,86 @@
"quickPresets": "快速预设",
"resetAllColors": "重置所有颜色",
"statusColors": "状态颜色",
+ "favorites": "收藏",
+ "branding": "品牌",
+ "theme": "主题",
+ "payments": "支付",
+ "subscriptions": "订阅",
+ "interface": "界面",
+ "notifications": "通知",
+ "database": "数据库",
+ "system": "系统",
+ "users": "用户",
+ "customization": "自定义",
+ "settingsLabel": "设置",
+ "backToAdmin": "返回管理面板",
+ "totalCount": "共 {{count}} 项",
+ "modifiedCount": "已修改 {{count}} 项",
+ "badgeDb": "数据库",
+ "badgeEnv": "环境变量",
+ "groups": {
+ "payments": "支付",
+ "subscriptions": "订阅",
+ "interface": "界面",
+ "users": "用户",
+ "notifications": "通知",
+ "database": "数据库",
+ "system": "系统"
+ },
+ "tree": {
+ "payments_general": "通用",
+ "payments_stars": "Telegram Stars",
+ "payments_yookassa": "YooKassa",
+ "payments_cryptobot": "CryptoBot",
+ "payments_cloudpayments": "CloudPayments",
+ "payments_freekassa": "FreeKassa",
+ "payments_kassa_ai": "Kassa AI",
+ "payments_platega": "Platega",
+ "payments_pal24": "PAL24",
+ "payments_heleket": "Heleket",
+ "payments_mulenpay": "MulenPay",
+ "payments_tribute": "Tribute",
+ "payments_wata": "Wata",
+ "payments_riopay": "RioPay",
+ "payments_severpay": "SeverPay",
+ "subs_core": "核心",
+ "subs_trial": "试用",
+ "subs_pricing": "定价",
+ "subs_periods": "周期",
+ "subs_traffic": "流量",
+ "subs_simple": "简单订阅",
+ "subs_autopay": "自动支付",
+ "iface_general": "通用",
+ "iface_connect": "连接按钮",
+ "iface_miniapp": "MiniApp",
+ "iface_happ": "HAPP",
+ "iface_widget": "登录小部件",
+ "iface_oidc": "OIDC",
+ "iface_skip": "跳过步骤",
+ "iface_additional": "附加",
+ "users_support": "支持",
+ "users_referral": "推荐",
+ "users_channel": "频道",
+ "users_localization": "本地化",
+ "users_moderation": "审核",
+ "notif_user": "用户通知",
+ "notif_admin": "管理员通知",
+ "notif_reports": "报告",
+ "db_general": "通用",
+ "db_postgres": "PostgreSQL",
+ "db_sqlite": "SQLite",
+ "db_redis": "Redis",
+ "sys_core": "核心与调试",
+ "sys_remnawave": "RemnaWave",
+ "sys_webapi": "Web API",
+ "sys_webhook": "Webhook",
+ "sys_server": "服务器状态",
+ "sys_monitoring": "监控",
+ "sys_maintenance": "维护",
+ "sys_backup": "备份",
+ "sys_version": "版本",
+ "sys_logging": "日志"
+ },
"categories": {
"TELEGRAM_WIDGET": "Telegram Login Widget",
"TELEGRAM_OIDC": "Telegram Login (OIDC)"
diff --git a/src/pages/AdminSettings.tsx b/src/pages/AdminSettings.tsx
index 7525a96..955b9f4 100644
--- a/src/pages/AdminSettings.tsx
+++ b/src/pages/AdminSettings.tsx
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
import { adminSettingsApi, SettingDefinition } from '../api/adminSettings';
import { themeColorsApi } from '../api/themeColors';
import { useFavoriteSettings } from '../hooks/useFavoriteSettings';
-import { MENU_SECTIONS, MenuItem, formatSettingKey } from '../components/admin';
+import { SETTINGS_TREE, findTreeLocation, formatSettingKey } from '../components/admin';
import { usePlatform } from '../platform/hooks/usePlatform';
import { AnalyticsTab } from '../components/admin/AnalyticsTab';
import { BrandingTab } from '../components/admin/BrandingTab';
@@ -13,12 +13,9 @@ import { MenuEditorTab } from '../components/admin/MenuEditorTab';
import { ThemeTab } from '../components/admin/ThemeTab';
import { FavoritesTab } from '../components/admin/FavoritesTab';
import { SettingsTab } from '../components/admin/SettingsTab';
+import { SettingsTreeSidebar } from '../components/admin/SettingsTreeSidebar';
import { SettingsMobileTabs } from '../components/admin/SettingsMobileTabs';
-import {
- SettingsSearch,
- SettingsSearchMobile,
- SettingsSearchResults,
-} from '../components/admin/SettingsSearch';
+import { SettingsSearchMobile, SettingsSearchResults } from '../components/admin/SettingsSearch';
// BackIcon
const BackIcon = () => (
@@ -33,17 +30,18 @@ const BackIcon = () => (
);
-// Find section ID by category key
-function findSectionByCategory(categoryKey: string): string | null {
- for (const section of MENU_SECTIONS) {
- for (const item of section.items) {
- if (item.categories?.includes(categoryKey)) {
- return item.id;
- }
- }
- }
- return null;
-}
+// ChevronRight for breadcrumbs
+const ChevronRightIcon = () => (
+
+
+
+);
export default function AdminSettings() {
const { t } = useTranslation();
@@ -73,23 +71,29 @@ export default function AdminSettings() {
queryFn: () => adminSettingsApi.getSettings(),
});
- // Get current menu item configuration
- const currentMenuItem = useMemo(() => {
- for (const section of MENU_SECTIONS) {
- const item = section.items.find((i: MenuItem) => i.id === activeSection);
- if (item) return item;
+ // Find active tree info (group + child for tree sub-items)
+ // No useMemo needed — SETTINGS_TREE is a static constant, iteration is trivial
+ let activeTreeInfo: {
+ group: (typeof SETTINGS_TREE.groups)[number];
+ child: (typeof SETTINGS_TREE.groups)[number]['children'][number];
+ } | null = null;
+ for (const group of SETTINGS_TREE.groups) {
+ const child = group.children.find((c) => c.id === activeSection);
+ if (child) {
+ activeTreeInfo = { group, child };
+ break;
}
- return null;
- }, [activeSection]);
+ }
- // Get categories for current section
+ // Get categories for current sub-item
const currentCategories = useMemo(() => {
- if (!currentMenuItem?.categories || !allSettings || !Array.isArray(allSettings)) return [];
+ if (!activeTreeInfo || !allSettings || !Array.isArray(allSettings)) return [];
+ const categoryKeys = activeTreeInfo.child.categories;
const categoryMap = new Map();
for (const setting of allSettings) {
- if (currentMenuItem.categories.includes(setting.category.key)) {
+ if (categoryKeys.includes(setting.category.key)) {
if (!categoryMap.has(setting.category.key)) {
categoryMap.set(setting.category.key, []);
}
@@ -102,7 +106,7 @@ export default function AdminSettings() {
label: t(`admin.settings.categories.${key}`, key),
settings,
}));
- }, [currentMenuItem, allSettings, t]);
+ }, [activeTreeInfo, allSettings, t]);
// Filter settings for search - GLOBAL search across all settings
const filteredSettings = useMemo(() => {
@@ -112,24 +116,14 @@ export default function AdminSettings() {
if (!q) return [];
return allSettings.filter((s: SettingDefinition) => {
- // Search by key
if (s.key.toLowerCase().includes(q)) return true;
-
- // Search by original name
if (s.name?.toLowerCase().includes(q)) return true;
-
- // Search by translated name
const formattedKey = formatSettingKey(s.name || s.key);
const translatedName = t(`admin.settings.settingNames.${formattedKey}`, formattedKey);
if (translatedName.toLowerCase().includes(q)) return true;
-
- // Search by description
if (s.hint?.description?.toLowerCase().includes(q)) return true;
-
- // Search by category
const categoryLabel = t(`admin.settings.categories.${s.category.key}`, s.category.key);
if (categoryLabel.toLowerCase().includes(q)) return true;
-
return false;
});
}, [allSettings, searchQuery, t]);
@@ -140,19 +134,42 @@ export default function AdminSettings() {
return allSettings.filter((s: SettingDefinition) => favorites.includes(s.key));
}, [allSettings, favorites]);
+ // Count of modified settings
+ const modifiedCount = useMemo(() => {
+ if (!allSettings || !Array.isArray(allSettings)) return 0;
+ return allSettings.filter((s: SettingDefinition) => s.has_override).length;
+ }, [allSettings]);
+
+ // Total settings count
+ const totalCount = useMemo(() => {
+ if (!allSettings || !Array.isArray(allSettings)) return 0;
+ return allSettings.length;
+ }, [allSettings]);
+
// Handle setting selection from autocomplete
const handleSelectSetting = useCallback(
(setting: SettingDefinition) => {
- const sectionId = findSectionByCategory(setting.category.key);
- if (sectionId) {
- setActiveSection(sectionId);
+ const location = findTreeLocation(setting.category.key);
+ if (location) {
+ setActiveSection(location.subItemId);
}
- // Set search to setting key to filter to just this setting
setSearchQuery(setting.key);
},
[setActiveSection, setSearchQuery],
);
+ // Get the display title for the current section
+ const sectionTitle = useMemo(() => {
+ // Special items
+ const specialItem = SETTINGS_TREE.specialItems.find((item) => item.id === activeSection);
+ if (specialItem) return t(`admin.settings.${specialItem.id}`);
+
+ // Tree sub-items
+ if (activeTreeInfo) return t(`admin.settings.tree.${activeTreeInfo.child.id}`);
+
+ return t('admin.settings.title');
+ }, [activeSection, activeTreeInfo, t]);
+
// Render content based on active section
const renderContent = () => {
// If searching, always show search results regardless of active section
@@ -186,17 +203,7 @@ export default function AdminSettings() {
/>
);
default:
- if (
- [
- 'payments',
- 'subscriptions',
- 'interface',
- 'notifications',
- 'database',
- 'system',
- 'users',
- ].includes(activeSection)
- ) {
+ if (activeTreeInfo) {
return (
{/* Fixed Sidebar */}
-
+
{/* Show back button only on web, not in Telegram Mini App */}
@@ -241,6 +248,7 @@ export default function AdminSettings() {
navigate('/admin')}
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
+ aria-label={t('admin.settings.backToAdmin')}
>
@@ -248,67 +256,52 @@ export default function AdminSettings() {
{t('admin.settings.title')}
-
- {MENU_SECTIONS.map((section, sectionIdx) => (
-
- {sectionIdx > 0 &&
}
- {section.items.map((item) => {
- const isActive = activeSection === item.id;
- const hasIcon = item.iconType === 'star';
- return (
-
setActiveSection(item.id)}
- className={`flex w-full items-center gap-3 rounded-xl px-3 py-2.5 transition-all ${
- isActive
- ? 'bg-accent-500/10 text-accent-400'
- : 'text-dark-400 hover:bg-dark-800/50 hover:text-dark-200'
- }`}
- >
- {hasIcon && (
-
-
-
- )}
- {t(`admin.settings.${item.id}`)}
- {item.id === 'favorites' && favorites.length > 0 && (
-
- {favorites.length}
-
- )}
-
- );
- })}
-
- ))}
-
+
{/* Scrollable Content */}
+ {/* Breadcrumb for tree sub-items */}
+ {activeTreeInfo && !searchQuery.trim() && (
+
+ setActiveSection(activeTreeInfo.group.children[0].id)}
+ className="text-dark-500 transition-colors hover:text-dark-300"
+ >
+ {t(`admin.settings.groups.${activeTreeInfo.group.id}`)}
+
+
+
+ {t(`admin.settings.tree.${activeTreeInfo.child.id}`)}
+
+
+ )}
+
+ {/* Title + count badges */}
-
- {t(`admin.settings.${activeSection}`)}
-
-
-
+
{sectionTitle}
+ {totalCount > 0 && !searchQuery.trim() && activeTreeInfo && (
+
+
+ {t('admin.settings.totalCount', { count: totalCount })}
+
+ {modifiedCount > 0 && (
+
+ {t('admin.settings.modifiedCount', { count: modifiedCount })}
+
+ )}
+
+ )}
+
{renderContent()}