dptxa-proxy / src /pages /admin /email.astro
TXAVLOG
Deploy DPTXA to Hugging Face Spaces
4bea261
Raw
History Blame Contribute Delete
18.6 kB
---
import AdminLayout from '../../layouts/AdminLayout.astro';
import { txasupabase } from '../../lib/txasupabase.js';
const logs = await txasupabase.getEmailLogs();
---
<AdminLayout title="Nhật ký Email gửi đi">
<div class="mb-8">
<h1 class="text-4xl font-black tracking-tighter text-white">Nhật ký Email</h1>
<p class="mt-2 text-gray-500">Giám sát và kiểm tra toàn bộ hòm thư gửi đi, trạng thái SMTP và xem trước nội dung email của hệ thống.</p>
</div>
<!-- 2 Column Layout - Desktop (Desktop: side-by-side, Mobile: dynamic overlay) -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
<!-- Left Column: Email List (8 cols or 5 cols? Let's do 5 cols on desktop for a compact list, and 7 cols for preview) -->
<div class="lg:col-span-5 glass rounded-[35px] border border-white/5 p-6 md:p-8 space-y-6">
<div class="flex items-center justify-between border-b border-white/5 pb-4">
<span class="text-xs font-black uppercase tracking-widest text-primary"><i class="fas fa-history"></i> Lịch sử thư gửi</span>
<span class="text-[10px] font-black uppercase bg-white/5 border border-white/10 px-3 py-1 rounded-full text-gray-400">Tổng: {logs.length}</span>
</div>
<!-- Filter & Search -->
<div class="relative">
<i class="fas fa-search absolute left-4 top-1/2 -translate-y-1/2 text-gray-500 text-xs"></i>
<input type="text" id="email-search" placeholder="Tìm người nhận, tiêu đề..." class="w-full bg-[#12141d] border border-white/5 rounded-2xl pl-11 pr-4 py-3 text-xs text-white focus:outline-none focus:border-primary/50 transition-all" />
</div>
<!-- Scrollable List -->
<div class="space-y-3 overflow-y-auto max-h-[calc(100vh-320px)] pr-2 scrollbar-thin" id="email-list">
<!-- Rendered via JS -->
</div>
</div>
<!-- Right Column: Content Preview Pane (7 cols - Hidden on mobile but slides in) -->
<div class="lg:col-span-7 hidden lg:block glass rounded-[35px] border border-white/5 p-8 h-[calc(100vh-200px)] sticky top-24" id="desktop-preview-pane">
<!-- Empty State -->
<div id="preview-empty-state" class="h-full flex flex-col items-center justify-center text-center text-gray-500">
<div class="h-16 w-16 rounded-full bg-white/5 flex items-center justify-center mb-4 border border-white/5 shadow-[0_0_30px_rgba(255,255,255,0.02)]">
<i class="fas fa-envelope-open text-2xl opacity-20"></i>
</div>
<p class="text-xs font-bold uppercase tracking-widest">Chọn một email</p>
<p class="text-[11px] text-gray-500 mt-2">Nhấp vào một email bất kỳ bên danh sách để hiển thị nội dung xem trước đầy đủ.</p>
</div>
<!-- Content Preview (Hidden by default) -->
<div id="preview-content-state" class="hidden h-full flex flex-col">
<!-- Preview Header -->
<div class="border-b border-white/5 pb-4 mb-4 flex items-start justify-between">
<div>
<h2 id="prev-subject" class="text-base font-black text-white leading-tight"></h2>
<p class="text-xs text-gray-400 mt-1">Đến: <span id="prev-to" class="font-bold text-primary"></span></p>
</div>
<div id="prev-status-badge"></div>
</div>
<!-- Meta Info Row -->
<div class="grid grid-cols-2 gap-4 bg-white/[0.02] border border-white/5 rounded-2xl p-4 mb-4 text-[11px]">
<div>
<span class="text-gray-500 block uppercase font-black tracking-wider text-[9px]">Mẫu Template</span>
<span id="prev-template" class="font-mono text-gray-300 font-bold"></span>
</div>
<div>
<span class="text-gray-500 block uppercase font-black tracking-wider text-[9px]">Thời gian gửi</span>
<span id="prev-time" class="text-gray-300 font-bold"></span>
</div>
</div>
<!-- Error message block if failed -->
<div id="prev-error-box" class="hidden bg-red-500/10 border border-red-500/20 rounded-2xl p-4 mb-4 text-xs text-red-400">
<p class="font-bold"><i class="fas fa-exclamation-triangle"></i> Lỗi gửi SMTP:</p>
<p id="prev-error-msg" class="mt-1 font-mono"></p>
</div>
<!-- Preview Selector Tab (HTML Preview / HTML Source) -->
<div class="flex gap-2 border-b border-white/5 pb-3 mb-4">
<button id="btn-view-preview" class="px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-wider bg-primary/10 text-primary border border-primary/20">Xem trước</button>
<button id="btn-view-source" class="px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-wider text-gray-500 hover:text-white border border-transparent">Mã nguồn HTML</button>
</div>
<!-- Body Preview Container -->
<div class="flex-1 rounded-2xl overflow-hidden bg-white relative border border-white/10" id="preview-iframe-wrapper">
<iframe id="preview-iframe" class="w-full h-full border-none"></iframe>
</div>
<div class="flex-1 rounded-2xl overflow-hidden bg-[#12141d] border border-white/5 p-4 font-mono text-xs text-gray-400 overflow-y-auto hidden" id="preview-source-wrapper">
<pre id="preview-source-code" class="whitespace-pre-wrap word-break-all"></pre>
</div>
</div>
</div>
</div>
<!-- Mobile Preview Slide-out Bottom Sheet / Drawer -->
<div id="mobile-preview-drawer" class="fixed inset-0 z-[200] hidden bg-black/80 opacity-0 transition-opacity duration-300 pointer-events-none lg:hidden flex flex-col justify-end">
<div class="w-full h-[85vh] bg-[#0f0f12] border-t border-white/10 rounded-t-[40px] p-6 flex flex-col transform translate-y-full transition-transform duration-300 pointer-events-auto">
<!-- Close & Drag Handle -->
<div class="flex items-center justify-between pb-4 border-b border-white/5 mb-4">
<div>
<span class="text-[10px] font-black uppercase tracking-widest text-primary"><i class="fas fa-eye"></i> Nội dung Email</span>
</div>
<button id="btn-close-mobile-preview" class="h-9 w-9 rounded-xl bg-white/5 flex items-center justify-center text-gray-400 hover:text-white border border-white/5 active:scale-95 transition-all">
<i class="fas fa-times text-xs"></i>
</button>
</div>
<!-- Preview content panel (Exactly like desktop inside bottom sheet) -->
<div class="flex-1 flex flex-col overflow-y-auto" id="mobile-preview-container">
<!-- Content will be mirrored from desktop rendering logic dynamically -->
</div>
</div>
</div>
</AdminLayout>
<script is:inline define:vars={{ initialLogs: logs }}>
let emailLogs = initialLogs || [];
let searchQuery = '';
let selectedLogId = null;
let viewMode = 'preview'; // 'preview' or 'source'
function formatTime(isoString) {
if (!isoString) return '';
const date = new Date(isoString);
return date.toLocaleString('vi-VN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
}
function renderList() {
const listContainer = document.getElementById('email-list');
if (!listContainer) return;
const filtered = emailLogs.filter(log =>
log.to_email.toLowerCase().includes(searchQuery.toLowerCase()) ||
log.subject.toLowerCase().includes(searchQuery.toLowerCase()) ||
log.template_name.toLowerCase().includes(searchQuery.toLowerCase())
);
if (filtered.length === 0) {
listContainer.innerHTML = `
<div class="py-12 text-center text-gray-500 text-xs">
<i class="fas fa-envelope text-2xl mb-2 opacity-10"></i>
Không tìm thấy email nào
</div>
`;
return;
}
listContainer.innerHTML = filtered.map(log => {
const isSelected = log.id === selectedLogId;
const isSuccess = log.status === 'sent';
return `
<div
class="email-row p-4 rounded-2xl border transition-all cursor-pointer flex flex-col gap-2 relative overflow-hidden ${
isSelected
? 'bg-primary/10 border-primary shadow-[0_0_15px_rgba(124,58,237,0.1)]'
: 'bg-[#12141d]/40 border-white/5 hover:bg-white/[0.02] hover:border-white/10'
}"
data-id="${log.id}"
>
<!-- Top Row -->
<div class="flex items-center justify-between">
<span class="text-[10px] font-black uppercase text-gray-400 font-mono">${log.template_name}</span>
<span class="text-[10px] text-gray-500">${formatTime(log.created_at)}</span>
</div>
<!-- Mid Row -->
<div class="flex flex-col">
<span class="text-xs font-bold text-white leading-snug truncate">${log.subject}</span>
<span class="text-[11px] text-primary/70 font-semibold truncate mt-0.5">${log.to_email}</span>
</div>
<!-- Status Indicator Dot -->
<div class="absolute right-4 bottom-4 flex items-center gap-1.5">
<span class="h-2 w-2 rounded-full relative flex">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${
isSuccess ? 'bg-emerald-400' : 'bg-red-400'
}"></span>
<span class="relative inline-flex rounded-full h-2 w-2 ${
isSuccess ? 'bg-emerald-500' : 'bg-red-500'
}"></span>
</span>
<span class="text-[8px] font-black uppercase tracking-widest ${
isSuccess ? 'text-emerald-400' : 'text-red-400'
}">${isSuccess ? 'Thành công' : 'Thất bại'}</span>
</div>
</div>
`;
}).join('');
bindListEvents();
}
function selectLog(id) {
selectedLogId = id;
renderList();
const log = emailLogs.find(l => l.id === id);
if (!log) return;
// Detect if we are on mobile
const isMobile = window.innerWidth < 1024;
if (isMobile) {
showMobilePreview(log);
} else {
showDesktopPreview(log);
}
}
function showDesktopPreview(log) {
const emptyState = document.getElementById('preview-empty-state');
const contentState = document.getElementById('preview-content-state');
if (!emptyState || !contentState) return;
emptyState.classList.add('hidden');
contentState.classList.remove('hidden');
// Fill elements
document.getElementById('prev-subject').textContent = log.subject;
document.getElementById('prev-to').textContent = log.to_email;
document.getElementById('prev-template').textContent = log.template_name;
document.getElementById('prev-time').textContent = formatTime(log.created_at);
// Badges
const badge = document.getElementById('prev-status-badge');
const isSuccess = log.status === 'sent';
if (badge) {
badge.innerHTML = `
<span class="px-3 py-1 rounded-full text-[9px] font-black uppercase tracking-widest border ${
isSuccess
? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-400'
: 'bg-red-500/10 border-red-500/20 text-red-400'
}">
${isSuccess ? '<i class="fas fa-check-circle"></i> Đã gửi' : '<i class="fas fa-times-circle"></i> Lỗi'}
</span>
`;
}
// Error Box
const errorBox = document.getElementById('prev-error-box');
const errorMsg = document.getElementById('prev-error-msg');
if (errorBox && errorMsg) {
if (!isSuccess && log.error_message) {
errorBox.classList.remove('hidden');
errorMsg.textContent = log.error_message;
} else {
errorBox.classList.add('hidden');
}
}
// Body Preview Frame
const iframe = document.getElementById('preview-iframe');
if (iframe) {
iframe.srcdoc = log.body;
}
// HTML source view
const sourceCode = document.getElementById('preview-source-code');
if (sourceCode) {
sourceCode.textContent = log.body;
}
updateViewMode();
}
function showMobilePreview(log) {
const drawer = document.getElementById('mobile-preview-drawer');
const panel = drawer?.querySelector('.pointer-events-auto');
const container = document.getElementById('mobile-preview-container');
if (!drawer || !panel || !container) return;
const isSuccess = log.status === 'sent';
const statusBadge = `
<span class="px-3 py-1 rounded-full text-[9px] font-black uppercase tracking-widest border ${
isSuccess
? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-400'
: 'bg-red-500/10 border-red-500/20 text-red-400'
}">
${isSuccess ? 'Thành công' : 'Lỗi SMTP'}
</span>
`;
const errorBlock = (!isSuccess && log.error_message) ? `
<div class="bg-red-500/10 border border-red-500/20 rounded-2xl p-4 mb-4 text-xs text-red-400">
<p class="font-bold"><i class="fas fa-exclamation-triangle"></i> Lỗi chi tiết:</p>
<p class="mt-1 font-mono">${log.error_message}</p>
</div>
` : '';
container.innerHTML = `
<div class="space-y-4">
<div class="flex items-start justify-between">
<div>
<h3 class="text-sm font-bold text-white leading-tight">${log.subject}</h3>
<p class="text-xs text-gray-400 mt-1">Đến: <span class="font-bold text-primary">${log.to_email}</span></p>
</div>
<div>${statusBadge}</div>
</div>
<div class="grid grid-cols-2 gap-4 bg-white/[0.02] border border-white/5 rounded-2xl p-3 text-[10px]">
<div>
<span class="text-gray-500 block uppercase font-black text-[8px]">Template</span>
<span class="font-mono text-gray-300 font-bold">${log.template_name}</span>
</div>
<div>
<span class="text-gray-500 block uppercase font-black text-[8px]">Thời gian</span>
<span class="text-gray-300 font-bold">${formatTime(log.created_at)}</span>
</div>
</div>
${errorBlock}
<!-- Body Preview -->
<div class="rounded-2xl overflow-hidden bg-white border border-white/10 h-[350px] relative">
<iframe class="w-full h-full border-none" srcdoc="${log.body.replace(/"/g, '&quot;')}"></iframe>
</div>
</div>
`;
// Open sheet
drawer.classList.remove('hidden');
void drawer.offsetWidth;
drawer.classList.add('opacity-100');
drawer.classList.remove('pointer-events-none');
panel.classList.remove('translate-x-full', 'translate-y-full');
}
function closeMobilePreview() {
const drawer = document.getElementById('mobile-preview-drawer');
const panel = drawer?.querySelector('.pointer-events-auto');
if (drawer && panel) {
drawer.classList.remove('opacity-100');
drawer.classList.add('pointer-events-none');
panel.classList.add('translate-y-full');
setTimeout(() => {
drawer.classList.add('hidden');
}, 300);
}
}
function updateViewMode() {
const iframeWrapper = document.getElementById('preview-iframe-wrapper');
const sourceWrapper = document.getElementById('preview-source-wrapper');
const btnPrev = document.getElementById('btn-view-preview');
const btnSrc = document.getElementById('btn-view-source');
if (!iframeWrapper || !sourceWrapper || !btnPrev || !btnSrc) return;
if (viewMode === 'preview') {
iframeWrapper.classList.remove('hidden');
sourceWrapper.classList.add('hidden');
btnPrev.className = 'px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-wider bg-primary/10 text-primary border border-primary/20';
btnSrc.className = 'px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-wider text-gray-500 hover:text-white border border-transparent';
} else {
iframeWrapper.classList.add('hidden');
sourceWrapper.classList.remove('hidden');
btnPrev.className = 'px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-wider text-gray-500 hover:text-white border border-transparent';
btnSrc.className = 'px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-wider bg-primary/10 text-primary border border-primary/20';
}
}
function bindListEvents() {
document.querySelectorAll('.email-row').forEach(row => {
row.onclick = () => {
const id = row.getAttribute('data-id');
selectLog(id);
};
});
}
function init() {
renderList();
const searchInput = document.getElementById('email-search');
if (searchInput) {
searchInput.oninput = (e) => {
searchQuery = e.target.value.trim();
renderList();
};
}
// View tabs trigger
const btnPrev = document.getElementById('btn-view-preview');
const btnSrc = document.getElementById('btn-view-source');
btnPrev?.addEventListener('click', (e) => {
e.preventDefault();
viewMode = 'preview';
updateViewMode();
});
btnSrc?.addEventListener('click', (e) => {
e.preventDefault();
viewMode = 'source';
updateViewMode();
});
// Mobile close
const btnCloseMobile = document.getElementById('btn-close-mobile-preview');
btnCloseMobile?.addEventListener('click', closeMobilePreview);
const mobileDrawer = document.getElementById('mobile-preview-drawer');
mobileDrawer?.addEventListener('click', (e) => {
if (e.target === mobileDrawer) closeMobilePreview();
});
}
document.addEventListener('astro:page-load', init);
</script>
<style>
.glass {
background: rgba(18, 20, 29, 0.4);
backdrop-filter: blur(40px) saturate(200%);
}
.glass-sub {
background: rgba(255, 255, 255, 0.02);
}
/* Custom scrollbar for clean dashboard */
.scrollbar-thin::-webkit-scrollbar {
width: 4px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background: rgba(124, 58, 237, 0.2);
border-radius: 99px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: rgba(124, 58, 237, 0.4);
}
</style>