first commit
This commit is contained in:
200
app/dashboard/mail/page.tsx
Normal file
200
app/dashboard/mail/page.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import MailLogin from "@/components/mail/MailLogin";
|
||||
import FolderList from "@/components/mail/FolderList";
|
||||
import MessageList from "@/components/mail/MessageList";
|
||||
import MessageView from "@/components/mail/MessageView";
|
||||
import ComposeModal from "@/components/mail/ComposeModal";
|
||||
|
||||
export interface MailFolder {
|
||||
name: string;
|
||||
path: string;
|
||||
specialUse?: string;
|
||||
messages: number;
|
||||
unseen: number;
|
||||
}
|
||||
|
||||
export interface MailEnvelope {
|
||||
uid: number;
|
||||
subject: string;
|
||||
from: { name: string; address: string }[];
|
||||
to: { name: string; address: string }[];
|
||||
date: string;
|
||||
seen: boolean;
|
||||
flagged: boolean;
|
||||
hasAttachments: boolean;
|
||||
}
|
||||
|
||||
export interface MailMessage extends MailEnvelope {
|
||||
cc: { name: string; address: string }[];
|
||||
html: string;
|
||||
text: string;
|
||||
attachments: { filename: string; contentType: string; size: number }[];
|
||||
}
|
||||
|
||||
export default function MailPage() {
|
||||
const [connected, setConnected] = useState<boolean | null>(null);
|
||||
const [email, setEmail] = useState("");
|
||||
const [folders, setFolders] = useState<MailFolder[]>([]);
|
||||
const [activeFolder, setActiveFolder] = useState("INBOX");
|
||||
const [messages, setMessages] = useState<MailEnvelope[]>([]);
|
||||
const [selectedUid, setSelectedUid] = useState<number | null>(null);
|
||||
const [openMessage, setOpenMessage] = useState<MailMessage | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showCompose, setShowCompose] = useState(false);
|
||||
const [replyTo, setReplyTo] = useState<MailMessage | null>(null);
|
||||
|
||||
// Check connection
|
||||
useEffect(() => {
|
||||
fetch("/api/mail/auth")
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
setConnected(d.connected);
|
||||
if (d.email) setEmail(d.email);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Load folders
|
||||
const loadFolders = useCallback(async () => {
|
||||
const res = await fetch("/api/mail/folders");
|
||||
if (res.ok) setFolders(await res.json());
|
||||
}, []);
|
||||
|
||||
// Load messages
|
||||
const loadMessages = useCallback(async (folder: string) => {
|
||||
setLoading(true);
|
||||
const res = await fetch(`/api/mail/messages?folder=${encodeURIComponent(folder)}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setMessages(data.messages ?? []);
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (connected) {
|
||||
loadFolders();
|
||||
loadMessages(activeFolder);
|
||||
}
|
||||
}, [connected, activeFolder, loadFolders, loadMessages]);
|
||||
|
||||
// Open message
|
||||
const openMsg = async (uid: number) => {
|
||||
setSelectedUid(uid);
|
||||
const res = await fetch(`/api/mail/messages/${uid}?folder=${encodeURIComponent(activeFolder)}`);
|
||||
if (res.ok) {
|
||||
const msg = await res.json();
|
||||
setOpenMessage(msg);
|
||||
// Mark as read in list
|
||||
setMessages((prev) => prev.map((m) => m.uid === uid ? { ...m, seen: true } : m));
|
||||
}
|
||||
};
|
||||
|
||||
// Delete
|
||||
const handleDelete = async (uid: number) => {
|
||||
await fetch("/api/mail/messages", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "delete", folder: activeFolder, uid }),
|
||||
});
|
||||
setMessages((prev) => prev.filter((m) => m.uid !== uid));
|
||||
if (selectedUid === uid) { setSelectedUid(null); setOpenMessage(null); }
|
||||
};
|
||||
|
||||
// Reply
|
||||
const handleReply = (msg: MailMessage) => {
|
||||
setReplyTo(msg);
|
||||
setShowCompose(true);
|
||||
};
|
||||
|
||||
// Disconnect
|
||||
const handleDisconnect = async () => {
|
||||
await fetch("/api/mail/auth", { method: "DELETE" });
|
||||
setConnected(false);
|
||||
setEmail("");
|
||||
setFolders([]);
|
||||
setMessages([]);
|
||||
setOpenMessage(null);
|
||||
};
|
||||
|
||||
if (connected === null) {
|
||||
return <div className="empty-state"><span className="spinner" style={{ width: 24, height: 24 }} /></div>;
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
return <MailLogin onSuccess={(e) => { setConnected(true); setEmail(e); }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mail-layout">
|
||||
<div className="mail-sidebar">
|
||||
<button className="btn btn-primary" style={{ width: "100%" }} onClick={() => { setReplyTo(null); setShowCompose(true); }}>
|
||||
<ComposeIcon /> Yeni Mail
|
||||
</button>
|
||||
<FolderList
|
||||
folders={folders}
|
||||
active={activeFolder}
|
||||
onSelect={(f) => { setActiveFolder(f); setSelectedUid(null); setOpenMessage(null); }}
|
||||
/>
|
||||
<div className="mail-account">
|
||||
<div className="mail-account-email">{email}</div>
|
||||
<button className="btn btn-ghost btn-sm" onClick={handleDisconnect} style={{ fontSize: 11 }}>
|
||||
Çıkış
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mail-list">
|
||||
<div className="mail-list-header">
|
||||
<h2>
|
||||
{folders.find((f) => f.path === activeFolder)?.name ?? activeFolder}
|
||||
</h2>
|
||||
<button className="btn btn-ghost btn-sm" onClick={() => loadMessages(activeFolder)}>↻</button>
|
||||
</div>
|
||||
<MessageList
|
||||
messages={messages}
|
||||
loading={loading}
|
||||
selectedUid={selectedUid}
|
||||
onSelect={openMsg}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mail-detail">
|
||||
{openMessage ? (
|
||||
<MessageView
|
||||
message={openMessage}
|
||||
onReply={() => handleReply(openMessage)}
|
||||
onDelete={() => handleDelete(openMessage.uid)}
|
||||
folder={activeFolder}
|
||||
/>
|
||||
) : (
|
||||
<div className="mail-empty">
|
||||
<div className="mail-empty-icon">
|
||||
<MailBigIcon />
|
||||
</div>
|
||||
<div style={{ fontWeight: 600, fontSize: 14, color: "var(--text-secondary)" }}>Bir mail seçin</div>
|
||||
<div style={{ fontSize: 12 }}>Okumak için soldaki listeden bir mail seçin</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showCompose && (
|
||||
<ComposeModal
|
||||
replyTo={replyTo}
|
||||
onClose={() => { setShowCompose(false); setReplyTo(null); }}
|
||||
onSent={() => { setShowCompose(false); setReplyTo(null); loadMessages(activeFolder); }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MailBigIcon() {
|
||||
return <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ opacity: 0.5 }}><rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>;
|
||||
}
|
||||
|
||||
function ComposeIcon() {
|
||||
return <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>;
|
||||
}
|
||||
Reference in New Issue
Block a user