Fixed notification composition

This commit is contained in:
2026-10-22 23:36:03 +02:00
parent 46c6ddcc80
commit 5050c901bc
9 changed files with 2949 additions and 358 deletions
@@ -9,12 +9,12 @@ import {
CheckCircle2,
Bold,
Italic,
Underline,
Heading2,
List,
ListOrdered,
Quote,
Table,
Code,
Minus,
Sparkles,
Eye,
@@ -25,7 +25,13 @@ import {
import { UserSearchPicker, SelectedUser } from './UserSearchPicker';
import { useInAppNotifications, DraftMessage } from '../../hooks/useInAppNotifications';
import { NotificationPlatform } from '../../types';
import { renderMarkdownToHtml, parseMarkdownToHtml, PFM_LOGO_DATA_URI } from '../../lib/markdownUtils';
import {
renderMarkdownToHtml,
parseMarkdownToHtml,
isHtmlContent,
stripMarkdown,
PFM_LOGO_DATA_URI,
} from '../../lib/markdownUtils';
import { BrandLogo } from '../ui/BrandLogo';
import { useAuth } from '../../auth/AuthProvider';
@@ -49,7 +55,10 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
draftToEdit,
}) => {
const { user, claims } = useAuth();
const tenantName = (claims?.tenant as string) || user?.tenant || 'Department of Science, Innovation & Technology';
const tenantName =
(claims?.tenant as string) ||
user?.tenant ||
'Department of Science, Innovation & Technology';
const { sendMessage, isSending, saveDraft, deleteDraft } = useInAppNotifications();
const [selectedUsers, setSelectedUsers] = useState<SelectedUser[]>([]);
@@ -60,10 +69,19 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
const [error, setError] = useState<string | null>(null);
const [successNotice, setSuccessNotice] = useState<string | null>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const editorRef = useRef<HTMLDivElement>(null);
const toInitialRichHtml = (raw: string): string => {
if (!raw || !raw.trim()) return '';
if (isHtmlContent(raw)) return raw;
return parseMarkdownToHtml(raw);
};
// Initialize or populate from draft or props
useEffect(() => {
if (!isOpen) return;
let initialHtml = '';
if (draftToEdit) {
setSelectedUsers(
draftToEdit.recipientId
@@ -77,23 +95,40 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
: []
);
setSubject(draftToEdit.subject || '');
setMessage(draftToEdit.message || '');
setPlatform(NotificationPlatform.InApp);
initialHtml = toInitialRichHtml(draftToEdit.message || '');
setMessage(initialHtml);
setPlatform(draftToEdit.platform || NotificationPlatform.InApp);
} else {
setSelectedUsers(initialRecipient ? [initialRecipient] : []);
setSubject(initialSubject || '');
setMessage(initialMessage || '');
initialHtml = toInitialRichHtml(initialMessage || '');
setMessage(initialHtml);
setPlatform(initialPlatform);
}
setActiveTab('write');
setError(null);
setSuccessNotice(null);
setTimeout(() => {
if (editorRef.current) {
editorRef.current.innerHTML = initialHtml;
}
}, 0);
}, [isOpen, initialRecipient, initialSubject, initialMessage, initialPlatform, draftToEdit]);
// Restore editor innerHTML when switching back from preview tab
useEffect(() => {
if (activeTab === 'write' && editorRef.current) {
if (editorRef.current.innerHTML !== message) {
editorRef.current.innerHTML = message;
}
}
}, [activeTab]);
if (!isOpen) return null;
const handleSelectUser = (user: SelectedUser) => {
setSelectedUsers([user]);
const handleSelectUser = (selected: SelectedUser) => {
setSelectedUsers([selected]);
setError(null);
};
@@ -101,51 +136,59 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
setSelectedUsers([]);
};
// Helper to insert markdown text at cursor in textarea
const insertMarkdown = (before: string, after: string = '', defaultText: string = '') => {
const textarea = textareaRef.current;
if (!textarea) return;
const syncEditorContent = () => {
if (editorRef.current) {
setMessage(editorRef.current.innerHTML);
}
};
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const text = textarea.value;
const selection = text.substring(start, end) || defaultText;
const applyFormat = (command: string, value?: string) => {
const editor = editorRef.current;
if (!editor) return;
editor.focus();
document.execCommand(command, false, value);
syncEditorContent();
};
const replacement = `${before}${selection}${after}`;
const newText = text.substring(0, start) + replacement + text.substring(end);
setMessage(newText);
setTimeout(() => {
textarea.focus();
textarea.setSelectionRange(
start + before.length,
start + before.length + selection.length
);
}, 0);
const insertHtmlBlock = (htmlSnippet: string) => {
const editor = editorRef.current;
if (!editor) return;
editor.focus();
const inserted = document.execCommand('insertHTML', false, htmlSnippet);
if (!inserted) {
editor.innerHTML += htmlSnippet;
}
syncEditorContent();
};
// Quick insertions for PFM branding and templates
const insertBrandedCallout = () => {
insertMarkdown(
`> [!NOTE]\n> **Post-Fund Management Official Notice**\n> Please verify all supporting evidence documentation before tranche disbursement sign-off.\n\n`
insertHtmlBlock(
`<div style="background:rgba(12,74,110,0.25);border-left:4px solid #38bdf8;padding:10px 14px;border-radius:8px;margin:10px 0;color:#bae6fd;font-size:12px;"><strong>Post-Fund Management Official Notice</strong><br/>Please verify all supporting evidence documentation before tranche disbursement sign-off.</div><p><br/></p>`
);
};
const insertMilestoneTable = () => {
insertMarkdown(
`| Deliverable Code | Milestone Name | Status | Due Date |\n| :--- | :--- | :--- | :--- |\n| DEL-01 | Technical Architecture Review | Verified | 2026-11-15 |\n| DEL-02 | Financial Expenditure Proof | Under Review | 2026-12-01 |\n\n`
insertHtmlBlock(
`<div style="overflow-x:auto;margin:12px 0;"><table style="width:100%;border-collapse:collapse;border:1px solid #334155;font-size:12px;text-align:left;"><thead><tr style="background:#1e293b;color:#f8fafc;"><th style="padding:8px 12px;border:1px solid #334155;">Deliverable Code</th><th style="padding:8px 12px;border:1px solid #334155;">Milestone Name</th><th style="padding:8px 12px;border:1px solid #334155;">Status</th><th style="padding:8px 12px;border:1px solid #334155;">Due Date</th></tr></thead><tbody><tr style="background:#0f172a;color:#cbd5e1;"><td style="padding:8px 12px;border:1px solid #334155;">DEL-01</td><td style="padding:8px 12px;border:1px solid #334155;">Technical Architecture Review</td><td style="padding:8px 12px;border:1px solid #334155;">Verified</td><td style="padding:8px 12px;border:1px solid #334155;">2026-11-15</td></tr><tr style="background:#0f172a;color:#cbd5e1;"><td style="padding:8px 12px;border:1px solid #334155;">DEL-02</td><td style="padding:8px 12px;border:1px solid #334155;">Financial Expenditure Proof</td><td style="padding:8px 12px;border:1px solid #334155;">Under Review</td><td style="padding:8px 12px;border:1px solid #334155;">2026-12-01</td></tr></tbody></table></div><p><br/></p>`
);
};
const insertPlatformLogo = () => {
insertMarkdown(
`![PFM Platform Logo](${PFM_LOGO_DATA_URI})\n\n`
insertHtmlBlock(
`<p><img src="${PFM_LOGO_DATA_URI}" alt="PFM Platform Logo" width="48" height="48" style="display:inline-block;border-radius:10px;margin:6px 0;" /></p><p><br/></p>`
);
};
const hasMeaningfulContent = (html: string) => {
if (!html) return false;
if (stripMarkdown(html).trim().length > 0) return true;
return /<(img|table|hr)\b/i.test(html);
};
const handleSaveDraft = () => {
if (!subject.trim() && !message.trim()) {
const currentHtml = editorRef.current ? editorRef.current.innerHTML : message;
if (!subject.trim() && !hasMeaningfulContent(currentHtml)) {
setError('Please add a subject or message to save as draft');
return;
}
@@ -157,18 +200,21 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
recipientEmail: firstUser?.email || '',
recipientName: firstUser?.name || '',
subject: subject.trim() || 'Untitled Draft',
message: message.trim(),
message: currentHtml.trim(),
platform,
});
setSuccessNotice('Draft saved successfully in Drafts folder');
setTimeout(() => {
onClose();
}, 800);
}, 600);
};
const handleSend = async (e: React.FormEvent) => {
e.preventDefault();
const currentHtml = editorRef.current ? editorRef.current.innerHTML : message;
if (selectedUsers.length === 0) {
setError('Please select a recipient from the tenant directory');
return;
@@ -179,7 +225,7 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
return;
}
if (!message.trim()) {
if (!hasMeaningfulContent(currentHtml)) {
setError('Please enter a message body');
return;
}
@@ -188,18 +234,15 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
try {
const recipient = selectedUsers[0];
// Format payload according to chosen platform:
// InApp messages are saved in markdown format.
// Email notifications are transformed from markdown to branded executive HTML before saving.
const finalMessage =
platform === NotificationPlatform.Email
? renderMarkdownToHtml(message.trim(), {
? renderMarkdownToHtml(currentHtml.trim(), {
applyBranding: true,
emailFormat: true,
subject: subject.trim(),
tenantName,
})
: message.trim();
: currentHtml.trim();
await sendMessage({
recipientId: recipient.id,
@@ -207,13 +250,14 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
recipientName: recipient.name,
subject: subject.trim(),
message: finalMessage,
platform,
draftId: draftToEdit?.id,
});
setSuccessNotice(
platform === NotificationPlatform.Email
? 'Email notification rendered to HTML and dispatched!'
: 'In-App Markdown message sent successfully!'
? 'Email notification dispatched!'
: 'In-App message sent successfully!'
);
setTimeout(() => {
onClose();
@@ -239,7 +283,9 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
subject: subject || 'Notice Preview',
tenantName,
})
: parseMarkdownToHtml(message);
: message;
const plainCharCount = stripMarkdown(message).length;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/85 backdrop-blur-md animate-in fade-in duration-150">
@@ -258,11 +304,11 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
: 'Compose New Communication'}
</h3>
<span className="text-[10px] font-semibold text-blue-300 bg-blue-500/15 border border-blue-500/30 px-2 py-0.5 rounded-full">
Rich Markdown Engine
Rich Text Composer
</span>
</div>
<p className="text-[11px] text-slate-400">
Author in Markdown with live preview, SVG branding, and dual InApp / Email delivery
Type and format your message using the toolbar for In-App or branded Email delivery
</p>
</div>
</div>
@@ -312,7 +358,7 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
}`}
>
<Smartphone className="w-3.5 h-3.5" />
<span>In-App (Markdown)</span>
<span>In-App</span>
</button>
<button
@@ -325,7 +371,7 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
}`}
>
<Mail className="w-3.5 h-3.5" />
<span>Email (HTML + Brand)</span>
<span>Email</span>
</button>
</div>
</div>
@@ -335,12 +381,12 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
{platform === NotificationPlatform.InApp ? (
<span className="text-blue-300 flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-blue-400 animate-pulse" />
Saved directly as <strong>Markdown</strong>. Delivered instantly to the recipient's InApp Mailbox & Notification Bell.
Delivered instantly to the recipient's <strong>In-App</strong> Mailbox & Notification Bell.
</span>
) : (
<span className="text-emerald-300 flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
Composed in Markdown, transformed into <strong>Branded Executive HTML</strong> with embedded SVG logo before dispatching.
Formatted with <strong>PFM Executive Branding</strong> and embedded logo before dispatching via email.
</span>
)}
</div>
@@ -373,14 +419,14 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
/>
</div>
{/* Markdown Editor / Preview Tabs */}
{/* Rich Text Editor / Branded Preview Tabs */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-xs font-semibold text-slate-300">
Message Content <span className="text-rose-400">*</span>
</label>
{/* Write vs Preview Tabs */}
{/* Compose vs Email Preview Tabs */}
<div className="flex items-center rounded-lg bg-slate-900 p-0.5 border border-slate-700/80">
<button
type="button"
@@ -392,11 +438,14 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
}`}
>
<Edit3 className="w-3 h-3" />
<span>Write Markdown</span>
<span>Compose</span>
</button>
<button
type="button"
onClick={() => setActiveTab('preview')}
onClick={() => {
syncEditorContent();
setActiveTab('preview');
}}
className={`flex items-center gap-1.5 px-3 py-1 rounded-md text-xs font-medium transition cursor-pointer ${
activeTab === 'preview'
? 'bg-blue-600 text-white font-semibold shadow-xs'
@@ -404,37 +453,48 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
}`}
>
<Eye className="w-3 h-3" />
<span>Preview Rendered</span>
<span>Preview Delivery</span>
</button>
</div>
</div>
{/* Markdown Toolbar (Only when activeTab === 'write') */}
{activeTab === 'write' ? (
<div className="rounded-2xl border border-slate-700/80 bg-slate-900/90 overflow-hidden shadow-inner focus-within:border-blue-500 transition">
{/* Toolbar */}
{/* WYSIWYG Formatting Toolbar */}
<div className="px-3 py-2 border-b border-slate-800 bg-slate-950/70 flex flex-wrap items-center gap-1">
<button
type="button"
onClick={() => insertMarkdown('**', '**', 'bold text')}
className="p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition cursor-pointer"
title="Bold (**text**)"
onMouseDown={e => e.preventDefault()}
onClick={() => applyFormat('bold')}
className="p-1.5 rounded-lg text-slate-300 hover:text-white hover:bg-slate-800 transition cursor-pointer"
title="Bold"
>
<Bold className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => insertMarkdown('*', '*', 'italic text')}
className="p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition cursor-pointer"
title="Italic (*text*)"
onMouseDown={e => e.preventDefault()}
onClick={() => applyFormat('italic')}
className="p-1.5 rounded-lg text-slate-300 hover:text-white hover:bg-slate-800 transition cursor-pointer"
title="Italic"
>
<Italic className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => insertMarkdown('### ', '', 'Heading Title')}
className="p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition cursor-pointer"
title="Heading (### )"
onMouseDown={e => e.preventDefault()}
onClick={() => applyFormat('underline')}
className="p-1.5 rounded-lg text-slate-300 hover:text-white hover:bg-slate-800 transition cursor-pointer"
title="Underline"
>
<Underline className="w-3.5 h-3.5" />
</button>
<button
type="button"
onMouseDown={e => e.preventDefault()}
onClick={() => applyFormat('formatBlock', 'H3')}
className="p-1.5 rounded-lg text-slate-300 hover:text-white hover:bg-slate-800 transition cursor-pointer"
title="Heading"
>
<Heading2 className="w-3.5 h-3.5" />
</button>
@@ -443,25 +503,28 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
<button
type="button"
onClick={() => insertMarkdown('- ', '', 'List item')}
className="p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition cursor-pointer"
title="Bullet List (- )"
onMouseDown={e => e.preventDefault()}
onClick={() => applyFormat('insertUnorderedList')}
className="p-1.5 rounded-lg text-slate-300 hover:text-white hover:bg-slate-800 transition cursor-pointer"
title="Bullet List"
>
<List className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => insertMarkdown('1. ', '', 'First item')}
className="p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition cursor-pointer"
title="Numbered List (1. )"
onMouseDown={e => e.preventDefault()}
onClick={() => applyFormat('insertOrderedList')}
className="p-1.5 rounded-lg text-slate-300 hover:text-white hover:bg-slate-800 transition cursor-pointer"
title="Numbered List"
>
<ListOrdered className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => insertMarkdown('> ', '', 'Quoted text')}
className="p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition cursor-pointer"
title="Blockquote (> )"
onMouseDown={e => e.preventDefault()}
onClick={() => applyFormat('formatBlock', 'BLOCKQUOTE')}
className="p-1.5 rounded-lg text-slate-300 hover:text-white hover:bg-slate-800 transition cursor-pointer"
title="Quote Block"
>
<Quote className="w-3.5 h-3.5" />
</button>
@@ -470,25 +533,19 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
<button
type="button"
onMouseDown={e => e.preventDefault()}
onClick={insertMilestoneTable}
className="p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition cursor-pointer"
className="p-1.5 rounded-lg text-slate-300 hover:text-white hover:bg-slate-800 transition cursor-pointer"
title="Insert Deliverable Table"
>
<Table className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => insertMarkdown('```\n', '\n```', 'code block')}
className="p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition cursor-pointer"
title="Code Block (```)"
>
<Code className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => insertMarkdown('\n---\n')}
className="p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition cursor-pointer"
title="Horizontal Rule (---)"
onMouseDown={e => e.preventDefault()}
onClick={() => applyFormat('insertHorizontalRule')}
className="p-1.5 rounded-lg text-slate-300 hover:text-white hover:bg-slate-800 transition cursor-pointer"
title="Insert Divider Line"
>
<Minus className="w-3.5 h-3.5" />
</button>
@@ -498,6 +555,7 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
{/* Platform Brand Insertions */}
<button
type="button"
onMouseDown={e => e.preventDefault()}
onClick={insertBrandedCallout}
className="flex items-center gap-1 px-2 py-1 rounded-lg bg-blue-500/10 hover:bg-blue-500/20 text-blue-300 text-[11px] font-medium border border-blue-500/25 transition cursor-pointer"
title="Insert official PFM callout block"
@@ -508,46 +566,54 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
<button
type="button"
onMouseDown={e => e.preventDefault()}
onClick={insertPlatformLogo}
className="flex items-center gap-1 px-2 py-1 rounded-lg bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-300 text-[11px] font-medium border border-emerald-500/25 transition cursor-pointer"
title="Embed official SVG PFM logo into markdown"
title="Embed official PFM logo"
>
<BrandLogo size={14} showGlow={false} animateOnHover={false} />
<span>Embed Logo</span>
</button>
</div>
{/* Textarea */}
<textarea
ref={textareaRef}
rows={8}
value={message}
onChange={e => setMessage(e.target.value)}
placeholder="Compose your message in Markdown... Use formatting buttons above for headings, bullet points, deliverable tables, and official PFM notices."
className="w-full p-4 bg-transparent text-slate-100 placeholder-slate-400 text-xs focus:outline-none transition resize-y leading-relaxed font-mono"
/>
{/* Rich Text WYSIWYG Surface */}
<div className="relative">
{!hasMeaningfulContent(message) && (
<div className="pointer-events-none absolute top-4 left-4 right-4 text-slate-500 text-xs select-none">
Type your message here and highlight text or use the toolbar above to format headings, bullet points, tables, and official notices...
</div>
)}
<div
ref={editorRef}
contentEditable
suppressContentEditableWarning
onInput={syncEditorContent}
onBlur={syncEditorContent}
className="w-full min-h-[200px] max-h-[320px] overflow-y-auto p-4 bg-transparent text-slate-100 text-xs focus:outline-none leading-relaxed font-sans [&_h1]:text-lg [&_h1]:font-bold [&_h1]:text-white [&_h1]:my-2 [&_h2]:text-base [&_h2]:font-bold [&_h2]:text-white [&_h2]:my-2 [&_h3]:text-sm [&_h3]:font-bold [&_h3]:text-white [&_h3]:my-1.5 [&_ul]:list-disc [&_ul]:pl-5 [&_ul]:my-1.5 [&_ol]:list-decimal [&_ol]:pl-5 [&_ol]:my-1.5 [&_blockquote]:border-l-4 [&_blockquote]:border-blue-500 [&_blockquote]:pl-3 [&_blockquote]:italic [&_blockquote]:text-slate-300 [&_blockquote]:my-2 [&_hr]:border-slate-700 [&_hr]:my-3"
/>
</div>
</div>
) : (
/* Live Fully-Rendered Markdown / HTML Preview */
/* Live Fully-Rendered Delivery Preview */
<div className="rounded-2xl border border-slate-700/80 bg-slate-950/80 p-5 overflow-y-auto max-h-72 min-h-56">
<div className="mb-3 pb-2 border-b border-slate-800 flex items-center justify-between text-[11px] text-slate-400">
<span className="flex items-center gap-1.5 text-blue-300 font-semibold">
<ShieldCheck className="w-3.5 h-3.5 text-blue-400" />
Live Rendered Preview ({platform === NotificationPlatform.Email ? 'HTML Email' : 'InApp Markdown'})
Delivery Preview ({platform === NotificationPlatform.Email ? 'Email' : 'In-App'})
</span>
<span className="text-[10px] text-slate-400 font-mono">
Subject: {subject || 'Untitled'}
</span>
</div>
{message.trim() ? (
{hasMeaningfulContent(message) ? (
<div
className="prose prose-invert max-w-none text-xs leading-relaxed"
className="prose prose-invert max-w-none text-xs leading-relaxed [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5 [&_blockquote]:border-l-4 [&_blockquote]:border-blue-500 [&_blockquote]:pl-3 [&_blockquote]:italic"
dangerouslySetInnerHTML={{ __html: livePreviewHtml }}
/>
) : (
<div className="py-12 text-center text-slate-400 text-xs italic">
Type your markdown message in the Write tab to see it live-rendered here.
Type your message in the Compose tab to preview how it will appear upon delivery.
</div>
)}
</div>
@@ -560,7 +626,7 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
<span>Tenant Partition:</span>
<strong className="text-slate-300">{tenantName}</strong>
</span>
<span className="font-mono">{message.length} characters</span>
<span className="font-mono">{plainCharCount} characters</span>
</div>
{/* Modal Footer Action Buttons */}
@@ -608,7 +674,7 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
)}
<span>
{platform === NotificationPlatform.Email
? 'Dispatch Email (HTML)'
? 'Dispatch Email'
: 'Send In-App Message'}
</span>
</button>
@@ -1,14 +1,11 @@
import React, { useState, useMemo } from 'react';
import {
Code,
Eye,
Copy,
Check,
ShieldCheck,
ExternalLink,
} from 'lucide-react';
import { BrandLogo } from '../ui/BrandLogo';
import { parseMarkdownToHtml, isHtmlContent } from '../../lib/markdownUtils';
import { parseMarkdownToHtml, isHtmlContent, stripMarkdown } from '../../lib/markdownUtils';
interface MarkdownViewerProps {
content: string;
@@ -19,11 +16,9 @@ interface MarkdownViewerProps {
export const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
content,
subject,
showBrandingHeader = true,
className = '',
}) => {
const [viewMode, setViewMode] = useState<'rendered' | 'raw'>('rendered');
const [copied, setCopied] = useState(false);
const isHtml = useMemo(() => isHtmlContent(content), [content]);
@@ -35,7 +30,7 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
}, [content, isHtml]);
const handleCopy = () => {
navigator.clipboard.writeText(content);
navigator.clipboard.writeText(stripMarkdown(content) || content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
@@ -48,49 +43,21 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
{showBrandingHeader && (
<div className="flex items-center gap-1.5 text-slate-300 font-semibold pr-2 border-r border-slate-700/60">
<BrandLogo size={18} showGlow={false} animateOnHover={false} />
<span className="text-[11px] tracking-tight">PFM Markdown Engine</span>
<span className="text-[11px] tracking-tight">PFM Communications</span>
</div>
)}
<span className="text-[10px] text-blue-400 font-mono">
{isHtml ? 'Format: HTML Email' : 'Format: GitHub Flavored Markdown'}
In-App Message
</span>
</div>
<div className="flex items-center gap-2">
{/* Toggle View Mode */}
<div className="flex items-center rounded-lg bg-slate-950/70 p-0.5 border border-slate-800">
<button
type="button"
onClick={() => setViewMode('rendered')}
className={`flex items-center gap-1 px-2 py-1 rounded-md text-[11px] font-medium transition cursor-pointer ${
viewMode === 'rendered'
? 'bg-blue-600/30 text-white font-semibold shadow-xs'
: 'text-slate-400 hover:text-white'
}`}
>
<Eye className="w-3 h-3" />
<span>Rendered</span>
</button>
<button
type="button"
onClick={() => setViewMode('raw')}
className={`flex items-center gap-1 px-2 py-1 rounded-md text-[11px] font-medium transition cursor-pointer ${
viewMode === 'raw'
? 'bg-blue-600/30 text-white font-semibold shadow-xs'
: 'text-slate-400 hover:text-white'
}`}
>
<Code className="w-3 h-3" />
<span>Raw Source</span>
</button>
</div>
{/* Copy Button */}
<button
type="button"
onClick={handleCopy}
className="flex items-center gap-1 px-2 py-1 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white text-[11px] font-medium transition cursor-pointer border border-slate-700/60"
title="Copy content to clipboard"
title="Copy message text to clipboard"
>
{copied ? (
<>
@@ -108,25 +75,12 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
</div>
{/* Content Rendering */}
{viewMode === 'rendered' ? (
<div className="p-6 overflow-x-auto text-sm leading-relaxed text-slate-200 font-sans">
{isHtml ? (
<div
className="prose prose-invert max-w-none"
dangerouslySetInnerHTML={{ __html: renderedHtml }}
/>
) : (
<div
className="prose prose-invert max-w-none [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_a]:text-blue-400 [&_a:hover]:underline [&_table]:border-collapse [&_th]:border [&_th]:border-slate-700 [&_th]:p-2 [&_td]:border [&_td]:border-slate-800 [&_td]:p-2 [&_blockquote]:border-l-4 [&_blockquote]:border-blue-500 [&_blockquote]:pl-4 [&_blockquote]:italic [&_code]:bg-slate-800 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-blue-300"
dangerouslySetInnerHTML={{ __html: renderedHtml }}
/>
)}
</div>
) : (
<pre className="p-5 overflow-x-auto font-mono text-xs text-slate-300 bg-[#070b13] select-all whitespace-pre-wrap leading-relaxed">
{content}
</pre>
)}
<div className="p-6 overflow-x-auto text-sm leading-relaxed text-slate-200 font-sans">
<div
className="prose prose-invert max-w-none [&_h1]:text-white [&_h1]:text-xl [&_h1]:font-bold [&_h1]:mb-3 [&_h2]:text-white [&_h2]:text-lg [&_h2]:font-bold [&_h2]:mb-2 [&_h3]:text-white [&_h3]:text-base [&_h3]:font-bold [&_h3]:mb-2 [&_h4]:text-white [&_h4]:font-semibold [&_a]:text-blue-400 [&_a:hover]:underline [&_ul]:list-disc [&_ul]:pl-5 [&_ul]:my-2 [&_ol]:list-decimal [&_ol]:pl-5 [&_ol]:my-2 [&_table]:w-full [&_table]:border-collapse [&_th]:border [&_th]:border-slate-700 [&_th]:bg-slate-800/80 [&_th]:p-2 [&_td]:border [&_td]:border-slate-800 [&_td]:p-2 [&_blockquote]:border-l-4 [&_blockquote]:border-blue-500 [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:my-2 [&_code]:bg-slate-800 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-blue-300"
dangerouslySetInnerHTML={{ __html: renderedHtml }}
/>
</div>
{/* Branded Footer Stamp */}
{showBrandingHeader && (
@@ -136,7 +90,7 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
<span>Verified PFM Secure Platform Formatting</span>
</div>
<span className="font-mono text-[10px] text-slate-400">
{content.length} characters
{stripMarkdown(content).length} characters
</span>
</div>
)}