From 3126b1bf8cb7c8a77f37abfcaea94d5d4c3475c7 Mon Sep 17 00:00:00 2001 From: Egor Date: Mon, 26 Jan 2026 21:59:06 +0300 Subject: [PATCH 1/4] Add files via upload --- src/api/adminEmailTemplates.ts | 85 ++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/api/adminEmailTemplates.ts diff --git a/src/api/adminEmailTemplates.ts b/src/api/adminEmailTemplates.ts new file mode 100644 index 0000000..4c0b225 --- /dev/null +++ b/src/api/adminEmailTemplates.ts @@ -0,0 +1,85 @@ +import apiClient from './client' + +export interface EmailTemplateLanguageStatus { + has_custom: boolean +} + +export interface EmailTemplateType { + type: string + label: Record + description: Record + context_vars: string[] + languages: Record +} + +export interface EmailTemplateListResponse { + items: EmailTemplateType[] + available_languages: string[] +} + +export interface EmailTemplateLanguageData { + subject: string + body_html: string + is_default: boolean + default_subject: string + default_body_html: string +} + +export interface EmailTemplateDetail { + notification_type: string + label: Record + description: Record + context_vars: string[] + languages: Record +} + +export interface EmailTemplateUpdateRequest { + subject: string + body_html: string +} + +export interface EmailTemplatePreviewRequest { + language: string + subject?: string + body_html?: string +} + +export interface EmailTemplatePreviewResponse { + subject: string + body_html: string +} + +export interface EmailTemplateSendTestRequest { + language: string + email?: string +} + +export const adminEmailTemplatesApi = { + getTemplateTypes: async (): Promise => { + const response = await apiClient.get('/cabinet/admin/email-templates') + return response.data + }, + + getTemplate: async (notificationType: string): Promise => { + const response = await apiClient.get(`/cabinet/admin/email-templates/${notificationType}`) + return response.data + }, + + updateTemplate: async (notificationType: string, language: string, data: EmailTemplateUpdateRequest): Promise => { + await apiClient.put(`/cabinet/admin/email-templates/${notificationType}/${language}`, data) + }, + + deleteTemplate: async (notificationType: string, language: string): Promise => { + await apiClient.delete(`/cabinet/admin/email-templates/${notificationType}/${language}`) + }, + + previewTemplate: async (notificationType: string, data: EmailTemplatePreviewRequest): Promise => { + const response = await apiClient.post(`/cabinet/admin/email-templates/${notificationType}/preview`, data) + return response.data + }, + + sendTestEmail: async (notificationType: string, data: EmailTemplateSendTestRequest): Promise<{ sent_to: string }> => { + const response = await apiClient.post<{ status: string; sent_to: string }>(`/cabinet/admin/email-templates/${notificationType}/test`, data) + return response.data + }, +} From f10594c4b4a571cd24db0a831463a1421f950372 Mon Sep 17 00:00:00 2001 From: Egor Date: Mon, 26 Jan 2026 21:59:31 +0300 Subject: [PATCH 2/4] Add files via upload --- src/pages/AdminEmailTemplates.tsx | 548 ++++++++++++++++++++++++++++++ src/pages/AdminPanel.tsx | 12 + 2 files changed, 560 insertions(+) create mode 100644 src/pages/AdminEmailTemplates.tsx diff --git a/src/pages/AdminEmailTemplates.tsx b/src/pages/AdminEmailTemplates.tsx new file mode 100644 index 0000000..44d6c15 --- /dev/null +++ b/src/pages/AdminEmailTemplates.tsx @@ -0,0 +1,548 @@ +import { useState, useCallback, useRef, useEffect } from 'react' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { useTranslation } from 'react-i18next' +import { Link } from 'react-router-dom' +import { + adminEmailTemplatesApi, + EmailTemplateType, + EmailTemplateDetail, + EmailTemplateLanguageData, +} from '../api/adminEmailTemplates' + +// Icons +const BackIcon = () => ( + + + +) + +const MailIcon = () => ( + + + +) + +const SaveIcon = () => ( + + + +) + +const EyeIcon = () => ( + + + + +) + +const SendIcon = () => ( + + + +) + +const ResetIcon = () => ( + + + +) + +const XIcon = () => ( + + + +) + +const LANG_LABELS: Record = { + ru: 'RU', + en: 'EN', + zh: 'ZH', + ua: 'UA', +} + +const LANG_FULL_LABELS: Record = { + ru: 'Русский', + en: 'English', + zh: '中文', + ua: 'Українська', +} + +// ============ Template List View ============ + +function TemplateCard({ + template, + currentLang, + onClick, +}: { + template: EmailTemplateType + currentLang: string + onClick: () => void +}) { + const label = template.label[currentLang] || template.label['en'] || template.type + const description = template.description[currentLang] || template.description['en'] || '' + const customCount = Object.values(template.languages).filter(l => l.has_custom).length + + return ( + + ) +} + +// ============ Template Editor ============ + +function TemplateEditor({ + detail, + onClose, + currentLang: interfaceLang, +}: { + detail: EmailTemplateDetail + onClose: () => void + currentLang: string +}) { + const { t } = useTranslation() + const queryClient = useQueryClient() + const [activeLang, setActiveLang] = useState('ru') + const [editSubject, setEditSubject] = useState('') + const [editBody, setEditBody] = useState('') + const [isDirty, setIsDirty] = useState(false) + const [showPreview, setShowPreview] = useState(false) + const [previewHtml, setPreviewHtml] = useState('') + const [toast, setToast] = useState<{ type: 'success' | 'error'; message: string } | null>(null) + const textareaRef = useRef(null) + const iframeRef = useRef(null) + + const langData: EmailTemplateLanguageData | undefined = detail.languages[activeLang] + + // Load data for current language + useEffect(() => { + if (langData) { + setEditSubject(langData.subject) + setEditBody(langData.is_default ? langData.body_html : langData.body_html) + setIsDirty(false) + } + }, [activeLang, langData]) + + const showToast = useCallback((type: 'success' | 'error', message: string) => { + setToast({ type, message }) + setTimeout(() => setToast(null), 3000) + }, []) + + // Extract body content from full HTML (strip base template wrapper) + const extractBodyContent = useCallback((html: string): string => { + // If it's wrapped in the base template, extract just the content div + const contentMatch = html.match(/
\s*([\s\S]*?)\s*<\/div>\s*