first commit
This commit is contained in:
147
components/mail/MessageView.tsx
Normal file
147
components/mail/MessageView.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
"use client";
|
||||
import type { MailMessage } from "@/app/dashboard/mail/page";
|
||||
import { formatBytes } from "@/lib/format";
|
||||
|
||||
function getFileIcon(contentType: string, filename: string): string {
|
||||
if (contentType.startsWith("image/")) return "🖼️";
|
||||
if (contentType === "application/pdf") return "📄";
|
||||
if (contentType.includes("zip") || contentType.includes("rar") || contentType.includes("tar")) return "📦";
|
||||
if (contentType.includes("word") || filename.endsWith(".doc") || filename.endsWith(".docx")) return "📝";
|
||||
if (contentType.includes("sheet") || contentType.includes("excel") || filename.endsWith(".xls")) return "📊";
|
||||
if (contentType.includes("presentation") || filename.endsWith(".ppt")) return "📑";
|
||||
if (contentType.startsWith("video/")) return "🎬";
|
||||
if (contentType.startsWith("audio/")) return "🎵";
|
||||
return "📎";
|
||||
}
|
||||
|
||||
function canPreview(contentType: string): boolean {
|
||||
return contentType.startsWith("image/") || contentType === "application/pdf";
|
||||
}
|
||||
|
||||
export default function MessageView({ message, onReply, onDelete, folder }: {
|
||||
message: MailMessage;
|
||||
onReply: () => void;
|
||||
onDelete: () => void;
|
||||
folder: string;
|
||||
}) {
|
||||
const from = message.from[0];
|
||||
const date = new Date(message.date).toLocaleString("tr-TR", {
|
||||
day: "numeric", month: "long", year: "numeric", hour: "2-digit", minute: "2-digit",
|
||||
});
|
||||
|
||||
const downloadUrl = (filename: string) =>
|
||||
`/api/mail/messages/${message.uid}/attachments?folder=${encodeURIComponent(folder)}&filename=${encodeURIComponent(filename)}`;
|
||||
|
||||
const handleAttachment = (att: { filename: string; contentType: string }, preview: boolean) => {
|
||||
const url = downloadUrl(att.filename);
|
||||
if (preview && canPreview(att.contentType)) {
|
||||
window.open(url, "_blank");
|
||||
} else {
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = att.filename;
|
||||
a.click();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="message-view">
|
||||
{/* Header */}
|
||||
<div className="message-view-header">
|
||||
<h2 className="message-view-subject">{message.subject}</h2>
|
||||
<div className="message-view-actions">
|
||||
<button className="btn btn-ghost btn-sm" onClick={onReply} title="Yanıtla">
|
||||
<ReplyIcon /> Yanıtla
|
||||
</button>
|
||||
<button className="btn btn-danger btn-sm" onClick={onDelete} title="Sil">
|
||||
<TrashIcon /> Sil
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Meta */}
|
||||
<div className="message-view-meta">
|
||||
<div className="message-view-avatar">{(from?.name || from?.address)?.[0]?.toUpperCase() ?? "?"}</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 13 }}>{from?.name || from?.address}</div>
|
||||
<div style={{ fontSize: 11, color: "var(--text-muted)" }}>
|
||||
{from?.address}
|
||||
{message.to.length > 0 && <> → {message.to.map((t) => t.address).join(", ")}</>}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: "var(--text-muted)", flexShrink: 0 }}>{date}</div>
|
||||
</div>
|
||||
|
||||
{/* Attachments */}
|
||||
{message.attachments.length > 0 && (
|
||||
<div className="message-attachments">
|
||||
<div style={{ width: "100%", fontSize: 12, fontWeight: 600, color: "var(--text-secondary)", marginBottom: 6 }}>
|
||||
📎 {message.attachments.length} ek
|
||||
</div>
|
||||
{message.attachments.map((att, i) => (
|
||||
<div key={i} className="attachment-chip" onClick={() => handleAttachment(att, false)}>
|
||||
<span>{getFileIcon(att.contentType, att.filename)}</span>
|
||||
<span style={{ maxWidth: 160, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{att.filename}
|
||||
</span>
|
||||
<span style={{ color: "var(--text-muted)", fontSize: 11 }}>{formatBytes(att.size)}</span>
|
||||
<span className="attachment-actions">
|
||||
<button
|
||||
className="att-btn"
|
||||
onClick={(e) => { e.stopPropagation(); handleAttachment(att, false); }}
|
||||
title="İndir"
|
||||
>
|
||||
⬇
|
||||
</button>
|
||||
{canPreview(att.contentType) && (
|
||||
<button
|
||||
className="att-btn"
|
||||
onClick={(e) => { e.stopPropagation(); handleAttachment(att, true); }}
|
||||
title="Önizle"
|
||||
>
|
||||
👁
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Body */}
|
||||
<div className="message-view-body">
|
||||
{message.html ? (
|
||||
<iframe
|
||||
srcDoc={sanitizeHtml(message.html)}
|
||||
sandbox="allow-same-origin"
|
||||
style={{ width: "100%", border: "none", minHeight: 400 }}
|
||||
onLoad={(e) => {
|
||||
const iframe = e.target as HTMLIFrameElement;
|
||||
if (iframe.contentDocument) {
|
||||
iframe.style.height = iframe.contentDocument.body.scrollHeight + 20 + "px";
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<pre style={{ whiteSpace: "pre-wrap", fontFamily: "inherit", fontSize: 13, color: "var(--text-secondary)" }}>
|
||||
{message.text}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizeHtml(html: string): string {
|
||||
const style = `<style>
|
||||
body { font-family: Inter, -apple-system, sans-serif; font-size: 14px; color: #c9d1d9; background: transparent; margin: 0; padding: 8px; line-height: 1.6; }
|
||||
a { color: #58a6ff; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
</style>`;
|
||||
let clean = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "");
|
||||
clean = clean.replace(/\son\w+\s*=\s*["'][^"']*["']/gi, "");
|
||||
return `<!DOCTYPE html><html><head>${style}</head><body>${clean}</body></html>`;
|
||||
}
|
||||
|
||||
function ReplyIcon() { return <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg>; }
|
||||
function TrashIcon() { return <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>; }
|
||||
Reference in New Issue
Block a user