feat: complete i18n support, telegram webhook, and security improvements
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { useState, useRef, useCallback } from "react";
|
||||
import type { MailMessage } from "@/app/[lang]/dashboard/mail/page";
|
||||
import { formatBytes } from "@/lib/format";
|
||||
import { useDictionary } from "@/components/DictionaryContext";
|
||||
|
||||
interface AttachmentFile {
|
||||
file: File;
|
||||
@@ -14,6 +15,7 @@ export default function ComposeModal({ replyTo, onClose, onSent }: {
|
||||
onClose: () => void;
|
||||
onSent: () => void;
|
||||
}) {
|
||||
const dict = useDictionary();
|
||||
const [to, setTo] = useState(replyTo ? replyTo.from[0]?.address ?? "" : "");
|
||||
const [cc, setCc] = useState("");
|
||||
const [subject, setSubject] = useState(replyTo ? `Re: ${replyTo.subject.replace(/^Re:\s*/i, "")}` : "");
|
||||
@@ -60,7 +62,7 @@ export default function ComposeModal({ replyTo, onClose, onSent }: {
|
||||
|
||||
const res = await fetch("/api/mail/send", { method: "POST", body: formData });
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || "Gönderilemedi");
|
||||
if (!res.ok) throw new Error(data.error || dict.mailClient.sendError || "Gönderilemedi");
|
||||
} else {
|
||||
// JSON for simple messages
|
||||
const res = await fetch("/api/mail/send", {
|
||||
@@ -72,7 +74,7 @@ export default function ComposeModal({ replyTo, onClose, onSent }: {
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || "Gönderilemedi");
|
||||
if (!res.ok) throw new Error(data.error || dict.mailClient.sendError || "Gönderilemedi");
|
||||
}
|
||||
onSent();
|
||||
} catch (err: any) {
|
||||
@@ -87,24 +89,24 @@ export default function ComposeModal({ replyTo, onClose, onSent }: {
|
||||
<div className="modal-overlay" onClick={(e) => e.target === e.currentTarget && onClose()}>
|
||||
<div className="modal" style={{ maxWidth: 620 }}>
|
||||
<div className="modal-header">
|
||||
<h2 className="modal-title">{replyTo ? "Yanıtla" : "Yeni Mail"}</h2>
|
||||
<h2 className="modal-title">{replyTo ? (dict.mailClient.reply || "Yanıtla") : (dict.mailClient.composeTitle || "Yeni Mail")}</h2>
|
||||
<button className="modal-close" onClick={onClose}>✕</button>
|
||||
</div>
|
||||
<form onSubmit={handleSend}>
|
||||
<div className="modal-body form-group">
|
||||
{error && <div className="error-msg">{error}</div>}
|
||||
<div>
|
||||
<label className="label">Alıcı</label>
|
||||
<label className="label">{dict.mailClient.to || "Alıcı"}</label>
|
||||
<input type="email" className="input" placeholder="alici@domain.com" value={to}
|
||||
onChange={(e) => setTo(e.target.value)} required autoFocus />
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">CC (isteğe bağlı)</label>
|
||||
<label className="label">{dict.mailClient.cc || "CC"}</label>
|
||||
<input type="text" className="input" placeholder="cc@domain.com" value={cc}
|
||||
onChange={(e) => setCc(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Konu</label>
|
||||
<label className="label">{dict.mailClient.subject || "Konu"}</label>
|
||||
<input type="text" className="input" value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)} required />
|
||||
</div>
|
||||
@@ -131,7 +133,7 @@ export default function ComposeModal({ replyTo, onClose, onSent }: {
|
||||
onChange={(e) => { addFiles(e.target.files); e.target.value = ""; }}
|
||||
/>
|
||||
<span style={{ fontSize: 20 }}>📎</span>
|
||||
<span>Dosya sürükleyin veya tıklayın</span>
|
||||
<span>{dict.mailClient.dropFiles || "Dosya sürükleyin veya tıklayın"}</span>
|
||||
</div>
|
||||
|
||||
{/* Attachment list */}
|
||||
@@ -145,15 +147,15 @@ export default function ComposeModal({ replyTo, onClose, onSent }: {
|
||||
</div>
|
||||
))}
|
||||
<div style={{ fontSize: 11, color: "var(--text-muted)", marginTop: 4 }}>
|
||||
Toplam: {formatBytes(totalSize)} — {attachments.length} dosya
|
||||
{formatBytes(totalSize)} — {attachments.length} {dict.mailClient.attachments || "dosya"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-ghost" onClick={onClose}>İptal</button>
|
||||
<button type="button" className="btn btn-ghost" onClick={onClose}>{dict.mailClient.cancel || "İptal"}</button>
|
||||
<button type="submit" className="btn btn-primary" disabled={sending}>
|
||||
{sending ? <span className="spinner" /> : <SendIcon />} Gönder
|
||||
{sending ? <span className="spinner" /> : <SendIcon />} {dict.mailClient.send || "Gönder"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
import type { MailFolder } from "@/app/[lang]/dashboard/mail/page";
|
||||
import { useDictionary } from "@/components/DictionaryContext";
|
||||
|
||||
const FOLDER_ICONS: Record<string, string> = {
|
||||
"\\Inbox": "📥",
|
||||
@@ -10,15 +11,6 @@ const FOLDER_ICONS: Record<string, string> = {
|
||||
"\\Archive": "📦",
|
||||
};
|
||||
|
||||
const FOLDER_LABELS: Record<string, string> = {
|
||||
INBOX: "Gelen Kutusu",
|
||||
Sent: "Gönderilenler",
|
||||
Drafts: "Taslaklar",
|
||||
Trash: "Çöp Kutusu",
|
||||
Junk: "Spam",
|
||||
Archive: "Arşiv",
|
||||
};
|
||||
|
||||
function getFolderIcon(folder: MailFolder): string {
|
||||
if (folder.specialUse && FOLDER_ICONS[folder.specialUse]) return FOLDER_ICONS[folder.specialUse];
|
||||
const lower = folder.path.toLowerCase();
|
||||
@@ -31,15 +23,25 @@ function getFolderIcon(folder: MailFolder): string {
|
||||
return "📁";
|
||||
}
|
||||
|
||||
function getFolderLabel(folder: MailFolder): string {
|
||||
return FOLDER_LABELS[folder.name] ?? FOLDER_LABELS[folder.path] ?? folder.name;
|
||||
}
|
||||
|
||||
export default function FolderList({ folders, active, onSelect }: {
|
||||
folders: MailFolder[];
|
||||
active: string;
|
||||
onSelect: (path: string) => void;
|
||||
}) {
|
||||
const dict = useDictionary();
|
||||
|
||||
const getFolderLabel = (folder: MailFolder): string => {
|
||||
const name = folder.name || folder.path;
|
||||
const lower = name.toLowerCase();
|
||||
if (lower === "inbox") return dict.mailClient.inbox || "Inbox";
|
||||
if (lower === "sent") return dict.mailClient.sent || "Sent";
|
||||
if (lower === "drafts") return dict.mailClient.drafts || "Drafts";
|
||||
if (lower === "trash") return dict.mailClient.trash || "Trash";
|
||||
if (lower === "junk" || lower === "spam") return dict.mailClient.junk || "Junk";
|
||||
if (lower === "archive") return dict.mailClient.archive || "Archive";
|
||||
return name;
|
||||
};
|
||||
|
||||
const sorted = [...folders].sort((a, b) => {
|
||||
if (a.path === "INBOX") return -1;
|
||||
if (b.path === "INBOX") return 1;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { useDictionary } from "@/components/DictionaryContext";
|
||||
|
||||
export default function MailLogin({ onSuccess }: { onSuccess: (email: string) => void }) {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const dict = useDictionary();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -30,29 +32,26 @@ export default function MailLogin({ onSuccess }: { onSuccess: (email: string) =>
|
||||
<div className="card" style={{ maxWidth: 420, width: "100%" }}>
|
||||
<div style={{ textAlign: "center", marginBottom: 24 }}>
|
||||
<div style={{ fontSize: 32, marginBottom: 8 }}>📧</div>
|
||||
<h2 style={{ fontSize: 18, fontWeight: 700, color: "var(--text-primary)" }}>Mail Hesabına Bağlan</h2>
|
||||
<h2 style={{ fontSize: 18, fontWeight: 700, color: "var(--text-primary)" }}>{dict.mailClient.loginTitle || "IMAP Girişi"}</h2>
|
||||
<p style={{ fontSize: 13, color: "var(--text-secondary)", marginTop: 4 }}>
|
||||
Mailcow mail hesabınızın bilgilerini girin
|
||||
{dict.mailClient.loginSubtitle || "Mail kutunuza bağlanın"}
|
||||
</p>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} className="form-group">
|
||||
{error && <div className="error-msg">{error}</div>}
|
||||
<div>
|
||||
<label className="label">E-posta Adresi</label>
|
||||
<input type="email" className="input" placeholder="info@domain.com" value={email}
|
||||
<label className="label">{dict.mailClient.emailLabel || "E-posta"}</label>
|
||||
<input type="email" className="input" placeholder={dict.mailClient.emailPlaceholder || "isim@domain.com"} value={email}
|
||||
onChange={(e) => setEmail(e.target.value)} required autoFocus />
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Şifre</label>
|
||||
<input type="password" className="input" placeholder="Mail hesabı şifresi" value={password}
|
||||
<label className="label">{dict.mailClient.passwordLabel || "Şifre"}</label>
|
||||
<input type="password" className="input" placeholder={dict.mailClient.passwordPlaceholder || "********"} value={password}
|
||||
onChange={(e) => setPassword(e.target.value)} required />
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary" style={{ width: "100%" }} disabled={loading}>
|
||||
{loading ? <span className="spinner" /> : "Bağlan"}
|
||||
{loading ? <span className="spinner" /> : (dict.mailClient.connect || "Bağlan")}
|
||||
</button>
|
||||
<p style={{ fontSize: 11, color: "var(--text-muted)", textAlign: "center", marginTop: 8 }}>
|
||||
Şifreniz sunucuda saklanmaz, sadece oturum süresince kullanılır.
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
"use client";
|
||||
import type { MailEnvelope } from "@/app/[lang]/dashboard/mail/page";
|
||||
import { useDictionary } from "@/components/DictionaryContext";
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
function timeAgo(dateStr: string): string {
|
||||
function timeAgo(dateStr: string, lang: string): string {
|
||||
const now = new Date();
|
||||
const d = new Date(dateStr);
|
||||
const diff = now.getTime() - d.getTime();
|
||||
const mins = Math.floor(diff / 60000);
|
||||
if (mins < 1) return "şimdi";
|
||||
if (mins < 60) return `${mins}dk`;
|
||||
if (mins < 1) return lang === "tr" ? "şimdi" : "now";
|
||||
if (mins < 60) return `${mins}${lang === "tr" ? "dk" : "m"}`;
|
||||
const hrs = Math.floor(mins / 60);
|
||||
if (hrs < 24) return `${hrs}sa`;
|
||||
if (hrs < 24) return `${hrs}${lang === "tr" ? "sa" : "h"}`;
|
||||
const days = Math.floor(hrs / 24);
|
||||
if (days < 7) return `${days}g`;
|
||||
return d.toLocaleDateString("tr-TR", { day: "numeric", month: "short" });
|
||||
if (days < 7) return `${days}${lang === "tr" ? "g" : "d"}`;
|
||||
return d.toLocaleDateString(lang === "tr" ? "tr-TR" : "en-US", { day: "numeric", month: "short" });
|
||||
}
|
||||
|
||||
function senderName(msg: MailEnvelope): string {
|
||||
const f = msg.from[0];
|
||||
return f?.name || f?.address || "Bilinmeyen";
|
||||
return f?.name || f?.address || "Unknown";
|
||||
}
|
||||
|
||||
export default function MessageList({ messages, loading, selectedUid, onSelect, onDelete }: {
|
||||
@@ -27,6 +29,10 @@ export default function MessageList({ messages, loading, selectedUid, onSelect,
|
||||
onSelect: (uid: number) => void;
|
||||
onDelete: (uid: number) => void;
|
||||
}) {
|
||||
const dict = useDictionary();
|
||||
const params = useParams();
|
||||
const lang = (params.lang as string) || "en";
|
||||
|
||||
if (loading) {
|
||||
return <div className="empty-state" style={{ padding: 40 }}><span className="spinner" style={{ width: 20, height: 20 }} /></div>;
|
||||
}
|
||||
@@ -34,7 +40,7 @@ export default function MessageList({ messages, loading, selectedUid, onSelect,
|
||||
if (messages.length === 0) {
|
||||
return (
|
||||
<div className="empty-state" style={{ padding: 40 }}>
|
||||
<div style={{ fontSize: 13, color: "var(--text-muted)" }}>Bu klasörde mail yok</div>
|
||||
<div style={{ fontSize: 13, color: "var(--text-muted)" }}>{dict.mailClient.emptyFolder || "Bu klasör boş"}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -53,11 +59,11 @@ export default function MessageList({ messages, loading, selectedUid, onSelect,
|
||||
<div className="message-content">
|
||||
<div className="message-top">
|
||||
<span className="message-sender">{senderName(m)}</span>
|
||||
<span className="message-time">{timeAgo(m.date)}</span>
|
||||
<span className="message-time">{timeAgo(m.date, lang)}</span>
|
||||
</div>
|
||||
<div className="message-subject">{m.subject}</div>
|
||||
<div className="message-subject">{m.subject || dict.mailClient.noSubject || "(Konu Yok)"}</div>
|
||||
</div>
|
||||
{m.hasAttachments && <span className="message-attach" title="Ek var">📎</span>}
|
||||
{m.hasAttachments && <span className="message-attach" title={dict.mailClient.attachments || "Ekler"}>📎</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
import type { MailMessage } from "@/app/[lang]/dashboard/mail/page";
|
||||
import { formatBytes } from "@/lib/format";
|
||||
import { useDictionary } from "@/components/DictionaryContext";
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
function getFileIcon(contentType: string, filename: string): string {
|
||||
if (contentType.startsWith("image/")) return "🖼️";
|
||||
@@ -24,8 +26,11 @@ export default function MessageView({ message, onReply, onDelete, folder }: {
|
||||
onDelete: () => void;
|
||||
folder: string;
|
||||
}) {
|
||||
const dict = useDictionary();
|
||||
const params = useParams();
|
||||
const lang = (params.lang as string) || "en";
|
||||
const from = message.from[0];
|
||||
const date = new Date(message.date).toLocaleString("tr-TR", {
|
||||
const date = new Date(message.date).toLocaleString(lang === "tr" ? "tr-TR" : "en-US", {
|
||||
day: "numeric", month: "long", year: "numeric", hour: "2-digit", minute: "2-digit",
|
||||
});
|
||||
|
||||
@@ -48,13 +53,13 @@ export default function MessageView({ message, onReply, onDelete, folder }: {
|
||||
<div className="message-view">
|
||||
{/* Header */}
|
||||
<div className="message-view-header">
|
||||
<h2 className="message-view-subject">{message.subject}</h2>
|
||||
<h2 className="message-view-subject">{message.subject || dict.mailClient.noSubject || "(Konu Yok)"}</h2>
|
||||
<div className="message-view-actions">
|
||||
<button className="btn btn-ghost btn-sm" onClick={onReply} title="Yanıtla">
|
||||
<ReplyIcon /> Yanıtla
|
||||
<button className="btn btn-ghost btn-sm" onClick={onReply} title={dict.mailClient.reply || "Yanıtla"}>
|
||||
<ReplyIcon /> {dict.mailClient.reply || "Yanıtla"}
|
||||
</button>
|
||||
<button className="btn btn-danger btn-sm" onClick={onDelete} title="Sil">
|
||||
<TrashIcon /> Sil
|
||||
<button className="btn btn-danger btn-sm" onClick={onDelete} title={dict.mailClient.delete || "Sil"}>
|
||||
<TrashIcon /> {dict.mailClient.delete || "Sil"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,7 +81,7 @@ export default function MessageView({ message, onReply, onDelete, folder }: {
|
||||
{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
|
||||
📎 {message.attachments.length} {dict.mailClient.attachments || "ek"}
|
||||
</div>
|
||||
{message.attachments.map((att, i) => (
|
||||
<div key={i} className="attachment-chip" onClick={() => handleAttachment(att, false)}>
|
||||
@@ -89,7 +94,7 @@ export default function MessageView({ message, onReply, onDelete, folder }: {
|
||||
<button
|
||||
className="att-btn"
|
||||
onClick={(e) => { e.stopPropagation(); handleAttachment(att, false); }}
|
||||
title="İndir"
|
||||
title={dict.mailClient.download || "İndir"}
|
||||
>
|
||||
⬇
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user