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 (