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

158 lines
6.6 KiB
TypeScript

import { useEffect, useRef, type ReactNode } from "react";
import { ArrowBigLeft, ArrowBigLeftDash, ArrowBigRight, ArrowBigRightDash } from "lucide-react";
import { ActionToolbar, Button, Dialog, 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;
actions?: ReactNode;
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,
actions,
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 }));
const contentRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
const dialogPanel = contentRef.current?.closest<HTMLElement>("[data-dialog-stack-state]");
if (dialogPanel?.dataset.dialogStackState !== "topmost") return;
if (isEditableTarget(event.target)) 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]);
return (
<Dialog
open
title={title}
onClose={onClose}
closeLabel={closeLabel}
closeOnBackdrop={false}
backdropClassName="overlay-backdrop message-preview-backdrop"
className="modal-panel template-preview-modal message-preview-modal"
headerClassName="modal-header"
bodyClassName="modal-body"
footerClassName="modal-footer"
footer={<>
{actions && <div className="button-row compact-actions">{actions}</div>}
<Button variant="primary" onClick={onClose}>{closeLabel}</Button>
</>}
>
<div ref={contentRef} className="message-preview-content">
{(recipientLabel || recipientNote || navigation) &&
<ActionToolbar 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>
}
</ActionToolbar>
}
<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>
</Dialog>);
}
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;