Files
govoplan-campaign/webui/src/features/campaigns/components/MessagePreviewOverlay.tsx

166 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ArrowBigLeft, ArrowBigLeftDash, ArrowBigRight, ArrowBigRightDash } from "lucide-react";
import Button from "../../../components/Button";
export type MessagePreviewAttachment = {
filename?: string | null;
label?: string | null;
detail?: string | null;
contentType?: string | null;
sizeBytes?: number | null;
archiveGroup?: string | null;
archiveLabel?: string | null;
};
export type MessagePreviewMetaItem = {
label: string;
value: React.ReactNode;
};
export type MessagePreviewNavigation = {
index: number;
total: number;
onFirst: () => void;
onPrevious: () => void;
onNext: () => void;
onLast: () => void;
};
export type MessagePreviewOverlayProps = {
title?: string;
subject?: string | null;
bodyMode?: "text" | "html";
text?: string | null;
html?: string | null;
recipientLabel?: React.ReactNode;
recipientNote?: React.ReactNode;
metaItems?: MessagePreviewMetaItem[];
attachments?: MessagePreviewAttachment[];
raw?: string | null;
rawLabel?: string;
navigation?: MessagePreviewNavigation;
closeLabel?: string;
onClose: () => void;
};
export default function MessagePreviewOverlay({
title = "Message preview",
subject,
bodyMode = "text",
text,
html,
recipientLabel,
recipientNote,
metaItems = [],
attachments = [],
raw,
rawLabel = "Raw MIME",
navigation,
closeLabel = "Close",
onClose
}: MessagePreviewOverlayProps) {
const shownSubject = subject?.trim() || "No subject";
const shownText = text?.trim() || "No plain-text body to preview.";
const shownHtml = html?.trim() || "<p>No HTML body to preview.</p>";
return (
<div className="overlay-backdrop" role="dialog" aria-modal="true" aria-labelledby="message-preview-title">
<div className="modal-panel template-preview-modal message-preview-modal">
<header className="modal-header">
<h2 id="message-preview-title">{title}</h2>
<button className="modal-close" onClick={onClose}>×</button>
</header>
<div className="modal-body">
{(recipientLabel || recipientNote || navigation) && (
<div className="template-preview-toolbar">
<div>
{recipientLabel && <strong>{recipientLabel}</strong>}
{recipientNote && <p className="muted small-note">{recipientNote}</p>}
</div>
{navigation && (
<div className="button-row compact-actions template-preview-nav" aria-label="Preview message navigation">
<button type="button" className="version-arrow" onClick={navigation.onFirst} disabled={navigation.index <= 0} title="First message" aria-label="First message"><ArrowBigLeftDash aria-hidden="true" /></button>
<button type="button" className="version-arrow" onClick={navigation.onPrevious} disabled={navigation.index <= 0} title="Previous message" aria-label="Previous message"><ArrowBigLeft aria-hidden="true" /></button>
<span className="template-preview-count">{navigation.index + 1} / {navigation.total}</span>
<button type="button" className="version-arrow" onClick={navigation.onNext} disabled={navigation.index >= navigation.total - 1} title="Next message" aria-label="Next message"><ArrowBigRight aria-hidden="true" /></button>
<button type="button" className="version-arrow" onClick={navigation.onLast} disabled={navigation.index >= navigation.total - 1} title="Last message" aria-label="Last message"><ArrowBigRightDash aria-hidden="true" /></button>
</div>
)}
</div>
)}
{metaItems.length > 0 && (
<div className="detail-grid message-preview-meta">
{metaItems.map((item) => (
<div key={item.label}><span className="muted small-note">{item.label}</span><strong>{item.value || "—"}</strong></div>
))}
</div>
)}
<div className="template-preview-box">
<h3>{shownSubject}</h3>
{bodyMode === "html" ? (
<iframe className="template-preview-frame" title="Rendered HTML body preview" sandbox="" srcDoc={shownHtml} />
) : (
<pre>{shownText}</pre>
)}
</div>
<MessagePreviewAttachmentBoxes attachments={attachments} />
{raw && (
<details className="message-preview-raw">
<summary>{rawLabel}</summary>
<pre className="mock-message-raw">{raw}</pre>
</details>
)}
</div>
<footer className="modal-footer"><Button variant="primary" onClick={onClose}>{closeLabel}</Button></footer>
</div>
</div>
);
}
function MessagePreviewAttachmentBoxes({ attachments }: { attachments: MessagePreviewAttachment[] }) {
const direct = attachments.filter((attachment) => !attachment.archiveGroup);
const archiveGroups = new Map<string, MessagePreviewAttachment[]>();
for (const attachment of attachments) {
if (!attachment.archiveGroup) continue;
const group = archiveGroups.get(attachment.archiveGroup) ?? [];
group.push(attachment);
archiveGroups.set(attachment.archiveGroup, group);
}
function attachmentChip(attachment: MessagePreviewAttachment, index: number) {
const filename = attachment.filename?.trim() || attachment.label?.trim() || "Unnamed attachment";
const details = [attachment.detail, attachment.contentType, attachment.sizeBytes ? `${attachment.sizeBytes} bytes` : ""].filter(Boolean).join(" · ");
return (
<div className="attachment-file-chip" key={`${filename}:${index}`}>
<strong>{filename}</strong>
{details && <span>{details}</span>}
</div>
);
}
return (
<div className="template-preview-attachments message-preview-attachments">
<h3>Attachments</h3>
{attachments.length === 0 ? (
<p className="muted small-note">No attachments are effective for this message.</p>
) : (
<div className="message-preview-attachment-layout">
{direct.length > 0 && <div className="attachment-chip-grid">{direct.map(attachmentChip)}</div>}
{[...archiveGroups.entries()].map(([groupName, items]) => (
<section className="attachment-zip-group" key={groupName}>
<header>
<strong>{items[0]?.archiveLabel || groupName}</strong>
<span>{items.length} file{items.length === 1 ? "" : "s"} inside ZIP</span>
</header>
<div className="attachment-chip-grid">{items.map(attachmentChip)}</div>
</section>
))}
</div>
)}
</div>
);
}