mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
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:
@@ -925,7 +925,74 @@
|
|||||||
"Channel Id": "ID канала",
|
"Channel Id": "ID канала",
|
||||||
"Channel Url": "URL канала",
|
"Channel Url": "URL канала",
|
||||||
"Timezone": "Часовой пояс",
|
"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": {
|
"categories": {
|
||||||
"PAYMENT": "Платежи",
|
"PAYMENT": "Платежи",
|
||||||
|
|||||||
@@ -38,12 +38,6 @@ const ChevronDownIcon = () => (
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const ChevronRightIcon = () => (
|
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
const UploadIcon = () => (
|
const UploadIcon = () => (
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
||||||
@@ -196,7 +190,6 @@ export default function AdminSettings() {
|
|||||||
|
|
||||||
// State
|
// State
|
||||||
const [activeSection, setActiveSection] = useState('branding')
|
const [activeSection, setActiveSection] = useState('branding')
|
||||||
const [activeSubCategory, setActiveSubCategory] = useState<string | null>(null)
|
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
const [editingName, setEditingName] = useState(false)
|
const [editingName, setEditingName] = useState(false)
|
||||||
const [newName, setNewName] = useState('')
|
const [newName, setNewName] = useState('')
|
||||||
@@ -368,30 +361,20 @@ export default function AdminSettings() {
|
|||||||
}))
|
}))
|
||||||
}, [currentMenuItem, allSettings, t])
|
}, [currentMenuItem, allSettings, t])
|
||||||
|
|
||||||
// Filter settings for current view
|
// Filter settings for search
|
||||||
const filteredSettings = useMemo(() => {
|
const filteredSettings = useMemo(() => {
|
||||||
if (!allSettings || !Array.isArray(allSettings)) return []
|
if (!allSettings || !Array.isArray(allSettings) || !searchQuery) return []
|
||||||
|
|
||||||
let settings: SettingDefinition[] = []
|
const q = searchQuery.toLowerCase()
|
||||||
|
let settings = allSettings.filter((s: SettingDefinition) =>
|
||||||
|
currentMenuItem?.categories?.includes(s.category.key)
|
||||||
|
)
|
||||||
|
|
||||||
if (activeSubCategory) {
|
return settings.filter((s: SettingDefinition) =>
|
||||||
settings = allSettings.filter((s: SettingDefinition) => s.category.key === activeSubCategory)
|
s.key.toLowerCase().includes(q) ||
|
||||||
} else if (currentMenuItem?.categories) {
|
s.name?.toLowerCase().includes(q)
|
||||||
settings = allSettings.filter((s: SettingDefinition) =>
|
)
|
||||||
currentMenuItem.categories!.includes(s.category.key)
|
}, [allSettings, currentMenuItem, searchQuery])
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchQuery) {
|
|
||||||
const q = searchQuery.toLowerCase()
|
|
||||||
settings = settings.filter((s: SettingDefinition) =>
|
|
||||||
s.key.toLowerCase().includes(q) ||
|
|
||||||
s.name?.toLowerCase().includes(q)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings
|
|
||||||
}, [allSettings, activeSection, activeSubCategory, currentMenuItem, searchQuery])
|
|
||||||
|
|
||||||
// Favorite settings
|
// Favorite settings
|
||||||
const favoriteSettings = useMemo(() => {
|
const favoriteSettings = useMemo(() => {
|
||||||
@@ -856,50 +839,70 @@ export default function AdminSettings() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
const renderSettingsContent = () => (
|
const renderSettingsContent = () => {
|
||||||
<div className="space-y-4">
|
// If searching, show flat list
|
||||||
{/* Sub-categories navigation */}
|
if (searchQuery) {
|
||||||
{currentCategories.length > 1 && !activeSubCategory && (
|
return (
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 mb-6">
|
<div className="space-y-4">
|
||||||
{currentCategories.map((cat) => (
|
{filteredSettings.length === 0 ? (
|
||||||
<button
|
<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 className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||||
|
{filteredSettings.map(renderSettingRow)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</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}
|
key={cat.key}
|
||||||
onClick={() => setActiveSubCategory(cat.key)}
|
className="rounded-2xl bg-dark-800/30 border border-dark-700/30 overflow-hidden"
|
||||||
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">
|
{/* Accordion header */}
|
||||||
<span className="font-medium text-dark-200 truncate">{cat.label}</span>
|
<button
|
||||||
<ChevronRightIcon />
|
onClick={() => toggleSection(cat.key)}
|
||||||
</div>
|
className="w-full flex items-center justify-between p-4 hover:bg-dark-800/50 transition-colors"
|
||||||
<span className="text-sm text-dark-500">{cat.settings.length} {t('admin.settings.settingsCount')}</span>
|
>
|
||||||
</button>
|
<div className="flex items-center gap-3">
|
||||||
))}
|
<span className="font-medium text-dark-100">{cat.label}</span>
|
||||||
</div>
|
<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>
|
||||||
|
|
||||||
{/* Back button if in sub-category */}
|
{/* Accordion content */}
|
||||||
{activeSubCategory && (
|
{isExpanded && (
|
||||||
<button
|
<div className="p-4 pt-0 border-t border-dark-700/30">
|
||||||
onClick={() => setActiveSubCategory(null)}
|
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 pt-4">
|
||||||
className="flex items-center gap-2 text-dark-400 hover:text-dark-200 transition-colors mb-4"
|
{cat.settings.map(renderSettingRow)}
|
||||||
>
|
</div>
|
||||||
<BackIcon />
|
</div>
|
||||||
<span>{t('common.back')}</span>
|
)}
|
||||||
</button>
|
</div>
|
||||||
)}
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
{/* Settings grid/list */}
|
{currentCategories.length === 0 && (
|
||||||
{filteredSettings.length === 0 ? (
|
<div className="p-12 rounded-2xl bg-dark-800/30 border border-dark-700/30 text-center">
|
||||||
<div className="p-12 rounded-2xl bg-dark-800/30 border border-dark-700/30 text-center">
|
<p className="text-dark-400">{t('admin.settings.noSettings')}</p>
|
||||||
<p className="text-dark-400">{t('admin.settings.noSettings')}</p>
|
</div>
|
||||||
</div>
|
)}
|
||||||
) : (
|
</div>
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4">
|
)
|
||||||
{filteredSettings.map(renderSettingRow)}
|
}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
// ============ RENDER ============
|
// ============ RENDER ============
|
||||||
return (
|
return (
|
||||||
@@ -949,7 +952,6 @@ export default function AdminSettings() {
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveSection(item.id)
|
setActiveSection(item.id)
|
||||||
setActiveSubCategory(null)
|
|
||||||
setMobileMenuOpen(false)
|
setMobileMenuOpen(false)
|
||||||
}}
|
}}
|
||||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all ${
|
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">
|
<h2 className="text-lg sm:text-xl font-semibold text-dark-100 truncate">
|
||||||
{t(`admin.settings.${activeSection}`)}
|
{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>
|
</h2>
|
||||||
|
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
|
|||||||
Reference in New Issue
Block a user