From 1a0a5ff45313da383ed402b09a630e2774d2ae04 Mon Sep 17 00:00:00 2001 From: Fringg Date: Thu, 12 Feb 2026 23:42:58 +0300 Subject: [PATCH] feat: add per-button enable/disable toggle and custom labels per locale - Add Toggle per section card to show/hide button in main menu - Add collapsible locale label inputs (ru, en, ua, zh, fa) per section - Normalize server response for backward compatibility with older API - Update all 4 locale files with new translation keys --- src/api/buttonStyles.ts | 37 ++++-- src/components/admin/ButtonsTab.tsx | 184 +++++++++++++++++++++++----- src/locales/en.json | 4 + src/locales/fa.json | 4 + src/locales/ru.json | 4 + src/locales/zh.json | 4 + 6 files changed, 197 insertions(+), 40 deletions(-) diff --git a/src/api/buttonStyles.ts b/src/api/buttonStyles.ts index c859433..1a67c17 100644 --- a/src/api/buttonStyles.ts +++ b/src/api/buttonStyles.ts @@ -3,6 +3,8 @@ import apiClient from './client'; export interface ButtonSectionConfig { style: 'primary' | 'success' | 'danger' | 'default'; icon_custom_emoji_id: string; + enabled: boolean; + labels: Record; } export interface ButtonStylesConfig { @@ -31,21 +33,40 @@ export const BUTTON_SECTIONS = [ export type ButtonSection = (typeof BUTTON_SECTIONS)[number]; +export const BOT_LOCALES = ['ru', 'en', 'ua', 'zh', 'fa'] as const; + +export type BotLocale = (typeof BOT_LOCALES)[number]; + +const DEFAULT_SECTION: ButtonSectionConfig = { + style: 'primary', + icon_custom_emoji_id: '', + enabled: true, + labels: {}, +}; + export const DEFAULT_BUTTON_STYLES: ButtonStylesConfig = { - home: { style: 'primary', icon_custom_emoji_id: '' }, - subscription: { style: 'success', icon_custom_emoji_id: '' }, - balance: { style: 'primary', icon_custom_emoji_id: '' }, - referral: { style: 'success', icon_custom_emoji_id: '' }, - support: { style: 'primary', icon_custom_emoji_id: '' }, - info: { style: 'primary', icon_custom_emoji_id: '' }, - admin: { style: 'danger', icon_custom_emoji_id: '' }, + home: { ...DEFAULT_SECTION, style: 'primary' }, + subscription: { ...DEFAULT_SECTION, style: 'success' }, + balance: { ...DEFAULT_SECTION, style: 'primary' }, + referral: { ...DEFAULT_SECTION, style: 'success' }, + support: { ...DEFAULT_SECTION, style: 'primary' }, + info: { ...DEFAULT_SECTION, style: 'primary' }, + admin: { ...DEFAULT_SECTION, style: 'danger' }, }; export const buttonStylesApi = { getStyles: async (): Promise => { try { const response = await apiClient.get('/cabinet/admin/button-styles'); - return response.data; + const data = response.data; + for (const section of BUTTON_SECTIONS) { + data[section] = { + ...DEFAULT_SECTION, + ...data[section], + labels: { ...(data[section]?.labels || {}) }, + }; + } + return data; } catch { return DEFAULT_BUTTON_STYLES; } diff --git a/src/components/admin/ButtonsTab.tsx b/src/components/admin/ButtonsTab.tsx index 257f33d..fd419ed 100644 --- a/src/components/admin/ButtonsTab.tsx +++ b/src/components/admin/ButtonsTab.tsx @@ -7,7 +7,9 @@ import { DEFAULT_BUTTON_STYLES, BUTTON_SECTIONS, ButtonSection, + BOT_LOCALES, } from '../../api/buttonStyles'; +import { Toggle } from './Toggle'; type StyleValue = 'primary' | 'success' | 'danger' | 'default'; @@ -18,10 +20,19 @@ const STYLE_OPTIONS: { value: StyleValue; colorClass: string }[] = [ { value: 'danger', colorClass: 'bg-red-500' }, ]; +function labelsEqual(a: Record, b: Record): boolean { + for (const locale of BOT_LOCALES) { + if ((a[locale] || '') !== (b[locale] || '')) return false; + } + return true; +} + function stylesEqual(a: ButtonStylesConfig, b: ButtonStylesConfig): boolean { for (const section of BUTTON_SECTIONS) { if (a[section].style !== b[section].style) return false; if (a[section].icon_custom_emoji_id !== b[section].icon_custom_emoji_id) return false; + if (a[section].enabled !== b[section].enabled) return false; + if (!labelsEqual(a[section].labels, b[section].labels)) return false; } return true; } @@ -36,6 +47,7 @@ export function ButtonsTab() { }); const [draftStyles, setDraftStyles] = useState(DEFAULT_BUTTON_STYLES); + const [expandedLabels, setExpandedLabels] = useState>(new Set()); const savedStylesRef = useRef(DEFAULT_BUTTON_STYLES); const draftStylesRef = useRef(draftStyles); draftStylesRef.current = draftStyles; @@ -85,27 +97,76 @@ export function ButtonsTab() { [], ); + const toggleEnabled = useCallback((section: ButtonSection) => { + setDraftStyles((prev) => ({ + ...prev, + [section]: { + ...prev[section], + enabled: !prev[section].enabled, + }, + })); + }, []); + + const updateLabel = useCallback((section: ButtonSection, locale: string, value: string) => { + setDraftStyles((prev) => ({ + ...prev, + [section]: { + ...prev[section], + labels: { + ...prev[section].labels, + [locale]: value, + }, + }, + })); + }, []); + + const toggleLabelsExpanded = useCallback((section: string) => { + setExpandedLabels((prev) => { + const next = new Set(prev); + if (next.has(section)) { + next.delete(section); + } else { + next.add(section); + } + return next; + }); + }, []); + const handleCancel = useCallback(() => { setDraftStyles(savedStylesRef.current); }, []); const handleSave = useCallback(() => { - // Build partial update — only send changed sections - const update: Record> = {}; + const update: Record> = {}; for (const section of BUTTON_SECTIONS) { const draft = draftStyles[section]; const saved = savedStylesRef.current[section]; - if ( - draft.style !== saved.style || - draft.icon_custom_emoji_id !== saved.icon_custom_emoji_id - ) { - update[section] = {}; - if (draft.style !== saved.style) { - update[section].style = draft.style; - } - if (draft.icon_custom_emoji_id !== saved.icon_custom_emoji_id) { - update[section].icon_custom_emoji_id = draft.icon_custom_emoji_id; + const sectionUpdate: Record = {}; + let changed = false; + + if (draft.style !== saved.style) { + sectionUpdate.style = draft.style; + changed = true; + } + if (draft.icon_custom_emoji_id !== saved.icon_custom_emoji_id) { + sectionUpdate.icon_custom_emoji_id = draft.icon_custom_emoji_id; + changed = true; + } + if (draft.enabled !== saved.enabled) { + sectionUpdate.enabled = draft.enabled; + changed = true; + } + if (!labelsEqual(draft.labels, saved.labels)) { + const cleanLabels: Record = {}; + for (const locale of BOT_LOCALES) { + cleanLabels[locale] = (draft.labels[locale] || '').trim(); } + sectionUpdate.labels = cleanLabels; + changed = true; + } + + if (changed) { + update[section] = sectionUpdate; } } if (Object.keys(update).length > 0) { @@ -119,33 +180,50 @@ export function ButtonsTab() {
{BUTTON_SECTIONS.map((section) => { const cfg = draftStyles[section]; + const isExpanded = expandedLabels.has(section); + const hasCustomLabels = BOT_LOCALES.some((l) => (cfg.labels[l] || '').trim()); + return (
+ {/* Header */}
-
-

- {t(`admin.buttons.sections.${section}`)} -

+
+
+

+ {t(`admin.buttons.sections.${section}`)} +

+ {!cfg.enabled && ( + + {t('admin.buttons.hidden')} + + )} +

{t(`admin.buttons.descriptions.${section}`)}

- {/* Live preview chip */} -
- {t(`admin.buttons.styles.${cfg.style}`)} +
+ {/* Live preview chip */} +
+ {t(`admin.buttons.styles.${cfg.style}`)} +
+ {/* Enabled toggle */} + toggleEnabled(section)} />
@@ -173,7 +251,7 @@ export function ButtonsTab() {
{/* Emoji ID input */} -
+
@@ -185,6 +263,48 @@ export function ButtonsTab() { className="w-full rounded-lg border border-dark-600 bg-dark-700/50 px-3 py-2 text-sm text-dark-100 placeholder-dark-500 transition-colors focus:border-accent-500 focus:outline-none" />
+ + {/* Custom labels */} +
+ + {isExpanded && ( +
+ {BOT_LOCALES.map((locale) => ( +
+ + {locale} + + updateLabel(section, locale, e.target.value)} + placeholder={t('admin.buttons.labelPlaceholder')} + maxLength={100} + className="w-full rounded-lg border border-dark-600 bg-dark-700/50 px-3 py-1.5 text-sm text-dark-100 placeholder-dark-500 transition-colors focus:border-accent-500 focus:outline-none" + /> +
+ ))} +

{t('admin.buttons.labelsHint')}

+
+ )} +
); })} @@ -198,7 +318,7 @@ export function ButtonsTab() { disabled={updateMutation.isPending} className="rounded-xl bg-accent-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-accent-600 disabled:opacity-50" > - {updateMutation.isPending ? t('common.saving', t('common.save')) : t('common.save')} + {updateMutation.isPending ? t('common.saving') : t('common.save')}