Polish email template system with UX improvements
- Consolidated multiple preview canvases into single shared preview with "Simpan & Preview" button - Fixed double scrollbar issue in preview box by using fixed height container and scrolling=no - Added modular email components to Tiptap editor: * EmailButton with URL, text, and full-width options * OTPBox with monospace font and dashed border styling * EmailTable with brutalist styling and proper header support - Generated contextual initial email content for all template types: * Payment success with professional details table * Access granted with celebration styling and prominent CTA * Order created with clear next steps and status information * Payment reminder with urgent styling and warning alerts * Consulting scheduled with session details and preparation tips * Event reminder with high-energy countdown and call-to-action * Bootcamp progress with motivational progress tracking - Enhanced RichTextEditor toolbar with email component buttons and visual separators - Improved NotifikasiTab with streamlined preview workflow 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,11 +3,16 @@ import StarterKit from '@tiptap/starter-kit';
|
||||
import Link from '@tiptap/extension-link';
|
||||
import Image from '@tiptap/extension-image';
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
import { Node } from '@tiptap/core';
|
||||
import Table from '@tiptap/extension-table';
|
||||
import TableRow from '@tiptap/extension-table-row';
|
||||
import TableCell from '@tiptap/extension-table-cell';
|
||||
import TableHeader from '@tiptap/extension-table-header';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Bold, Italic, List, ListOrdered, Quote, Link as LinkIcon,
|
||||
Image as ImageIcon, Heading1, Heading2, Undo, Redo,
|
||||
Maximize2, Minimize2
|
||||
Maximize2, Minimize2, MousePointer, Square, Table as TableIcon
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
@@ -49,6 +54,247 @@ const ResizableImage = Image.extend({
|
||||
},
|
||||
});
|
||||
|
||||
// Custom Button extension for email templates
|
||||
const EmailButton = Node.create({
|
||||
name: 'emailButton',
|
||||
|
||||
group: 'block',
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
url: {
|
||||
default: '#',
|
||||
parseHTML: element => element.getAttribute('data-url') || '#',
|
||||
renderHTML: attributes => ({
|
||||
'data-url': attributes.url,
|
||||
}),
|
||||
},
|
||||
text: {
|
||||
default: 'Button',
|
||||
parseHTML: element => element.textContent || 'Button',
|
||||
renderHTML: attributes => ({}),
|
||||
},
|
||||
fullWidth: {
|
||||
default: false,
|
||||
parseHTML: element => element.classList.contains('btn-full'),
|
||||
renderHTML: attributes => ({
|
||||
class: attributes.fullWidth ? 'btn btn-full' : 'btn',
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'div[data-email-button]',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes, node }) {
|
||||
const { url, text, fullWidth } = node.attrs;
|
||||
return [
|
||||
'p',
|
||||
{ style: 'margin-top: 20px; text-align: center;' },
|
||||
[
|
||||
'a',
|
||||
{
|
||||
href: url,
|
||||
class: fullWidth ? 'btn btn-full' : 'btn',
|
||||
'data-email-button': '',
|
||||
style: `
|
||||
display: inline-block;
|
||||
background-color: #000;
|
||||
color: #FFF !important;
|
||||
padding: 14px 28px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none !important;
|
||||
font-size: 16px;
|
||||
border: 2px solid #000;
|
||||
box-shadow: 4px 4px 0px 0px #000000;
|
||||
margin: 10px 0;
|
||||
transition: all 0.1s;
|
||||
text-align: center;
|
||||
${fullWidth ? 'width: 100%; box-sizing: border-box;' : ''}
|
||||
`,
|
||||
},
|
||||
text || 'Button',
|
||||
],
|
||||
];
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ({ node, editor }) => {
|
||||
const dom = document.createElement('div');
|
||||
dom.style.cssText = 'margin: 10px 0; border: 2px dashed #007acc; padding: 8px; border-radius: 4px; background: #f0f9ff;';
|
||||
|
||||
const button = document.createElement('a');
|
||||
button.href = node.attrs.url;
|
||||
button.textContent = node.attrs.text;
|
||||
button.style.cssText = `
|
||||
display: inline-block;
|
||||
background-color: #000;
|
||||
color: #FFF;
|
||||
padding: 14px 28px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
border: 2px solid #000;
|
||||
box-shadow: 4px 4px 0px 0px #000000;
|
||||
cursor: pointer;
|
||||
${node.attrs.fullWidth ? 'width: 100%; text-align: center; box-sizing: border-box;' : ''}
|
||||
`;
|
||||
|
||||
dom.appendChild(button);
|
||||
|
||||
return {
|
||||
dom,
|
||||
destroy: () => {
|
||||
dom.remove();
|
||||
},
|
||||
};
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Custom OTP Box extension
|
||||
const OTPBox = Node.create({
|
||||
name: 'otpBox',
|
||||
|
||||
group: 'block',
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
code: {
|
||||
default: '123-456',
|
||||
parseHTML: element => element.getAttribute('data-code') || '123-456',
|
||||
renderHTML: attributes => ({
|
||||
'data-code': attributes.code,
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'div[data-otp-box]',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes, node }) {
|
||||
const { code } = node.attrs;
|
||||
return [
|
||||
'div',
|
||||
{
|
||||
'data-otp-box': '',
|
||||
style: `
|
||||
background-color: #F4F4F5;
|
||||
border: 2px dashed #000;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
letter-spacing: 5px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
`,
|
||||
},
|
||||
code,
|
||||
];
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ({ node, editor }) => {
|
||||
const dom = document.createElement('div');
|
||||
dom.style.cssText = 'margin: 10px 0; border: 2px dashed #007acc; padding: 8px; border-radius: 4px; background: #f0f9ff;';
|
||||
dom.innerHTML = `
|
||||
<div style="text-align: center; font-size: 12px; color: #007acc; margin-bottom: 4px;">OTP Box: ${node.attrs.code}</div>
|
||||
<div style="
|
||||
background-color: #F4F4F5;
|
||||
border: 2px dashed #000;
|
||||
padding: 20px;
|
||||
letter-spacing: 5px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
">${node.attrs.code}</div>
|
||||
`;
|
||||
|
||||
return {
|
||||
dom,
|
||||
destroy: () => {
|
||||
dom.remove();
|
||||
},
|
||||
};
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Custom Email Table extension
|
||||
const EmailTable = Table.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
style: {
|
||||
default: 'width: 100%; border: 2px solid #000; margin-bottom: 25px; border-collapse: collapse;',
|
||||
renderHTML: attributes => {
|
||||
return { style: attributes.style };
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const EmailTableRow = TableRow.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
style: {
|
||||
default: '',
|
||||
renderHTML: attributes => {
|
||||
return { style: attributes.style };
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const EmailTableCell = TableCell.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
style: {
|
||||
default: 'padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;',
|
||||
renderHTML: attributes => {
|
||||
return { style: attributes.style };
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const EmailTableHeader = TableHeader.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
style: {
|
||||
default: 'background-color: #000; color: #FFF; padding: 12px; text-align: left; font-size: 14px; text-transform: uppercase; font-weight: 700; border: 1px solid #000;',
|
||||
renderHTML: attributes => {
|
||||
return { style: attributes.style };
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten...', className }: RichTextEditorProps) {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [selectedImage, setSelectedImage] = useState<{ src: string; width?: number; height?: number } | null>(null);
|
||||
@@ -57,7 +303,12 @@ export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten.
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
StarterKit.configure({
|
||||
table: false,
|
||||
tableRow: false,
|
||||
tableCell: false,
|
||||
tableHeader: false,
|
||||
}),
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
HTMLAttributes: {
|
||||
@@ -69,6 +320,17 @@ export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten.
|
||||
class: 'max-w-full h-auto rounded-md cursor-pointer',
|
||||
},
|
||||
}),
|
||||
EmailButton,
|
||||
OTPBox,
|
||||
EmailTable.configure({
|
||||
resizable: true,
|
||||
HTMLAttributes: {
|
||||
class: 'email-table',
|
||||
},
|
||||
}),
|
||||
EmailTableRow,
|
||||
EmailTableCell,
|
||||
EmailTableHeader,
|
||||
Placeholder.configure({
|
||||
placeholder,
|
||||
}),
|
||||
@@ -110,6 +372,63 @@ export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten.
|
||||
}
|
||||
}, [editor]);
|
||||
|
||||
const addButton = useCallback(() => {
|
||||
if (!editor) return;
|
||||
const text = window.prompt('Teks Button:') || 'Button';
|
||||
const url = window.prompt('URL Button:') || '#';
|
||||
const fullWidth = window.confirm('Gunakan lebar penuh?');
|
||||
|
||||
editor.chain().focus().insertContent({
|
||||
type: 'emailButton',
|
||||
attrs: {
|
||||
text,
|
||||
url,
|
||||
fullWidth,
|
||||
},
|
||||
}).run();
|
||||
}, [editor]);
|
||||
|
||||
const addOTPBox = useCallback(() => {
|
||||
if (!editor) return;
|
||||
const code = window.prompt('Kode OTP (contoh: 123-456):') || '123-456';
|
||||
|
||||
editor.chain().focus().insertContent({
|
||||
type: 'otpBox',
|
||||
attrs: {
|
||||
code,
|
||||
},
|
||||
}).run();
|
||||
}, [editor]);
|
||||
|
||||
const addTable = useCallback(() => {
|
||||
if (!editor) return;
|
||||
const rows = parseInt(window.prompt('Jumlah baris:') || '3');
|
||||
const cols = parseInt(window.prompt('Jumlah kolom:') || '2');
|
||||
const hasHeader = window.confirm('Apakah table memiliki header?');
|
||||
|
||||
let tableHTML = '<table style="width: 100%; border: 2px solid #000; margin-bottom: 25px; border-collapse: collapse;">';
|
||||
|
||||
if (hasHeader) {
|
||||
tableHTML += '<thead><tr>';
|
||||
for (let i = 0; i < cols; i++) {
|
||||
tableHTML += `<th style="background-color: #000; color: #FFF; padding: 12px; text-align: left; font-size: 14px; text-transform: uppercase; font-weight: 700; border: 1px solid #000;">Kolom ${i + 1}</th>`;
|
||||
}
|
||||
tableHTML += '</tr></thead>';
|
||||
}
|
||||
|
||||
tableHTML += '<tbody>';
|
||||
for (let i = 0; i < (hasHeader ? rows - 1 : rows); i++) {
|
||||
tableHTML += '<tr>';
|
||||
for (let j = 0; j < cols; j++) {
|
||||
tableHTML += `<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">Isi sel</td>`;
|
||||
}
|
||||
tableHTML += '</tr>';
|
||||
}
|
||||
tableHTML += '</tbody></table>';
|
||||
|
||||
editor.chain().focus().insertContent(tableHTML).run();
|
||||
}, [editor]);
|
||||
|
||||
const uploadImageToStorage = async (file: File): Promise<string | null> => {
|
||||
try {
|
||||
const fileExt = file.name.split('.').pop();
|
||||
@@ -299,6 +618,42 @@ export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten.
|
||||
>
|
||||
<LinkIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
{/* Email Components Separator */}
|
||||
<div className="w-px h-6 bg-border mx-1" />
|
||||
|
||||
{/* Email Component Buttons */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={addButton}
|
||||
title="Tambah Email Button"
|
||||
>
|
||||
<MousePointer className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={addOTPBox}
|
||||
title="Tambah OTP Box"
|
||||
>
|
||||
<Square className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={addTable}
|
||||
title="Tambah Email Table"
|
||||
>
|
||||
<TableIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
{/* Image Upload Separator */}
|
||||
<div className="w-px h-6 bg-border mx-1" />
|
||||
|
||||
<label>
|
||||
<Button type="button" variant="ghost" size="sm" asChild disabled={uploading}>
|
||||
<span className={uploading ? 'opacity-50' : ''}>
|
||||
|
||||
@@ -134,12 +134,16 @@ export function EmailTemplatePreview({ template, onTest, isTestSending = false }
|
||||
{previewMode === 'master' ? 'Full Email Preview' : 'Content Preview'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="max-h-96 overflow-auto bg-white">
|
||||
<div className="bg-white" style={{ height: '600px', overflow: 'hidden' }}>
|
||||
<iframe
|
||||
srcDoc={previewHtml}
|
||||
className="w-full border-0"
|
||||
style={{ height: '500px' }}
|
||||
className="w-full h-full border-0"
|
||||
style={{
|
||||
height: '100%',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
sandbox="allow-same-origin"
|
||||
scrolling="no"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,43 +36,308 @@ const DEFAULT_TEMPLATES: { key: string; name: string; defaultSubject: string; de
|
||||
key: 'payment_success',
|
||||
name: 'Pembayaran Berhasil',
|
||||
defaultSubject: 'Pembayaran Berhasil - Order #{order_id}',
|
||||
defaultBody: '<h2>Halo {nama}!</h2><p>Terima kasih, pembayaran Anda sebesar <strong>{total}</strong> telah berhasil dikonfirmasi.</p><p><strong>Detail Pesanan:</strong></p><ul><li>Order ID: {order_id}</li><li>Tanggal: {tanggal_pesanan}</li><li>Metode: {metode_pembayaran}</li></ul><p>Produk: {produk}</p>'
|
||||
defaultBody: `
|
||||
<h2>Pembayaran Berhasil! 🎉</h2>
|
||||
<p>Halo <strong>{nama}</strong>, terima kasih atas pembayaran Anda. Kami senang menginformasikan bahwa pembayaran Anda telah berhasil dikonfirmasi.</p>
|
||||
|
||||
<h3>Detail Pembayaran</h3>
|
||||
<table style="width: 100%; border: 2px solid #000; margin-bottom: 25px; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="background-color: #000; color: #FFF; padding: 12px; text-align: left; font-size: 14px; text-transform: uppercase; font-weight: 700; border: 1px solid #000;">Parameter</th>
|
||||
<th style="background-color: #000; color: #FFF; padding: 12px; text-align: left; font-size: 14px; text-transform: uppercase; font-weight: 700; border: 1px solid #000;">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">Order ID</td>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;"><strong>{order_id}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">Tanggal Pesanan</td>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">{tanggal_pesanan}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">Total Pembayaran</td>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;"><strong>{total}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">Metode Pembayaran</td>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">{metode_pembayaran}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Produk yang Dibeli</h3>
|
||||
<p><strong>{produk}</strong></p>
|
||||
|
||||
<p style="margin-top: 30px;">
|
||||
<a href="#" style="display:inline-block;background-color:#000;color:#FFF !important;padding:14px 28px;font-weight:700;text-transform:uppercase;text-decoration:none !important;font-size:16px;border:2px solid #000;box-shadow:4px 4px 0px 0px #000000;margin:10px 0;transition:all 0.1s;text-align:center">
|
||||
Lihat Detail Pesanan
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<blockquote style="margin: 0 0 20px 0; padding: 15px 20px; border-left: 6px solid #00A651; background-color: #E6F4EA; font-style: italic; font-weight: 500; color: #005A2B;">
|
||||
<strong>Info:</strong> Anda akan menerima email terpisah untuk mengakses produk Anda.
|
||||
</blockquote>
|
||||
`
|
||||
},
|
||||
{
|
||||
key: 'access_granted',
|
||||
name: 'Akses Produk Diberikan',
|
||||
defaultSubject: 'Akses Anda Sudah Aktif - {produk}',
|
||||
defaultBody: '<h2>Halo {nama}!</h2><p>Selamat! Akses Anda ke <strong>{produk}</strong> sudah aktif.</p><p><a href="{link_akses}" style="display:inline-block;padding:12px 24px;background:#0066cc;color:white;text-decoration:none;border-radius:6px;">Akses Sekarang</a></p>'
|
||||
defaultBody: `
|
||||
<h2>Selamat! Akses Aktif 🚀</h2>
|
||||
<p>Halo <strong>{nama}</strong>, selamat! Akses Anda ke <strong>{produk}</strong> sudah aktif dan siap digunakan.</p>
|
||||
|
||||
<p>Anda sekarang dapat mengakses semua materi dan fitur yang tersedia dalam produk ini.</p>
|
||||
|
||||
<div style="background-color: #F4F4F5; border: 2px dashed #000; padding: 20px; text-align: center; margin: 20px 0; letter-spacing: 5px; font-family: 'Courier New', Courier, monospace; font-size: 32px; font-weight: 700; color: #000;">
|
||||
ACCESS GRANTED
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 30px; text-align: center;">
|
||||
<a href="{link_akses}" style="display:inline-block;background-color:#000;color:#FFF !important;padding:14px 28px;font-weight:700;text-transform:uppercase;text-decoration:none !important;font-size:16px;border:2px solid #000;box-shadow:4px 4px 0px 0px #000000;margin:10px 0;transition:all 0.1s;width:100%;box-sizing:border-box">
|
||||
Akses Sekarang
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h3>Penting:</h3>
|
||||
<ul>
|
||||
<li>Simpan link akses ini dengan aman</li>
|
||||
<li>Jangan bagikan kredensial Anda kepada orang lain</li>
|
||||
<li>Jika mengalami kendala, hubungi support kami</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
{
|
||||
key: 'order_created',
|
||||
name: 'Pesanan Dibuat',
|
||||
defaultSubject: 'Pesanan Anda #{order_id} Sedang Diproses',
|
||||
defaultBody: '<h2>Halo {nama}!</h2><p>Pesanan Anda dengan nomor <strong>{order_id}</strong> telah kami terima.</p><p>Total: <strong>{total}</strong></p><p>Silakan selesaikan pembayaran sebelum batas waktu.</p>'
|
||||
defaultSubject: 'Pesanan #{order_id} Sedang Diproses',
|
||||
defaultBody: `
|
||||
<h2>Pesanan Diterima ✅</h2>
|
||||
<p>Halo <strong>{nama}</strong>, terima kasih telah melakukan pesanan. Kami telah menerima pesanan Anda dengan detail sebagai berikut:</p>
|
||||
|
||||
<table style="width: 100%; border: 2px solid #000; margin-bottom: 25px; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="background-color: #000; color: #FFF; padding: 12px; text-align: left; font-size: 14px; text-transform: uppercase; font-weight: 700; border: 1px solid #000;">Informasi Pesanan</th>
|
||||
<th style="background-color: #000; color: #FFF; padding: 12px; text-align: left; font-size: 14px; text-transform: uppercase; font-weight: 700; border: 1px solid #000;">Detail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">Nomor Pesanan</td>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;"><strong>{order_id}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">Total Pembayaran</td>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;"><strong>{total}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">Status</td>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">Menunggu Pembayaran</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Langkah Selanjutnya:</h3>
|
||||
<ol>
|
||||
<li>Selesaikan pembayaran sebelum batas waktu</li>
|
||||
<li>Setelah pembayaran dikonfirmasi, Anda akan menerima email akses produk</li>
|
||||
<li>Simpan bukti pembayaran untuk arsip Anda</li>
|
||||
</ol>
|
||||
|
||||
<blockquote style="margin: 0 0 20px 0; padding: 15px 20px; border-left: 6px solid #E11D48; background-color: #FFE4E6; font-style: italic; font-weight: 500; color: #881337;">
|
||||
<strong>Penting:</strong> Segera lakukan pembayaran agar pesanan tidak kedaluwarsa.
|
||||
</blockquote>
|
||||
`
|
||||
},
|
||||
{
|
||||
key: 'payment_reminder',
|
||||
name: 'Pengingat Pembayaran',
|
||||
defaultSubject: 'Jangan Lupa Bayar - Order #{order_id}',
|
||||
defaultBody: '<h2>Halo {nama}!</h2><p>Pesanan Anda dengan nomor <strong>{order_id}</strong> menunggu pembayaran.</p><p>Total: <strong>{total}</strong></p><p>Segera selesaikan pembayaran agar tidak kedaluwarsa.</p>'
|
||||
defaultSubject: 'Reminder: Segera Selesaikan Pembayaran #{order_id}',
|
||||
defaultBody: `
|
||||
<h2>Reminder Pembayaran ⏰</h2>
|
||||
<p>Halo <strong>{nama}</strong>, ini adalah pengingat bahwa pesanan Anda masih menunggu pembayaran.</p>
|
||||
|
||||
<table style="width: 100%; border: 2px solid #000; margin-bottom: 25px; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="background-color: #000; color: #FFF; padding: 12px; text-align: left; font-size: 14px; text-transform: uppercase; font-weight: 700; border: 1px solid #000;">Detail Pesanan</th>
|
||||
<th style="background-color: #000; color: #FFF; padding: 12px; text-align: left; font-size: 14px; text-transform: uppercase; font-weight: 700; border: 1px solid #000;">Informasi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">Order ID</td>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;"><strong>{order_id}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">Total Pembayaran</td>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;"><strong>{total}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">Tanggal Pesanan</td>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">{tanggal_pesanan}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p style="margin-top: 30px; text-align: center;">
|
||||
<a href="#" style="display:inline-block;background-color:#E11D48;color:#FFF !important;padding:14px 28px;font-weight:700;text-transform:uppercase;text-decoration:none !important;font-size:16px;border:2px solid #E11D48;box-shadow:4px 4px 0px 0px #E11D48;margin:10px 0;transition:all 0.1s;width:100%;box-sizing:border-box">
|
||||
Bayar Sekarang
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<blockquote style="margin: 0 0 20px 0; padding: 15px 20px; border-left: 6px solid #E11D48; background-color: #FFE4E6; font-style: italic; font-weight: 500; color: #881337;">
|
||||
<strong>Peringatan:</strong> Jika pembayaran tidak diselesaikan dalam batas waktu, pesanan akan otomatis dibatalkan.
|
||||
</blockquote>
|
||||
`
|
||||
},
|
||||
{
|
||||
key: 'consulting_scheduled',
|
||||
name: 'Konsultasi Terjadwal',
|
||||
defaultSubject: 'Konsultasi Anda Sudah Terjadwal - {tanggal_konsultasi}',
|
||||
defaultBody: '<h2>Halo {nama}!</h2><p>Sesi konsultasi Anda telah dikonfirmasi:</p><ul><li>Tanggal: <strong>{tanggal_konsultasi}</strong></li><li>Jam: <strong>{jam_konsultasi}</strong></li></ul><p>Link meeting: <a href="{link_meet}">{link_meet}</a></p><p>Jika ada pertanyaan, hubungi kami.</p>'
|
||||
defaultSubject: 'Konsultasi Terjadwal - {tanggal_konsultasi}',
|
||||
defaultBody: `
|
||||
<h2>Sesi Konsultasi Dikonfirmasi 📅</h2>
|
||||
<p>Halo <strong>{nama}</strong>, sesi konsultasi Anda telah berhasil dijadwalkan. Berikut adalah detailnya:</p>
|
||||
|
||||
<table style="width: 100%; border: 2px solid #000; margin-bottom: 25px; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="background-color: #000; color: #FFF; padding: 12px; text-align: left; font-size: 14px; text-transform: uppercase; font-weight: 700; border: 1px solid #000;">Detail Sesi</th>
|
||||
<th style="background-color: #000; color: #FFF; padding: 12px; text-align: left; font-size: 14px; text-transform: uppercase; font-weight: 700; border: 1px solid #000;">Informasi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">Tanggal</td>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;"><strong>{tanggal_konsultasi}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">Waktu</td>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;"><strong>{jam_konsultasi}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">Link Meeting</td>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">
|
||||
<a href="{link_meet}" style="color: #000; text-decoration: underline; font-weight: 700;">{link_meet}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p style="margin-top: 30px; text-align: center;">
|
||||
<a href="{link_meet}" style="display:inline-block;background-color:#0066cc;color:#FFF !important;padding:14px 28px;font-weight:700;text-transform:uppercase;text-decoration:none !important;font-size:16px;border:2px solid #0066cc;box-shadow:4px 4px 0px 0px #0066cc;margin:10px 0;transition:all 0.1s">
|
||||
Bergabung ke Meeting
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h3>Persiapan Sebelum Sesi:</h3>
|
||||
<ul>
|
||||
<li>Uji koneksi internet Anda</li>
|
||||
<li>Siapkan materi atau pertanyaan yang akan dibahas</li>
|
||||
<li>Login 10 menit sebelum jadwal</li>
|
||||
<li>Gunakan laptop dengan kamera dan mikrofon</li>
|
||||
</ul>
|
||||
|
||||
<blockquote style="margin: 0 0 20px 0; padding: 15px 20px; border-left: 6px solid #1976D2; background-color: #E3F2FD; font-style: italic; font-weight: 500; color: #0D47A1;">
|
||||
<strong>Tip:</strong> Gunakan Google Chrome untuk pengalaman meeting terbaik.
|
||||
</blockquote>
|
||||
`
|
||||
},
|
||||
{
|
||||
key: 'event_reminder',
|
||||
name: 'Reminder Webinar/Bootcamp',
|
||||
defaultSubject: 'Reminder: {judul_event} Dimulai {tanggal_event}',
|
||||
defaultBody: '<h2>Halo {nama}!</h2><p>Jangan lupa, <strong>{judul_event}</strong> akan dimulai:</p><ul><li>Tanggal: {tanggal_event}</li><li>Jam: {jam_event}</li></ul><p><a href="{link_event}" style="display:inline-block;padding:12px 24px;background:#0066cc;color:white;text-decoration:none;border-radius:6px;">Bergabung</a></p>'
|
||||
defaultSubject: 'Reminder: {judul_event} - {tanggal_event}',
|
||||
defaultBody: `
|
||||
<h2>Jangan Sampai Ketinggalan! 🔥</h2>
|
||||
<p>Halo <strong>{nama}</strong>, jangan lupa bahwa <strong>{judul_event}</strong> akan segera dimulai!</p>
|
||||
|
||||
<div style="background-color: #F4F4F5; border: 2px dashed #000; padding: 20px; text-align: center; margin: 20px 0;">
|
||||
<h3 style="margin-top: 0; color: #E11D48;">EVENT STARTING SOON!</h3>
|
||||
<div style="font-size: 24px; font-weight: 700; letter-spacing: 2px; margin: 10px 0;">
|
||||
{judul_event}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table style="width: 100%; border: 2px solid #000; margin-bottom: 25px; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="background-color: #000; color: #FFF; padding: 12px; text-align: left; font-size: 14px; text-transform: uppercase; font-weight: 700; border: 1px solid #000;">Event Detail</th>
|
||||
<th style="background-color: #000; color: #FFF; padding: 12px; text-align: left; font-size: 14px; text-transform: uppercase; font-weight: 700; border: 1px solid #000;">Informasi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">Judul Event</td>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;"><strong>{judul_event}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">Tanggal</td>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">{tanggal_event}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">Waktu</td>
|
||||
<td style="padding: 12px; border: 1px solid #000; font-size: 15px; vertical-align: top;">{jam_event}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p style="margin-top: 30px; text-align: center;">
|
||||
<a href="{link_event}" style="display:inline-block;background-color:#00A651;color:#FFF !important;padding:14px 28px;font-weight:700;text-transform:uppercase;text-decoration:none !important;font-size:16px;border:2px solid #00A651;box-shadow:4px 4px 0px 0px #00A651;margin:10px 0;transition:all 0.1s;width:100%;box-sizing:border-box">
|
||||
Bergabung Sekarang
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h3>Persiapan Event:</h3>
|
||||
<ul>
|
||||
<li>Stabilkan koneksi internet Anda</li>
|
||||
<li>Siapkan notebook untuk mencatat</li>
|
||||
<li>Login 15 menit sebelum event dimulai</li>
|
||||
<li>Siapkan pertanyaan untuk sesi Q&A</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
{
|
||||
key: 'bootcamp_progress',
|
||||
name: 'Progress Bootcamp',
|
||||
defaultSubject: 'Update Progress Bootcamp Anda',
|
||||
defaultBody: '<h2>Halo {nama}!</h2><p>Ini adalah update progress bootcamp Anda.</p><p>Terus semangat belajar!</p>'
|
||||
defaultSubject: 'Update Progress Bootcamp - {nama}',
|
||||
defaultBody: `
|
||||
<h2>Progress Update 📈</h2>
|
||||
<p>Halo <strong>{nama}</strong>, ini adalah update terbaru tentang progress bootcamp Anda.</p>
|
||||
|
||||
<div style="background-color: #F4F4F5; border: 2px dashed #000; padding: 20px; text-align: center; margin: 20px 0;">
|
||||
<div style="font-size: 18px; font-weight: 700; margin-bottom: 10px;">PROGRESS ANDA</div>
|
||||
<div style="font-size: 48px; font-weight: 900; color: #00A651;">75%</div>
|
||||
<div style="font-size: 14px; color: #666;">Completed Modules: 15/20</div>
|
||||
</div>
|
||||
|
||||
<h3>Module Selesai:</h3>
|
||||
<ul>
|
||||
<li>✅ Fundamentals & Basics</li>
|
||||
<li>✅ Advanced Concepts</li>
|
||||
<li>✅ Practical Applications</li>
|
||||
<li>✅ Project Workshop</li>
|
||||
</ul>
|
||||
|
||||
<h3>Module Berikutnya:</h3>
|
||||
<ul>
|
||||
<li>🔄 Final Assessment</li>
|
||||
<li>📋 Portfolio Development</li>
|
||||
</ul>
|
||||
|
||||
<p style="margin-top: 30px; text-align: center;">
|
||||
<a href="#" style="display:inline-block;background-color:#000;color:#FFF !important;padding:14px 28px;font-weight:700;text-transform:uppercase;text-decoration:none !important;font-size:16px;border:2px solid #000;box-shadow:4px 4px 0px 0px #000000;margin:10px 0;transition:all 0.1s">
|
||||
Lanjut Belajar
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<blockquote style="margin: 0 0 20px 0; padding: 15px 20px; border-left: 6px solid #00A651; background-color: #E6F4EA; font-style: italic; font-weight: 500; color: #005A2B;">
|
||||
<strong>Motivasi:</strong> Anda sudah 75% selesai! Terus semangat, kesuksesan Anda sudah di depan mata!
|
||||
</blockquote>
|
||||
`
|
||||
},
|
||||
];
|
||||
|
||||
@@ -81,6 +346,8 @@ export function NotifikasiTab() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [expandedTemplates, setExpandedTemplates] = useState<Set<string>>(new Set());
|
||||
const [testingTemplate, setTestingTemplate] = useState<string | null>(null);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<NotificationTemplate | null>(null);
|
||||
const [previewMode, setPreviewMode] = useState<'master' | 'content'>('master');
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
@@ -316,14 +583,16 @@ export function NotifikasiTab() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email Preview */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="font-semibold">Email Preview</h4>
|
||||
<EmailTemplatePreview
|
||||
template={template}
|
||||
onTest={sendTestEmail}
|
||||
isTestSending={testingTemplate === template.id}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
updateTemplate(template);
|
||||
setSelectedTemplate(template);
|
||||
}}
|
||||
className="shadow-sm flex-1"
|
||||
>
|
||||
Simpan & Preview
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{template.last_payload_example && (
|
||||
@@ -350,6 +619,34 @@ export function NotifikasiTab() {
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Consolidated Email Preview */}
|
||||
{selectedTemplate && (
|
||||
<Card className="border-2 border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span>Preview: {selectedTemplate.name}</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setSelectedTemplate(null)}
|
||||
>
|
||||
Tutup Preview
|
||||
</Button>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Preview template email dengan master styling
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<EmailTemplatePreview
|
||||
template={selectedTemplate}
|
||||
onTest={sendTestEmail}
|
||||
isTestSending={testingTemplate === selectedTemplate.id}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user