Add subscription translations and collapsible categories in AdminSettings

- Add 65+ new translations for subscription, trial, traffic, autopay settings
- Replace flat navigation with accordion-style collapsible subcategories
- Categories are collapsed by default, click to expand
- Search shows flat list when query is present
- Remove unused ChevronRightIcon and activeSubCategory state
This commit is contained in:
PEDZEO
2026-01-21 03:48:44 +03:00
parent c572fc0b69
commit dcac738aeb
2 changed files with 139 additions and 73 deletions

View File

@@ -925,7 +925,74 @@
"Channel Id": "ID канала",
"Channel Url": "URL канала",
"Timezone": "Часовой пояс",
"Locale Default": "Язык"
"Locale Default": "Язык",
"Simple Subscription Device Limit": "Лимит устройств",
"Simple Subscription Enabled": "Быстрая покупка",
"Simple Subscription Period Days": "Период (дни)",
"Simple Subscription Squad Uuid": "UUID сквада",
"Simple Subscription Traffic Gb": "Трафик (ГБ)",
"Trial Activation Price": "Цена активации",
"Trial Add Remaining Days To Paid": "Добавить дни к платной",
"Trial Device Limit": "Лимит устройств",
"Trial Duration Days": "Длительность (дни)",
"Trial Payment Enabled": "Платный триал",
"Trial Tariff Id": "ID тарифа",
"Trial Traffic Limit Gb": "Лимит трафика (ГБ)",
"Trial User Tag": "Тег пользователя",
"Trial Warning Hours": "Предупреждение (час)",
"Paid Subscription User Tag": "Тег платной подписки",
"Price 14 Days": "Цена 14 дней",
"Price 180 Days": "Цена 180 дней",
"Price 30 Days": "Цена 30 дней",
"Price 360 Days": "Цена 360 дней",
"Price 60 Days": "Цена 60 дней",
"Price 90 Days": "Цена 90 дней",
"Base Promo Group Period Discounts": "Скидки по периодам",
"Base Promo Group Period Discounts Enabled": "Скидки по периодам",
"Base Subscription Price": "Базовая цена",
"Default Device Limit": "Лимит устройств",
"Default Traffic Limit Gb": "Лимит трафика (ГБ)",
"Devices Selection Disabled Amount": "Устройств без выбора",
"Devices Selection Enabled": "Выбор устройств",
"Max Devices Limit": "Макс. устройств",
"Price Per Device": "Цена за устройство",
"Sales Mode": "Режим продаж",
"Available Renewal Periods": "Периоды продления",
"Available Subscription Periods": "Периоды подписки",
"Default Traffic Reset Strategy": "Стратегия сброса",
"Fixed Traffic Limit Gb": "Фикс. лимит (ГБ)",
"Reset Traffic On Payment": "Сброс при оплате",
"Traffic Check Batch Size": "Размер пакета",
"Traffic Check Concurrency": "Параллелизм",
"Traffic Daily Check Enabled": "Ежедневная проверка",
"Traffic Daily Check Time": "Время проверки",
"Traffic Daily Threshold Gb": "Порог в день (ГБ)",
"Traffic Excluded User Uuids": "Исключённые UUID",
"Traffic Fast Check Enabled": "Быстрая проверка",
"Traffic Fast Check Interval Minutes": "Интервал (мин)",
"Traffic Fast Check Threshold Gb": "Порог (ГБ)",
"Traffic Ignored Nodes": "Игнор. ноды",
"Traffic Notification Cooldown Minutes": "Кулдаун уведомлений",
"Traffic Reset Base Price": "Цена сброса",
"Traffic Reset Price Mode": "Режим цены сброса",
"Traffic Selection Mode": "Режим выбора",
"Traffic Threshold Gb Per Day": "Порог в день (ГБ)",
"Traffic Topup Enabled": "Докупка трафика",
"Traffic Topup Packages Config": "Конфиг докупки",
"Price Traffic 1000gb": "Цена 1000 ГБ",
"Price Traffic 100gb": "Цена 100 ГБ",
"Price Traffic 10gb": "Цена 10 ГБ",
"Price Traffic 250gb": "Цена 250 ГБ",
"Price Traffic 25gb": "Цена 25 ГБ",
"Price Traffic 500gb": "Цена 500 ГБ",
"Price Traffic 50gb": "Цена 50 ГБ",
"Price Traffic 5gb": "Цена 5 ГБ",
"Price Traffic Unlimited": "Цена безлимита",
"Traffic Packages Config": "Конфиг пакетов",
"Autopay Warning Days": "Предупреждение (дни)",
"Default Autopay Days Before": "Дней до автоплатежа",
"Default Autopay Enabled": "Автоплатёж по умолчанию",
"Min Balance For Autopay Kopeks": "Мин. баланс (коп.)"
},
"categories": {
"PAYMENT": "Платежи",

View File

@@ -38,12 +38,6 @@ const ChevronDownIcon = () => (
</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 = () => (
<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" />
@@ -196,7 +190,6 @@ export default function AdminSettings() {
// State
const [activeSection, setActiveSection] = useState('branding')
const [activeSubCategory, setActiveSubCategory] = useState<string | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const [editingName, setEditingName] = useState(false)
const [newName, setNewName] = useState('')
@@ -368,30 +361,20 @@ export default function AdminSettings() {
}))
}, [currentMenuItem, allSettings, t])
// Filter settings for current view
// Filter settings for search
const filteredSettings = useMemo(() => {
if (!allSettings || !Array.isArray(allSettings)) return []
if (!allSettings || !Array.isArray(allSettings) || !searchQuery) return []
let settings: SettingDefinition[] = []
if (activeSubCategory) {
settings = allSettings.filter((s: SettingDefinition) => s.category.key === activeSubCategory)
} else if (currentMenuItem?.categories) {
settings = allSettings.filter((s: SettingDefinition) =>
currentMenuItem.categories!.includes(s.category.key)
)
}
if (searchQuery) {
const q = searchQuery.toLowerCase()
settings = settings.filter((s: SettingDefinition) =>
let settings = allSettings.filter((s: SettingDefinition) =>
currentMenuItem?.categories?.includes(s.category.key)
)
return settings.filter((s: SettingDefinition) =>
s.key.toLowerCase().includes(q) ||
s.name?.toLowerCase().includes(q)
)
}
return settings
}, [allSettings, activeSection, activeSubCategory, currentMenuItem, searchQuery])
}, [allSettings, currentMenuItem, searchQuery])
// Favorite settings
const favoriteSettings = useMemo(() => {
@@ -856,39 +839,11 @@ export default function AdminSettings() {
</div>
)
const renderSettingsContent = () => (
const renderSettingsContent = () => {
// If searching, show flat list
if (searchQuery) {
return (
<div className="space-y-4">
{/* Sub-categories navigation */}
{currentCategories.length > 1 && !activeSubCategory && (
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 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 hover:bg-dark-800/70 transition-all text-left"
>
<div className="flex items-center justify-between">
<span className="font-medium text-dark-200 truncate">{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 grid/list */}
{filteredSettings.length === 0 ? (
<div className="p-12 rounded-2xl bg-dark-800/30 border border-dark-700/30 text-center">
<p className="text-dark-400">{t('admin.settings.noSettings')}</p>
@@ -900,6 +855,54 @@ export default function AdminSettings() {
)}
</div>
)
}
// Show accordion for subcategories
return (
<div className="space-y-3">
{currentCategories.map((cat) => {
const isExpanded = expandedSections.has(cat.key)
return (
<div
key={cat.key}
className="rounded-2xl bg-dark-800/30 border border-dark-700/30 overflow-hidden"
>
{/* Accordion header */}
<button
onClick={() => toggleSection(cat.key)}
className="w-full flex items-center justify-between p-4 hover:bg-dark-800/50 transition-colors"
>
<div className="flex items-center gap-3">
<span className="font-medium text-dark-100">{cat.label}</span>
<span className="px-2 py-0.5 text-xs rounded-full bg-dark-700 text-dark-400">
{cat.settings.length}
</span>
</div>
<div className={`transition-transform duration-200 text-dark-400 ${isExpanded ? 'rotate-180' : ''}`}>
<ChevronDownIcon />
</div>
</button>
{/* Accordion content */}
{isExpanded && (
<div className="p-4 pt-0 border-t border-dark-700/30">
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 pt-4">
{cat.settings.map(renderSettingRow)}
</div>
</div>
)}
</div>
)
})}
{currentCategories.length === 0 && (
<div className="p-12 rounded-2xl bg-dark-800/30 border border-dark-700/30 text-center">
<p className="text-dark-400">{t('admin.settings.noSettings')}</p>
</div>
)}
</div>
)
}
// ============ RENDER ============
return (
@@ -949,7 +952,6 @@ export default function AdminSettings() {
key={item.id}
onClick={() => {
setActiveSection(item.id)
setActiveSubCategory(null)
setMobileMenuOpen(false)
}}
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all ${
@@ -988,9 +990,6 @@ export default function AdminSettings() {
<h2 className="text-lg sm:text-xl font-semibold text-dark-100 truncate">
{t(`admin.settings.${activeSection}`)}
{activeSubCategory && (
<span className="text-dark-400 font-normal text-sm sm:text-base"> / {t(`admin.settings.categories.${activeSubCategory}`, activeSubCategory)}</span>
)}
</h2>
<div className="flex-1" />