From b2898730b98b3bf73d075158a9f59ef5bf1f6e54 Mon Sep 17 00:00:00 2001 From: Fringg Date: Thu, 12 Feb 2026 23:16:08 +0300 Subject: [PATCH 1/7] feat: add ButtonsTab for per-section button style customization Visual editor in admin settings for configuring Telegram button colors and custom emoji IDs per menu section. Includes API client, draft state management, save/reset, and translations for ru/en/fa/zh locales. --- src/api/buttonStyles.ts | 66 +++++++++ src/components/admin/ButtonsTab.tsx | 222 ++++++++++++++++++++++++++++ src/components/admin/constants.ts | 1 + src/components/admin/index.ts | 1 + src/locales/en.json | 30 ++++ src/locales/fa.json | 30 ++++ src/locales/ru.json | 30 ++++ src/locales/zh.json | 30 ++++ src/pages/AdminSettings.tsx | 3 + 9 files changed, 413 insertions(+) create mode 100644 src/api/buttonStyles.ts create mode 100644 src/components/admin/ButtonsTab.tsx diff --git a/src/api/buttonStyles.ts b/src/api/buttonStyles.ts new file mode 100644 index 0000000..fe2adcb --- /dev/null +++ b/src/api/buttonStyles.ts @@ -0,0 +1,66 @@ +import apiClient from './client'; + +export interface ButtonSectionConfig { + style: 'primary' | 'success' | 'danger'; + icon_custom_emoji_id: string; +} + +export interface ButtonStylesConfig { + home: ButtonSectionConfig; + subscription: ButtonSectionConfig; + balance: ButtonSectionConfig; + referral: ButtonSectionConfig; + support: ButtonSectionConfig; + info: ButtonSectionConfig; + admin: ButtonSectionConfig; +} + +export type ButtonStylesUpdate = { + [K in keyof ButtonStylesConfig]?: Partial; +}; + +export const BUTTON_SECTIONS = [ + 'home', + 'subscription', + 'balance', + 'referral', + 'support', + 'info', + 'admin', +] as const; + +export type ButtonSection = (typeof BUTTON_SECTIONS)[number]; + +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: '' }, +}; + +export const buttonStylesApi = { + getStyles: async (): Promise => { + try { + const response = await apiClient.get('/cabinet/admin/button-styles'); + return response.data; + } catch { + return DEFAULT_BUTTON_STYLES; + } + }, + + updateStyles: async (update: ButtonStylesUpdate): Promise => { + const response = await apiClient.patch( + '/cabinet/admin/button-styles', + update, + ); + return response.data; + }, + + resetStyles: async (): Promise => { + const response = await apiClient.post('/cabinet/admin/button-styles/reset'); + return response.data; + }, +}; diff --git a/src/components/admin/ButtonsTab.tsx b/src/components/admin/ButtonsTab.tsx new file mode 100644 index 0000000..dbd757f --- /dev/null +++ b/src/components/admin/ButtonsTab.tsx @@ -0,0 +1,222 @@ +import { useEffect, useState, useRef, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { + buttonStylesApi, + ButtonStylesConfig, + DEFAULT_BUTTON_STYLES, + BUTTON_SECTIONS, + ButtonSection, +} from '../../api/buttonStyles'; + +type StyleValue = 'primary' | 'success' | 'danger'; + +const STYLE_OPTIONS: { value: StyleValue; colorClass: string }[] = [ + { value: 'primary', colorClass: 'bg-blue-500' }, + { value: 'success', colorClass: 'bg-green-500' }, + { value: 'danger', colorClass: 'bg-red-500' }, +]; + +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; + } + return true; +} + +export function ButtonsTab() { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + const { data: serverStyles } = useQuery({ + queryKey: ['button-styles'], + queryFn: buttonStylesApi.getStyles, + }); + + const [draftStyles, setDraftStyles] = useState(DEFAULT_BUTTON_STYLES); + const savedStylesRef = useRef(DEFAULT_BUTTON_STYLES); + const draftStylesRef = useRef(draftStyles); + draftStylesRef.current = draftStyles; + + useEffect(() => { + if (serverStyles) { + if ( + stylesEqual(savedStylesRef.current, draftStylesRef.current) || + stylesEqual(savedStylesRef.current, DEFAULT_BUTTON_STYLES) + ) { + setDraftStyles(serverStyles); + savedStylesRef.current = serverStyles; + } + } + }, [serverStyles]); + + const hasUnsavedChanges = !stylesEqual(draftStyles, savedStylesRef.current); + + const updateMutation = useMutation({ + mutationFn: buttonStylesApi.updateStyles, + onSuccess: (data) => { + savedStylesRef.current = data; + setDraftStyles(data); + queryClient.setQueryData(['button-styles'], data); + }, + }); + + const resetMutation = useMutation({ + mutationFn: buttonStylesApi.resetStyles, + onSuccess: (data) => { + savedStylesRef.current = data; + setDraftStyles(data); + queryClient.setQueryData(['button-styles'], data); + }, + }); + + const updateSection = useCallback( + (section: ButtonSection, field: 'style' | 'icon_custom_emoji_id', value: string) => { + setDraftStyles((prev) => ({ + ...prev, + [section]: { + ...prev[section], + [field]: value, + }, + })); + }, + [], + ); + + const handleCancel = useCallback(() => { + setDraftStyles(savedStylesRef.current); + }, []); + + const handleSave = useCallback(() => { + // Build partial update — only send changed sections + 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; + } + } + } + if (Object.keys(update).length > 0) { + updateMutation.mutate(update); + } + }, [draftStyles, updateMutation]); + + return ( +
+ {/* Section cards */} +
+ {BUTTON_SECTIONS.map((section) => { + const cfg = draftStyles[section]; + return ( +
+
+
+

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

+

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

+
+ {/* Live preview chip */} +
+ {t(`admin.buttons.styles.${cfg.style}`)} +
+
+ + {/* Color selector chips */} +
+ +
+ {STYLE_OPTIONS.map((opt) => ( + + ))} +
+
+ + {/* Emoji ID input */} +
+ + updateSection(section, 'icon_custom_emoji_id', e.target.value)} + placeholder={t('admin.buttons.emojiPlaceholder')} + 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" + /> +
+
+ ); + })} +
+ + {/* Save / Cancel */} + {hasUnsavedChanges && ( +
+ + +
+ )} + + {/* Reset */} +
+ +
+
+ ); +} diff --git a/src/components/admin/constants.ts b/src/components/admin/constants.ts index d35fee9..30ab6a4 100644 --- a/src/components/admin/constants.ts +++ b/src/components/admin/constants.ts @@ -21,6 +21,7 @@ export const MENU_SECTIONS: MenuSection[] = [ { id: 'branding', iconType: null }, { id: 'theme', iconType: null }, { id: 'analytics', iconType: null }, + { id: 'buttons', iconType: null }, ], }, { diff --git a/src/components/admin/index.ts b/src/components/admin/index.ts index 4fc1815..9771d97 100644 --- a/src/components/admin/index.ts +++ b/src/components/admin/index.ts @@ -6,6 +6,7 @@ export * from './SettingInput'; export * from './SettingRow'; export * from './AnalyticsTab'; export * from './BrandingTab'; +export * from './ButtonsTab'; export * from './ThemeTab'; export * from './FavoritesTab'; export * from './SettingsTab'; diff --git a/src/locales/en.json b/src/locales/en.json index ab88e42..dda35ea 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1217,6 +1217,7 @@ "noSearchResults": "Nothing found", "example": "Example", "analytics": "Analytics", + "buttons": "Buttons", "notConfigured": "Not configured", "counterActive": "Active", "counterInactive": "Inactive", @@ -1286,6 +1287,35 @@ "quickPresets": "Quick presets", "saturation": "Saturation" }, + "buttons": { + "color": "Button color", + "emojiId": "Custom Emoji ID", + "emojiPlaceholder": "Custom emoji ID (optional)", + "resetAll": "Reset all styles", + "sections": { + "home": "Home", + "subscription": "Subscription", + "balance": "Balance", + "referral": "Referrals", + "support": "Support", + "info": "Info", + "admin": "Admin Panel" + }, + "descriptions": { + "home": "Personal cabinet button", + "subscription": "Subscription management", + "balance": "Top-up and balance", + "referral": "Referral program", + "support": "Support tickets", + "info": "Information section", + "admin": "Web admin panel" + }, + "styles": { + "primary": "Blue", + "success": "Green", + "danger": "Red" + } + }, "apps": { "title": "App Management", "addApp": "Add App", diff --git a/src/locales/fa.json b/src/locales/fa.json index 1490fcd..2524832 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -935,6 +935,7 @@ "noSearchResults": "چیزی یافت نشد", "example": "مثال", "analytics": "تحلیل‌ها", + "buttons": "دکمه‌ها", "notConfigured": "پیکربندی نشده", "counterActive": "فعال", "counterInactive": "غیرفعال", @@ -995,6 +996,35 @@ "resetAllColors": "بازنشانی همه رنگ‌ها", "statusColors": "رنگ‌های وضعیت" }, + "buttons": { + "color": "رنگ دکمه", + "emojiId": "شناسه ایموجی سفارشی", + "emojiPlaceholder": "شناسه ایموجی سفارشی (اختیاری)", + "resetAll": "بازنشانی همه سبک‌ها", + "sections": { + "home": "صفحه اصلی", + "subscription": "اشتراک", + "balance": "موجودی", + "referral": "معرفی", + "support": "پشتیبانی", + "info": "اطلاعات", + "admin": "پنل مدیریت" + }, + "descriptions": { + "home": "دکمه کابینت شخصی", + "subscription": "مدیریت اشتراک", + "balance": "شارژ و موجودی", + "referral": "برنامه معرفی", + "support": "تیکت‌های پشتیبانی", + "info": "بخش اطلاعات", + "admin": "پنل مدیریت وب" + }, + "styles": { + "primary": "آبی", + "success": "سبز", + "danger": "قرمز" + } + }, "apps": { "title": "مدیریت برنامه‌ها", "addApp": "افزودن برنامه", diff --git a/src/locales/ru.json b/src/locales/ru.json index e567be5..2126f98 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1260,6 +1260,7 @@ "branding": "Брендинг", "theme": "Тема", "analytics": "Аналитика", + "buttons": "Кнопки", "payments": "Платежи", "subscriptions": "Подписки", "interface": "Интерфейс", @@ -1801,6 +1802,35 @@ "MODERATION": "Модерация" } }, + "buttons": { + "color": "Цвет кнопки", + "emojiId": "Custom Emoji ID", + "emojiPlaceholder": "ID кастомного эмодзи (необязательно)", + "resetAll": "Сбросить все стили", + "sections": { + "home": "Главная", + "subscription": "Подписка", + "balance": "Баланс", + "referral": "Рефералы", + "support": "Поддержка", + "info": "Информация", + "admin": "Админка" + }, + "descriptions": { + "home": "Кнопка личного кабинета", + "subscription": "Управление подпиской", + "balance": "Пополнение и баланс", + "referral": "Реферальная программа", + "support": "Обращения в поддержку", + "info": "Информационный раздел", + "admin": "Веб-админка" + }, + "styles": { + "primary": "Синий", + "success": "Зелёный", + "danger": "Красный" + } + }, "apps": { "title": "Управление приложениями", "addApp": "Добавить приложение", diff --git a/src/locales/zh.json b/src/locales/zh.json index 0b7eaa9..b5271c4 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -973,6 +973,7 @@ "noSearchResults": "未找到结果", "example": "示例", "analytics": "分析", + "buttons": "按钮", "notConfigured": "未配置", "counterActive": "已启用", "counterInactive": "未启用", @@ -1033,6 +1034,35 @@ "resetAllColors": "重置所有颜色", "statusColors": "状态颜色" }, + "buttons": { + "color": "按钮颜色", + "emojiId": "自定义表情 ID", + "emojiPlaceholder": "自定义表情 ID(可选)", + "resetAll": "重置所有样式", + "sections": { + "home": "主页", + "subscription": "订阅", + "balance": "余额", + "referral": "推荐", + "support": "支持", + "info": "信息", + "admin": "管理面板" + }, + "descriptions": { + "home": "个人中心按钮", + "subscription": "订阅管理", + "balance": "充值和余额", + "referral": "推荐计划", + "support": "支持工单", + "info": "信息板块", + "admin": "网页管理面板" + }, + "styles": { + "primary": "蓝色", + "success": "绿色", + "danger": "红色" + } + }, "apps": { "title": "应用管理", "addApp": "添加应用", diff --git a/src/pages/AdminSettings.tsx b/src/pages/AdminSettings.tsx index 26ba607..9b429ed 100644 --- a/src/pages/AdminSettings.tsx +++ b/src/pages/AdminSettings.tsx @@ -9,6 +9,7 @@ import { MENU_SECTIONS, MenuItem, formatSettingKey } from '../components/admin'; import { usePlatform } from '../platform/hooks/usePlatform'; import { AnalyticsTab } from '../components/admin/AnalyticsTab'; import { BrandingTab } from '../components/admin/BrandingTab'; +import { ButtonsTab } from '../components/admin/ButtonsTab'; import { ThemeTab } from '../components/admin/ThemeTab'; import { FavoritesTab } from '../components/admin/FavoritesTab'; import { SettingsTab } from '../components/admin/SettingsTab'; @@ -174,6 +175,8 @@ export default function AdminSettings() { return ; case 'theme': return ; + case 'buttons': + return ; case 'favorites': return ( Date: Thu, 12 Feb 2026 23:25:49 +0300 Subject: [PATCH 2/7] feat: add 'no color' option for button style customization Adds a 'default' style option that removes color from buttons, showing Telegram's native default style. Updates type, UI chip, preview, and translations for all 4 locales. --- src/api/buttonStyles.ts | 2 +- src/components/admin/ButtonsTab.tsx | 17 ++++++++++------- src/locales/en.json | 1 + src/locales/fa.json | 1 + src/locales/ru.json | 1 + src/locales/zh.json | 1 + 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/api/buttonStyles.ts b/src/api/buttonStyles.ts index fe2adcb..c859433 100644 --- a/src/api/buttonStyles.ts +++ b/src/api/buttonStyles.ts @@ -1,7 +1,7 @@ import apiClient from './client'; export interface ButtonSectionConfig { - style: 'primary' | 'success' | 'danger'; + style: 'primary' | 'success' | 'danger' | 'default'; icon_custom_emoji_id: string; } diff --git a/src/components/admin/ButtonsTab.tsx b/src/components/admin/ButtonsTab.tsx index dbd757f..257f33d 100644 --- a/src/components/admin/ButtonsTab.tsx +++ b/src/components/admin/ButtonsTab.tsx @@ -9,9 +9,10 @@ import { ButtonSection, } from '../../api/buttonStyles'; -type StyleValue = 'primary' | 'success' | 'danger'; +type StyleValue = 'primary' | 'success' | 'danger' | 'default'; const STYLE_OPTIONS: { value: StyleValue; colorClass: string }[] = [ + { value: 'default', colorClass: 'bg-dark-500' }, { value: 'primary', colorClass: 'bg-blue-500' }, { value: 'success', colorClass: 'bg-green-500' }, { value: 'danger', colorClass: 'bg-red-500' }, @@ -134,12 +135,14 @@ export function ButtonsTab() { {/* Live preview chip */}
{t(`admin.buttons.styles.${cfg.style}`)} diff --git a/src/locales/en.json b/src/locales/en.json index dda35ea..c8d16ca 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1311,6 +1311,7 @@ "admin": "Web admin panel" }, "styles": { + "default": "No color", "primary": "Blue", "success": "Green", "danger": "Red" diff --git a/src/locales/fa.json b/src/locales/fa.json index 2524832..0939fb1 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -1020,6 +1020,7 @@ "admin": "پنل مدیریت وب" }, "styles": { + "default": "بدون رنگ", "primary": "آبی", "success": "سبز", "danger": "قرمز" diff --git a/src/locales/ru.json b/src/locales/ru.json index 2126f98..655d77c 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1826,6 +1826,7 @@ "admin": "Веб-админка" }, "styles": { + "default": "Без цвета", "primary": "Синий", "success": "Зелёный", "danger": "Красный" diff --git a/src/locales/zh.json b/src/locales/zh.json index b5271c4..36a0bb3 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -1058,6 +1058,7 @@ "admin": "网页管理面板" }, "styles": { + "default": "无颜色", "primary": "蓝色", "success": "绿色", "danger": "红色" From 1a0a5ff45313da383ed402b09a630e2774d2ae04 Mon Sep 17 00:00:00 2001 From: Fringg Date: Thu, 12 Feb 2026 23:42:58 +0300 Subject: [PATCH 3/7] 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')} ))}