import { useMemo, type ReactNode } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; interface PreviewButton { text: string; url?: string; callback_data?: string; } interface TelegramPreviewProps { open: boolean; onClose: () => void; text: string; mediaUrl?: string | null; mediaType?: 'photo' | 'video' | null; buttons?: PreviewButton[][]; } interface EmailPreviewProps { open: boolean; onClose: () => void; subject: string; htmlContent: string; } interface Token { kind: 'text' | 'open' | 'close' | 'br'; tag?: string; href?: string; value?: string; } const TG_TAGS = new Set([ 'b', 'strong', 'i', 'em', 'u', 'ins', 's', 'strike', 'del', 'code', 'pre', 'a', 'tg-spoiler', 'span', ]); function tokenize(input: string): Token[] { const tokens: Token[] = []; const pattern = /<(\/?)([a-z][a-z0-9-]*)(\s+[^>]*)?>|/gi; const matches = [...input.matchAll(pattern)]; let lastIdx = 0; for (const m of matches) { const idx = m.index || 0; if (idx > lastIdx) tokens.push({ kind: 'text', value: input.slice(lastIdx, idx) }); if (m[0].toLowerCase().startsWith(' (i === 0 ? [p] : [
, p])); } type Frame = { tag: string | null; href?: string; children: ReactNode[] }; function wrap(frame: Frame, key: number): ReactNode { const k = `el-${key}`; switch (frame.tag) { case 'b': case 'strong': return {frame.children}; case 'i': case 'em': return {frame.children}; case 'u': case 'ins': return {frame.children}; case 's': case 'strike': case 'del': return {frame.children}; case 'code': return ( {frame.children} ); case 'pre': return (
          {frame.children}
        
); case 'a': { const safeHref = frame.href && /^(https?:|tg:|mailto:|tel:)/i.test(frame.href) ? frame.href : '#'; return ( {frame.children} ); } case 'tg-spoiler': case 'span': return {frame.children}; default: return {frame.children}; } } function tokensToReact(tokens: Token[]): ReactNode { const root: Frame = { tag: null, children: [] }; const stack: Frame[] = [root]; let textKey = 0; let elKey = 0; for (const tok of tokens) { const top = stack[stack.length - 1]; if (tok.kind === 'text') { top.children.push({renderText(tok.value || '', textKey)}); } else if (tok.kind === 'br') { top.children.push(
); } else if (tok.kind === 'open') { stack.push({ tag: tok.tag!, href: tok.href, children: [] }); } else if (tok.kind === 'close') { let foundIdx = -1; for (let i = stack.length - 1; i >= 1; i--) { if (stack[i].tag === tok.tag) { foundIdx = i; break; } } if (foundIdx === -1) continue; while (stack.length > foundIdx) { const closed = stack.pop()!; const parent = stack[stack.length - 1] || root; parent.children.push(wrap(closed, elKey++)); } } } while (stack.length > 1) { const closed = stack.pop()!; const parent = stack[stack.length - 1]; parent.children.push(wrap(closed, elKey++)); } return root.children; } export function TelegramPreview({ open, onClose, text, mediaUrl, mediaType, buttons, }: TelegramPreviewProps) { const { t } = useTranslation(); const rendered = useMemo(() => tokensToReact(tokenize(text)), [text]); if (!open) return null; return createPortal(
e.stopPropagation()} >

{t('admin.broadcasts.preview', 'Предпросмотр Telegram')}

{mediaUrl && mediaType === 'photo' && ( )} {mediaUrl && mediaType === 'video' && (
{buttons && buttons.length > 0 && (
{buttons.map((row, ri) => (
{row.map((b, ci) => ( ))}
))}
)}
, document.body, ); } export function EmailPreview({ open, onClose, subject, htmlContent }: EmailPreviewProps) { const { t } = useTranslation(); if (!open) return null; const emptyHtml = `

${t('admin.broadcasts.previewEmpty', '— пусто —')}

`; return createPortal(
e.stopPropagation()} >
{t('admin.broadcasts.emailSubject', 'Тема')}
{subject || t('admin.broadcasts.previewEmpty', '— пусто —')}