feat(email): render shop logo inside WYSIWYG editor (cid swap)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useEditor, EditorContent } from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import Link from "@tiptap/extension-link";
|
||||
import Image from "@tiptap/extension-image";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface RichTextEditorProps {
|
||||
@@ -16,6 +17,13 @@ interface RichTextEditorProps {
|
||||
variables?: { token: string; label?: string }[];
|
||||
/** Min editor height in px. */
|
||||
minHeight?: number;
|
||||
/**
|
||||
* If set, occurrences of `cid:invoice-logo` (in src attributes) are
|
||||
* displayed using this data/HTTPS URL inside the editor, but mirrored
|
||||
* back to `cid:invoice-logo` in the submitted hidden field. This keeps
|
||||
* the stored template portable while showing the real logo while editing.
|
||||
*/
|
||||
logoDataUrl?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,6 +40,7 @@ export function RichTextEditor({
|
||||
helpText,
|
||||
variables = [],
|
||||
minHeight = 200,
|
||||
logoDataUrl = null,
|
||||
}: RichTextEditorProps) {
|
||||
// TipTap calls into the DOM on init; defer mounting until after hydration
|
||||
// so SSR markup matches the initial client render.
|
||||
@@ -46,8 +55,9 @@ export function RichTextEditor({
|
||||
heading: { levels: [2, 3] },
|
||||
}),
|
||||
Link.configure({ openOnClick: false }),
|
||||
Image.configure({ inline: false, allowBase64: true }),
|
||||
],
|
||||
content: defaultValue || "<p></p>",
|
||||
content: swapCidToLogo(defaultValue || "<p></p>", logoDataUrl),
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: "wysiwyg-editor",
|
||||
@@ -59,7 +69,7 @@ export function RichTextEditor({
|
||||
// Keep editor disposed cleanly on unmount.
|
||||
useEffect(() => () => editor?.destroy(), [editor]);
|
||||
|
||||
const html = editor?.getHTML() ?? defaultValue;
|
||||
const html = swapLogoToCid(editor?.getHTML() ?? defaultValue, logoDataUrl);
|
||||
|
||||
if (!mounted) {
|
||||
// Server / pre-hydration fallback: a textarea so the value is still
|
||||
@@ -251,3 +261,29 @@ function ToolbarButton({
|
||||
function Sep() {
|
||||
return <span aria-hidden style={{ width: 1, background: "#c9cccf", margin: "2px 4px" }} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces `src="cid:invoice-logo"` with the supplied URL so the editor
|
||||
* can display the actual logo. Done as a string replace because TipTap
|
||||
* doesn't render `cid:` URLs.
|
||||
*/
|
||||
function swapCidToLogo(html: string, logoUrl: string | null): string {
|
||||
if (!logoUrl) return html;
|
||||
const escaped = logoUrl.replace(/"/g, """);
|
||||
return html
|
||||
.replace(/src="cid:invoice-logo"/g, `src="${escaped}"`)
|
||||
.replace(/src='cid:invoice-logo'/g, `src='${escaped}'`);
|
||||
}
|
||||
|
||||
/** Inverse of swapCidToLogo — ensures the cid token is what we store. */
|
||||
function swapLogoToCid(html: string, logoUrl: string | null): string {
|
||||
if (!logoUrl) return html;
|
||||
const escaped = logoUrl.replace(/"/g, """);
|
||||
return html
|
||||
.replace(new RegExp(`src="${escapeRegExp(escaped)}"`, "g"), 'src="cid:invoice-logo"')
|
||||
.replace(new RegExp(`src='${escapeRegExp(escaped)}'`, "g"), "src='cid:invoice-logo'");
|
||||
}
|
||||
|
||||
function escapeRegExp(s: string): string {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user