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:
dwindown
2025-12-22 20:35:50 +07:00
parent efc085e231
commit 37680bd25b
3 changed files with 697 additions and 41 deletions

View File

@@ -3,11 +3,16 @@ import StarterKit from '@tiptap/starter-kit';
import Link from '@tiptap/extension-link'; import Link from '@tiptap/extension-link';
import Image from '@tiptap/extension-image'; import Image from '@tiptap/extension-image';
import Placeholder from '@tiptap/extension-placeholder'; 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 { Button } from '@/components/ui/button';
import { import {
Bold, Italic, List, ListOrdered, Quote, Link as LinkIcon, Bold, Italic, List, ListOrdered, Quote, Link as LinkIcon,
Image as ImageIcon, Heading1, Heading2, Undo, Redo, Image as ImageIcon, Heading1, Heading2, Undo, Redo,
Maximize2, Minimize2 Maximize2, Minimize2, MousePointer, Square, Table as TableIcon
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useCallback, useEffect, useState } from 'react'; 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) { export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten...', className }: RichTextEditorProps) {
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const [selectedImage, setSelectedImage] = useState<{ src: string; width?: number; height?: number } | null>(null); 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({ const editor = useEditor({
extensions: [ extensions: [
StarterKit, StarterKit.configure({
table: false,
tableRow: false,
tableCell: false,
tableHeader: false,
}),
Link.configure({ Link.configure({
openOnClick: false, openOnClick: false,
HTMLAttributes: { HTMLAttributes: {
@@ -69,6 +320,17 @@ export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten.
class: 'max-w-full h-auto rounded-md cursor-pointer', 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.configure({
placeholder, placeholder,
}), }),
@@ -110,6 +372,63 @@ export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten.
} }
}, [editor]); }, [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> => { const uploadImageToStorage = async (file: File): Promise<string | null> => {
try { try {
const fileExt = file.name.split('.').pop(); const fileExt = file.name.split('.').pop();
@@ -299,6 +618,42 @@ export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten.
> >
<LinkIcon className="w-4 h-4" /> <LinkIcon className="w-4 h-4" />
</Button> </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> <label>
<Button type="button" variant="ghost" size="sm" asChild disabled={uploading}> <Button type="button" variant="ghost" size="sm" asChild disabled={uploading}>
<span className={uploading ? 'opacity-50' : ''}> <span className={uploading ? 'opacity-50' : ''}>

View File

@@ -134,12 +134,16 @@ export function EmailTemplatePreview({ template, onTest, isTestSending = false }
{previewMode === 'master' ? 'Full Email Preview' : 'Content Preview'} {previewMode === 'master' ? 'Full Email Preview' : 'Content Preview'}
</span> </span>
</div> </div>
<div className="max-h-96 overflow-auto bg-white"> <div className="bg-white" style={{ height: '600px', overflow: 'hidden' }}>
<iframe <iframe
srcDoc={previewHtml} srcDoc={previewHtml}
className="w-full border-0" className="w-full h-full border-0"
style={{ height: '500px' }} style={{
height: '100%',
overflow: 'hidden'
}}
sandbox="allow-same-origin" sandbox="allow-same-origin"
scrolling="no"
/> />
</div> </div>
</div> </div>

View File

@@ -32,47 +32,312 @@ const SHORTCODES_HELP = {
}; };
const DEFAULT_TEMPLATES: { key: string; name: string; defaultSubject: string; defaultBody: string }[] = [ const DEFAULT_TEMPLATES: { key: string; name: string; defaultSubject: string; defaultBody: string }[] = [
{ {
key: 'payment_success', key: 'payment_success',
name: 'Pembayaran Berhasil', name: 'Pembayaran Berhasil',
defaultSubject: 'Pembayaran Berhasil - Order #{order_id}', 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', key: 'access_granted',
name: 'Akses Produk Diberikan', name: 'Akses Produk Diberikan',
defaultSubject: 'Akses Anda Sudah Aktif - {produk}', 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', key: 'order_created',
name: 'Pesanan Dibuat', name: 'Pesanan Dibuat',
defaultSubject: 'Pesanan Anda #{order_id} Sedang Diproses', defaultSubject: 'Pesanan #{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>' 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', key: 'payment_reminder',
name: 'Pengingat Pembayaran', name: 'Pengingat Pembayaran',
defaultSubject: 'Jangan Lupa Bayar - Order #{order_id}', defaultSubject: 'Reminder: Segera Selesaikan Pembayaran #{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>' 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', key: 'consulting_scheduled',
name: 'Konsultasi Terjadwal', name: 'Konsultasi Terjadwal',
defaultSubject: 'Konsultasi Anda Sudah Terjadwal - {tanggal_konsultasi}', defaultSubject: 'Konsultasi 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>' 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', key: 'event_reminder',
name: 'Reminder Webinar/Bootcamp', name: 'Reminder Webinar/Bootcamp',
defaultSubject: 'Reminder: {judul_event} Dimulai {tanggal_event}', defaultSubject: 'Reminder: {judul_event} - {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>' 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', key: 'bootcamp_progress',
name: 'Progress Bootcamp', name: 'Progress Bootcamp',
defaultSubject: 'Update Progress Bootcamp Anda', defaultSubject: 'Update Progress Bootcamp - {nama}',
defaultBody: '<h2>Halo {nama}!</h2><p>Ini adalah update progress bootcamp Anda.</p><p>Terus semangat belajar!</p>' 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 [loading, setLoading] = useState(true);
const [expandedTemplates, setExpandedTemplates] = useState<Set<string>>(new Set()); const [expandedTemplates, setExpandedTemplates] = useState<Set<string>>(new Set());
const [testingTemplate, setTestingTemplate] = useState<string | null>(null); const [testingTemplate, setTestingTemplate] = useState<string | null>(null);
const [selectedTemplate, setSelectedTemplate] = useState<NotificationTemplate | null>(null);
const [previewMode, setPreviewMode] = useState<'master' | 'content'>('master');
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
@@ -316,14 +583,16 @@ export function NotifikasiTab() {
</div> </div>
</div> </div>
{/* Email Preview */} <div className="flex gap-2">
<div className="space-y-4"> <Button
<h4 className="font-semibold">Email Preview</h4> onClick={() => {
<EmailTemplatePreview updateTemplate(template);
template={template} setSelectedTemplate(template);
onTest={sendTestEmail} }}
isTestSending={testingTemplate === template.id} className="shadow-sm flex-1"
/> >
Simpan & Preview
</Button>
</div> </div>
{template.last_payload_example && ( {template.last_payload_example && (
@@ -350,6 +619,34 @@ export function NotifikasiTab() {
))} ))}
</CardContent> </CardContent>
</Card> </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> </div>
); );
} }