diff --git a/src/api/adminBroadcasts.ts b/src/api/adminBroadcasts.ts index d5360b2..2f0d0dd 100644 --- a/src/api/adminBroadcasts.ts +++ b/src/api/adminBroadcasts.ts @@ -48,6 +48,12 @@ export interface BroadcastButtonsResponse { buttons: BroadcastButton[]; } +export interface CustomBroadcastButton { + label: string; + action_type: 'callback' | 'url'; + action_value: string; +} + export interface BroadcastMedia { type: 'photo' | 'video' | 'document'; file_id: string; @@ -73,6 +79,7 @@ export interface CombinedBroadcastCreateRequest { // Telegram fields message_text?: string; selected_buttons?: string[]; + custom_buttons?: CustomBroadcastButton[]; media?: BroadcastMedia; // Email fields email_subject?: string; diff --git a/src/locales/en.json b/src/locales/en.json index 6e338c3..42942a2 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1701,7 +1701,14 @@ "enableEmail": "Email", "sendingBoth": "2 broadcasts will be created", "bothCreated": "Broadcasts created", - "atLeastOneChannel": "Select at least one channel" + "atLeastOneChannel": "Select at least one channel", + "customButtons": "Custom buttons", + "addCustomButton": "Add button", + "customButtonLabelPlaceholder": "Button text", + "customButtonTypeCallback": "Callback", + "customButtonTypeUrl": "URL", + "customButtonCallbackPlaceholder": "callback_data (e.g. back_to_menu)", + "customButtonUrlPlaceholder": "https://example.com" }, "pinnedMessages": { "title": "Pinned Messages", diff --git a/src/locales/fa.json b/src/locales/fa.json index 797c3e9..9f08085 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -1336,7 +1336,14 @@ "enableEmail": "Email", "sendingBoth": "۲ پیام‌رسانی ایجاد خواهد شد", "bothCreated": "پیام‌رسانی‌ها ایجاد شدند", - "atLeastOneChannel": "حداقل یک کانال را انتخاب کنید" + "atLeastOneChannel": "حداقل یک کانال را انتخاب کنید", + "customButtons": "دکمه‌های سفارشی", + "addCustomButton": "افزودن دکمه", + "customButtonLabelPlaceholder": "متن دکمه", + "customButtonTypeCallback": "Callback", + "customButtonTypeUrl": "لینک", + "customButtonCallbackPlaceholder": "callback_data (مثلاً back_to_menu)", + "customButtonUrlPlaceholder": "https://example.com" }, "pinnedMessages": { "title": "Pinned Messages", diff --git a/src/locales/ru.json b/src/locales/ru.json index 5137d97..99c85b6 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1726,7 +1726,14 @@ "enableEmail": "Email", "sendingBoth": "Будет создано 2 рассылки", "bothCreated": "Рассылки созданы", - "atLeastOneChannel": "Выберите хотя бы один канал" + "atLeastOneChannel": "Выберите хотя бы один канал", + "customButtons": "Кастомные кнопки", + "addCustomButton": "Добавить кнопку", + "customButtonLabelPlaceholder": "Текст кнопки", + "customButtonTypeCallback": "Callback", + "customButtonTypeUrl": "Ссылка", + "customButtonCallbackPlaceholder": "callback_data (например, back_to_menu)", + "customButtonUrlPlaceholder": "https://example.com" }, "pinnedMessages": { "title": "Закреплённые сообщения", diff --git a/src/locales/zh.json b/src/locales/zh.json index 734a8bb..4cc4801 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -1410,7 +1410,14 @@ "enableEmail": "Email", "sendingBoth": "将创建2个群发", "bothCreated": "群发已创建", - "atLeastOneChannel": "请至少选择一个渠道" + "atLeastOneChannel": "请至少选择一个渠道", + "customButtons": "自定义按钮", + "addCustomButton": "添加按钮", + "customButtonLabelPlaceholder": "按钮文字", + "customButtonTypeCallback": "回调", + "customButtonTypeUrl": "链接", + "customButtonCallbackPlaceholder": "callback_data(例如 back_to_menu)", + "customButtonUrlPlaceholder": "https://example.com" }, "pinnedMessages": { "title": "置顶消息", diff --git a/src/pages/AdminBroadcastCreate.tsx b/src/pages/AdminBroadcastCreate.tsx index 5e6073b..1709aab 100644 --- a/src/pages/AdminBroadcastCreate.tsx +++ b/src/pages/AdminBroadcastCreate.tsx @@ -7,6 +7,7 @@ import { BroadcastFilter, TariffFilter, CombinedBroadcastCreateRequest, + CustomBroadcastButton, } from '../api/adminBroadcasts'; import { AdminBackButton } from '../components/admin'; @@ -130,6 +131,11 @@ export default function AdminBroadcastCreate() { // Telegram-specific state const [messageText, setMessageText] = useState(''); const [selectedButtons, setSelectedButtons] = useState(['home']); + const [customButtons, setCustomButtons] = useState([]); + const [isAddingCustomButton, setIsAddingCustomButton] = useState(false); + const [newButtonLabel, setNewButtonLabel] = useState(''); + const [newButtonActionType, setNewButtonActionType] = useState<'callback' | 'url'>('callback'); + const [newButtonActionValue, setNewButtonActionValue] = useState(''); const [mediaFile, setMediaFile] = useState(null); const [mediaType, setMediaType] = useState<'photo' | 'video' | 'document'>('photo'); const [mediaPreview, setMediaPreview] = useState(null); @@ -275,28 +281,33 @@ export default function AdminBroadcastCreate() { const file = e.target.files?.[0]; if (!file) return; - setMediaFile(file); - + // Determine type locally to avoid stale state in async call + let detectedType: 'photo' | 'video' | 'document'; if (file.type.startsWith('image/')) { - setMediaType('photo'); + detectedType = 'photo'; + } else if (file.type.startsWith('video/')) { + detectedType = 'video'; + } else { + detectedType = 'document'; + } + + setMediaFile(file); + setMediaType(detectedType); + + if (detectedType === 'photo') { if (mediaPreviewRef.current) URL.revokeObjectURL(mediaPreviewRef.current); const url = URL.createObjectURL(file); mediaPreviewRef.current = url; setMediaPreview(url); - } else if (file.type.startsWith('video/')) { - setMediaType('video'); - setMediaPreview(null); } else { - setMediaType('document'); setMediaPreview(null); } setIsUploading(true); try { - const result = await adminBroadcastsApi.uploadMedia(file, mediaType); + const result = await adminBroadcastsApi.uploadMedia(file, detectedType); setUploadedFileId(result.file_id); - } catch (err) { - console.error('Upload failed:', err); + } catch { setMediaFile(null); setMediaPreview(null); } finally { @@ -325,6 +336,39 @@ export default function AdminBroadcastCreate() { ); }; + // Custom button validation + const isNewButtonValid = useMemo(() => { + if (!newButtonLabel.trim() || !newButtonActionValue.trim()) return false; + if (newButtonActionType === 'url') { + return /^https:\/\/|^tg:\/\//.test(newButtonActionValue.trim()); + } + if (newButtonActionType === 'callback') { + return new TextEncoder().encode(newButtonActionValue.trim()).length <= 64; + } + return true; + }, [newButtonLabel, newButtonActionType, newButtonActionValue]); + + // Custom button handlers + const addCustomButton = () => { + if (!isNewButtonValid) return; + setCustomButtons((prev) => [ + ...prev, + { + label: newButtonLabel.trim(), + action_type: newButtonActionType, + action_value: newButtonActionValue.trim(), + }, + ]); + setNewButtonLabel(''); + setNewButtonActionValue(''); + setNewButtonActionType('callback'); + setIsAddingCustomButton(false); + }; + + const removeCustomButton = (index: number) => { + setCustomButtons((prev) => prev.filter((_, i) => i !== index)); + }; + // Validate form const isTelegramValid = telegramEnabled && telegramTarget && messageText.trim().length > 0; const isEmailValid = @@ -350,6 +394,7 @@ export default function AdminBroadcastCreate() { target: telegramTarget, message_text: messageText, selected_buttons: selectedButtons, + custom_buttons: customButtons.length > 0 ? customButtons : undefined, }; if (uploadedFileId) { data.media = { type: mediaType, file_id: uploadedFileId }; @@ -377,6 +422,7 @@ export default function AdminBroadcastCreate() { target: telegramTarget, message_text: messageText, selected_buttons: selectedButtons, + custom_buttons: customButtons.length > 0 ? customButtons : undefined, }; if (uploadedFileId) { telegramData.media = { type: mediaType, file_id: uploadedFileId }; @@ -655,6 +701,123 @@ export default function AdminBroadcastCreate() { ))} + + {/* Custom buttons */} +
+ + + {/* Existing custom buttons */} + {customButtons.length > 0 && ( +
+ {customButtons.map((btn, index) => ( +
+
+ + {btn.action_type === 'url' + ? t('admin.broadcasts.customButtonTypeUrl') + : t('admin.broadcasts.customButtonTypeCallback')} + + {btn.label} + {btn.action_value} +
+ +
+ ))} +
+ )} + + {/* Inline add form */} + {isAddingCustomButton ? ( +
{ + e.preventDefault(); + addCustomButton(); + }} + className="space-y-3 rounded-lg border border-dark-600 bg-dark-800/50 p-3" + > + setNewButtonLabel(e.target.value)} + placeholder={t('admin.broadcasts.customButtonLabelPlaceholder')} + maxLength={64} + className="input" + autoFocus + /> +
+ + +
+ setNewButtonActionValue(e.target.value)} + placeholder={ + newButtonActionType === 'url' + ? t('admin.broadcasts.customButtonUrlPlaceholder') + : t('admin.broadcasts.customButtonCallbackPlaceholder') + } + maxLength={newButtonActionType === 'callback' ? 64 : 256} + className="input" + /> +
+ + +
+
+ ) : ( + + )} +
)}