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

149 lines
6.3 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 { useEffect, type ReactNode } from "react";
import { ArrowBigLeft, ArrowBigLeftDash, ArrowBigRight, ArrowBigRightDash } from "lucide-react";
import { Button, MessageDisplayPanel, type MessageDisplayAttachment } from "@govoplan/core-webui";
// Campaign review/template/mock previews need recipient navigation, review notes,
// raw MIME inspection and attachment grouping. Generic mailbox reading uses
// core MessageDisplayPanel instead.
export type CampaignMessagePreviewAttachment = MessageDisplayAttachment & {
label?: string | null;
};
export type CampaignMessagePreviewMetaItem = {
label: string;
value: ReactNode;
};
export type CampaignMessagePreviewNavigation = {
index: number;
total: number;
onFirst: () => void;
onPrevious: () => void;
onNext: () => void;
onLast: () => void;
};
export type CampaignMessagePreviewOverlayProps = {
title?: string;
subject?: string | null;
bodyMode?: "text" | "html";
text?: string | null;
html?: string | null;
recipientLabel?: ReactNode;
recipientNote?: ReactNode;
metaItems?: CampaignMessagePreviewMetaItem[];
attachments?: CampaignMessagePreviewAttachment[];
raw?: string | null;
rawLabel?: string;
navigation?: CampaignMessagePreviewNavigation;
closeLabel?: string;
onClose: () => void;
};
export default function CampaignMessagePreviewOverlay({
title = "i18n:govoplan-campaign.message_preview.58de1450",
subject,
bodyMode = "text",
text,
html,
recipientLabel,
recipientNote,
metaItems = [],
attachments = [],
raw,
rawLabel = "i18n:govoplan-campaign.raw_mime.82c612d4",
navigation,
closeLabel = "i18n:govoplan-campaign.close.bbfa773e",
onClose
}: CampaignMessagePreviewOverlayProps) {
const shownSubject = subject?.trim() || "i18n:govoplan-campaign.no_subject.7b4e8035";
const fields = metaItems.map((item) => ({ label: item.label, value: item.value }));
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (isEditableTarget(event.target)) return;
if (event.key === "Escape") {
event.preventDefault();
onClose();
return;
}
if (!navigation) return;
if (event.key === "ArrowLeft") {
event.preventDefault();
if (navigation.index > 0) navigation.onPrevious();
} else if (event.key === "ArrowRight") {
event.preventDefault();
if (navigation.index < navigation.total - 1) navigation.onNext();
} else if (event.key === "Home") {
event.preventDefault();
if (navigation.index > 0) navigation.onFirst();
} else if (event.key === "End") {
event.preventDefault();
if (navigation.index < navigation.total - 1) navigation.onLast();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [navigation, onClose]);
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="i18n:govoplan-campaign.preview_message_navigation.d28a8dc0">
<button type="button" className="version-arrow" onClick={navigation.onFirst} disabled={navigation.index <= 0} title="i18n:govoplan-campaign.first_message.ffc124fd" aria-label="i18n:govoplan-campaign.first_message.ffc124fd"><ArrowBigLeftDash aria-hidden="true" /></button>
<button type="button" className="version-arrow" onClick={navigation.onPrevious} disabled={navigation.index <= 0} title="i18n:govoplan-campaign.previous_message.93261bd8" aria-label="i18n:govoplan-campaign.previous_message.93261bd8"><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="i18n:govoplan-campaign.next_message.e3960a5d" aria-label="i18n:govoplan-campaign.next_message.e3960a5d"><ArrowBigRight aria-hidden="true" /></button>
<button type="button" className="version-arrow" onClick={navigation.onLast} disabled={navigation.index >= navigation.total - 1} title="i18n:govoplan-campaign.last_message.83741110" aria-label="i18n:govoplan-campaign.last_message.83741110"><ArrowBigRightDash aria-hidden="true" /></button>
</div>
}
</div>
}
<MessageDisplayPanel
title={shownSubject}
fields={fields}
bodyText={text}
bodyHtml={html}
preferredBodyMode={bodyMode}
deriveTextFromHtml={false}
attachments={attachments}
emptyText="i18n:govoplan-campaign.no_message_content_is_available.54e8e7e6" />
{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 isEditableTarget(target: EventTarget | null): boolean {
if (!(target instanceof HTMLElement)) return false;
return target.isContentEditable || ["INPUT", "SELECT", "TEXTAREA"].includes(target.tagName);
}
export type MessagePreviewAttachment = CampaignMessagePreviewAttachment;
export type MessagePreviewMetaItem = CampaignMessagePreviewMetaItem;
export type MessagePreviewNavigation = CampaignMessagePreviewNavigation;
export type MessagePreviewOverlayProps = CampaignMessagePreviewOverlayProps;