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, BOT_LOCALES, } from '../../api/buttonStyles'; import { Toggle } from './Toggle'; import { useNotify } from '../../platform/hooks/useNotify'; 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' }, ]; 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; } export function ButtonsTab() { const { t } = useTranslation(); const queryClient = useQueryClient(); const notify = useNotify(); const { data: serverStyles } = useQuery({ queryKey: ['button-styles'], queryFn: buttonStylesApi.getStyles, }); 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; 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); }, onError: () => { notify.error(t('common.error')); }, }); const resetMutation = useMutation({ mutationFn: buttonStylesApi.resetStyles, onSuccess: (data) => { savedStylesRef.current = data; setDraftStyles(data); queryClient.setQueryData(['button-styles'], data); }, onError: () => { notify.error(t('common.error')); }, }); const updateSection = useCallback( (section: ButtonSection, field: 'style' | 'icon_custom_emoji_id', value: string) => { setDraftStyles((prev) => ({ ...prev, [section]: { ...prev[section], [field]: value, }, })); }, [], ); 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(() => { const update: Record> = {}; for (const section of BUTTON_SECTIONS) { const draft = draftStyles[section]; const saved = savedStylesRef.current[section]; 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) { updateMutation.mutate(update); } }, [draftStyles, updateMutation]); return (
{/* Section cards */}
{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}`)}

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

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

{/* Live preview chip */}
{t(`admin.buttons.styles.${cfg.style}`)}
{/* Enabled toggle */} toggleEnabled(section)} />
{/* 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" />
{/* 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')}

)}
); })}
{/* Save / Cancel */} {hasUnsavedChanges && (
)} {/* Reset */}
); }