feat: add menu editor tab with drag-and-drop rows, custom URL buttons, and button configuration

- Add menuLayout.ts API client with normalizeConfig and type definitions
- Add MenuEditorTab.tsx with DnD rows, button CRUD, style/emoji/label editors
- Replace ButtonsTab with MenuEditorTab on admin settings page
- Add menu editor translations for ru, en, zh, fa locales
- Loading/error states, useMemo optimizations, proper ref patterns
This commit is contained in:
Fringg
2026-03-09 23:07:38 +03:00
parent 78fda22679
commit 23aa86f1a8
7 changed files with 974 additions and 10 deletions

88
src/api/menuLayout.ts Normal file
View File

@@ -0,0 +1,88 @@
import apiClient from './client';
export interface MenuButtonConfig {
id: string;
type: 'builtin' | 'custom';
style: 'primary' | 'success' | 'danger' | 'default';
icon_custom_emoji_id: string;
enabled: boolean;
labels: Record<string, string>;
url: string | null;
}
export interface MenuRowConfig {
id: string;
max_per_row: number;
buttons: MenuButtonConfig[];
}
export interface MenuConfig {
rows: MenuRowConfig[];
}
export const BOT_LOCALES = ['ru', 'en', 'ua', 'zh', 'fa'] as const;
export type BotLocale = (typeof BOT_LOCALES)[number];
export const BUILTIN_SECTIONS = [
'home',
'subscription',
'balance',
'referral',
'support',
'info',
'admin',
'language',
] as const;
export type BuiltinSection = (typeof BUILTIN_SECTIONS)[number];
export const STYLE_OPTIONS = [
{ value: 'default' as const, colorClass: 'bg-dark-500' },
{ value: 'primary' as const, colorClass: 'bg-blue-500' },
{ value: 'success' as const, colorClass: 'bg-success-500' },
{ value: 'danger' as const, colorClass: 'bg-red-500' },
];
const DEFAULT_CONFIG: MenuConfig = { rows: [] };
const DEFAULT_BUTTON: Omit<MenuButtonConfig, 'id' | 'type'> = {
style: 'primary',
icon_custom_emoji_id: '',
enabled: true,
labels: {},
url: null,
};
function normalizeConfig(data: MenuConfig): MenuConfig {
if (!data || typeof data !== 'object') {
return DEFAULT_CONFIG;
}
return {
rows: (data.rows || []).map((row) => ({
id: row.id,
max_per_row: row.max_per_row ?? 2,
buttons: (row.buttons || []).map((btn) => ({
...DEFAULT_BUTTON,
...btn,
labels: { ...(btn.labels || {}) },
})),
})),
};
}
export const menuLayoutApi = {
getConfig: async (): Promise<MenuConfig> => {
const response = await apiClient.get<MenuConfig>('/cabinet/admin/menu-layout');
return normalizeConfig(response.data);
},
updateConfig: async (config: MenuConfig): Promise<MenuConfig> => {
const response = await apiClient.put<MenuConfig>('/cabinet/admin/menu-layout', config);
return normalizeConfig(response.data);
},
resetConfig: async (): Promise<MenuConfig> => {
const response = await apiClient.post<MenuConfig>('/cabinet/admin/menu-layout/reset');
return normalizeConfig(response.data);
},
};