Dealer_Onboard_Frontend/src/features/master/components/EmailTemplateBodyEditor.tsx

145 lines
5.5 KiB
TypeScript

import * as React from 'react';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
import { Textarea } from '@/components/ui/textarea';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Info } from 'lucide-react';
import { cn } from '@/components/ui/utils';
const quillModules = {
toolbar: [
[{ header: [1, 2, 3, false] }],
['bold', 'italic', 'underline'],
[{ list: 'ordered' }, { list: 'bullet' }],
['link'],
['blockquote'],
['clean'],
],
};
const quillFormats = ['header', 'bold', 'italic', 'underline', 'list', 'bullet', 'link', 'blockquote'];
/** Handlebars helpers/partials or full HTML docs must be edited as source — rich editors strip or corrupt them. */
export function requiresAdvancedHtmlEditing(html: string): boolean {
const s = html || '';
return (
/\{\{\s*[>#\/]/.test(s) ||
/\{\{>\s*\w+/.test(s) ||
/<!DOCTYPE/i.test(s) ||
/<html[\s>]/i.test(s) ||
/<\/html>/i.test(s) ||
/<style\b/i.test(s) ||
/<head[\s>]/i.test(s)
);
}
export type EmailTemplateBodyEditorHandle = {
insertPlaceholder: (placeholder: string) => void;
};
type Props = {
value: string;
onChange: (html: string) => void;
/** visual | html */
tab: string;
onTabChange: (v: string) => void;
textareaRef: React.RefObject<HTMLTextAreaElement>;
};
export const EmailTemplateBodyEditor = React.forwardRef<EmailTemplateBodyEditorHandle, Props>(
({ value, onChange, tab, onTabChange, textareaRef }, ref) => {
const quillRef = React.useRef<ReactQuill>(null);
const advanced = React.useMemo(() => requiresAdvancedHtmlEditing(value), [value]);
React.useImperativeHandle(ref, () => ({
insertPlaceholder: (placeholder: string) => {
const token = `{{${placeholder}}}`;
if (tab === 'html' && textareaRef.current) {
const el = textareaRef.current;
const start = el.selectionStart ?? 0;
const end = el.selectionEnd ?? 0;
const next = value.slice(0, start) + token + value.slice(end);
onChange(next);
requestAnimationFrame(() => {
el.focus();
const pos = start + token.length;
el.setSelectionRange(pos, pos);
});
return;
}
const editor = quillRef.current?.getEditor?.();
if (!editor) return;
const range = editor.getSelection(true);
const idx = range?.index ?? editor.getLength();
editor.insertText(idx, token, 'user');
},
}));
return (
<div className="flex flex-col gap-2 flex-1 min-h-0">
<Tabs value={tab} onValueChange={onTabChange} className="flex flex-col flex-1 gap-2 min-h-0">
<TabsList className="w-fit">
<TabsTrigger value="visual" disabled={advanced} className="text-xs">
Rich text
</TabsTrigger>
<TabsTrigger value="html" className="text-xs font-mono">
HTML source
</TabsTrigger>
</TabsList>
{advanced && (
<Alert className="border-amber-200 bg-amber-50 py-2">
<Info className="h-4 w-4 text-amber-700" />
<AlertDescription className="text-[11px] text-amber-900">
This template uses layout partials (<code className="font-mono">{'{{> ...}}'}</code>), block helpers, or a full HTML
document. Edit it in <strong>HTML source</strong> so nothing is stripped. Use placeholders on the left to insert
fields safely.
</AlertDescription>
</Alert>
)}
<TabsContent value="visual" className="flex-1 flex flex-col gap-2 mt-2 min-h-[420px] data-[state=inactive]:hidden">
<div
className={cn(
'email-quill rounded-md border border-slate-200 bg-white overflow-hidden flex flex-col flex-1',
'[&_.ql-toolbar]:border-slate-200 [&_.ql-toolbar]:bg-slate-50',
'[&_.ql-container]:border-0 [&_.ql-editor]:min-h-[380px] [&_.ql-editor]:text-sm [&_.ql-editor]:text-slate-900'
)}
>
<ReactQuill
ref={quillRef}
theme="snow"
value={value || ''}
onChange={onChange}
modules={quillModules}
formats={quillFormats}
/>
</div>
<p className="text-[10px] text-slate-500">
Use headings, lists, and links. Insert dynamic fields from <strong>Available Placeholders</strong> on the left.
</p>
</TabsContent>
<TabsContent value="html" className="flex-1 flex flex-col mt-2 min-h-[420px] data-[state=inactive]:hidden">
<Textarea
ref={textareaRef}
placeholder="Raw HTML / Handlebars — use for partials {{> ...}} or full layouts"
className="flex-1 min-h-[420px] font-mono text-xs text-slate-900 bg-slate-900/5 focus:bg-white p-4 resize-y"
value={value || ''}
onChange={(e) => onChange(e.target.value)}
spellCheck={false}
/>
<p className="text-[10px] text-slate-500 mt-1">
Full control for <code className="font-mono">{'{{> email_header}}'}</code>, styles, and edge cases. Backend still
sanitizes on save.
</p>
</TabsContent>
</Tabs>
</div>
);
}
);
EmailTemplateBodyEditor.displayName = 'EmailTemplateBodyEditor';