first commit

This commit is contained in:
AyrisAI
2026-05-14 01:57:52 +03:00
parent 863a32cd35
commit 4a9196f483
47 changed files with 12043 additions and 102 deletions

View 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>; }