feat: complete i18n support, telegram webhook, and security improvements

This commit is contained in:
AyrisAI
2026-05-14 13:46:17 +03:00
parent 4c9a07e3ef
commit cc65a2bd72
23 changed files with 798 additions and 205 deletions

View File

@@ -2,6 +2,7 @@
import { useState, useEffect, useTransition } from "react";
import { formatBytes } from "@/lib/format";
import { useDictionary } from "@/components/DictionaryContext";
interface Domain {
domain_name: string;
@@ -23,6 +24,7 @@ export default function DomainsPage() {
const [search, setSearch] = useState("");
const [form, setForm] = useState({ domain: "", description: "", mailboxes: "10", quota: "10240", maxquota: "10240" });
const [formError, setFormError] = useState("");
const dict = useDictionary();
const fetchDomains = async () => {
setLoading(true);
@@ -54,14 +56,23 @@ export default function DomainsPage() {
fetchDomains();
} else {
const data = await res.json();
const msg = Array.isArray(data) ? data.map((d: { msg?: string }) => d.msg).join(", ") : (data?.error ?? "Bir hata oluştu");
setFormError(String(msg));
let msg = "Bir hata oluştu";
if (Array.isArray(data)) {
msg = data.map((d: any) => {
if (typeof d.msg === "string") return d.msg;
if (Array.isArray(d.msg)) return d.msg.join(", ");
return JSON.stringify(d.msg || d);
}).join(" | ");
} else if (data?.error) {
msg = data.error;
}
setFormError(msg);
}
});
};
const handleDelete = (domain: string) => {
if (!confirm(`"${domain}" domainini Mailcow'dan silmek istediğinizden emin misiniz?\n\nBu işlem geri alınamaz!`)) return;
if (!confirm(`"${domain}" domainini silmek istediğinizden emin misiniz?\n\nBu işlem geri alınamaz!`)) return;
startTransition(async () => {
await fetch(`/api/domains/${encodeURIComponent(domain)}`, { method: "DELETE" });
fetchDomains();
@@ -78,11 +89,11 @@ export default function DomainsPage() {
<>
<div className="page-header">
<div>
<h1 className="page-title">Domainler</h1>
<p className="page-subtitle">Mailcow üzerindeki tüm domainleri yönetin</p>
<h1 className="page-title">{dict.domains.title || "Domainler"}</h1>
<p className="page-subtitle">{domains.length} {dict.domains.subtitle || "domain listeleniyor"}</p>
</div>
<button className="btn btn-primary" onClick={() => setShowModal(true)}>
<PlusIcon /> Domain Ekle
<PlusIcon /> {dict.domains.addDomain || "Domain Ekle"}
</button>
</div>
@@ -93,13 +104,13 @@ export default function DomainsPage() {
<input
type="text"
className="input search-input"
placeholder="Domain veya açıklama ara..."
placeholder={dict.domains.searchPlaceholder || "Domain ara..."}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<button className="btn btn-ghost" onClick={fetchDomains}>
<RefreshIcon /> Yenile
<RefreshIcon /> {dict.domains.refresh || "Yenile"}
</button>
</div>
@@ -109,19 +120,19 @@ export default function DomainsPage() {
) : filtered.length === 0 ? (
<div className="empty-state">
<div className="empty-icon"><GlobeIcon size={24} /></div>
<div style={{ fontWeight: 600 }}>Domain bulunamadı</div>
<div style={{ fontSize: 12 }}>Mailcow&apos;a domain eklemek için &quot;Domain Ekle&quot; butonuna tıklayın.</div>
<div style={{ fontWeight: 600 }}>{dict.domains.noDomains || "Domain bulunamadı"}</div>
<div style={{ fontSize: 12 }}>{dict.domains.tryDiffSearch || "Farklı bir arama yapın."}</div>
</div>
) : (
<table>
<thead>
<tr>
<th>Domain</th>
<th>Mail Kutuları</th>
<th>Alias</th>
<th>Kota</th>
<th>Durum</th>
<th>İşlemler</th>
<th>{dict.domains.domain || "Domain"}</th>
<th>{dict.domains.mailboxes || "Mail Kutuları"}</th>
<th>{dict.domains.aliases || "Alias"}</th>
<th>{dict.domains.quota || "Kota"}</th>
<th>{dict.domains.status || "Durum"}</th>
<th>{dict.domains.actions || "İşlemler"}</th>
</tr>
</thead>
<tbody>
@@ -160,7 +171,7 @@ export default function DomainsPage() {
</td>
<td>
<span className={`badge ${String(d.active) === "1" ? "badge-green" : "badge-red"}`}>
{String(d.active) === "1" ? "● Aktif" : "● Pasif"}
{String(d.active) === "1" ? `${dict.domains.active || "Aktif"}` : `${dict.domains.inactive || "Pasif"}`}
</span>
</td>
<td>
@@ -181,20 +192,20 @@ export default function DomainsPage() {
<div className="modal-overlay" onClick={(e) => e.target === e.currentTarget && setShowModal(false)}>
<div className="modal">
<div className="modal-header">
<h2 className="modal-title">Mailcow&apos;a Domain Ekle</h2>
<h2 className="modal-title">{dict.domains.addDomain || "Domain Ekle"}</h2>
<button className="modal-close" onClick={() => setShowModal(false)}><XIcon /></button>
</div>
<form onSubmit={handleCreate}>
<div className="modal-body form-group">
{formError && <div className="error-msg">{formError}</div>}
<div>
<label className="label">Domain Adı</label>
<label className="label">{dict.domains.domain || "Domain Adı"}</label>
<input type="text" className="input" placeholder="ornek.com" value={form.domain}
onChange={(e) => setForm({ ...form, domain: e.target.value })} required />
</div>
<div>
<label className="label">ıklama (isteğe bağlı)</label>
<input type="text" className="input" placeholder="Bu domain hakkında..." value={form.description}
<label className="label">{dict.domains.description || "Açıklama"}</label>
<input type="text" className="input" placeholder="" value={form.description}
onChange={(e) => setForm({ ...form, description: e.target.value })} />
</div>
<div className="form-row">

View File

@@ -3,6 +3,7 @@ import { redirect } from "next/navigation";
import Providers from "@/components/Providers";
import Sidebar from "@/components/Sidebar";
import { getDictionary, Locale } from "@/app/dictionaries";
import { DictionaryProvider } from "@/components/DictionaryContext";
export default async function DashboardLayout(
props: {
@@ -23,10 +24,12 @@ export default async function DashboardLayout(
return (
<Providers>
<div className="app-layout">
<Sidebar dict={dict.sidebar} lang={params.lang} />
<div className="main-content">{children}</div>
</div>
<DictionaryProvider dictionary={dict}>
<div className="app-layout">
<Sidebar dict={dict} lang={params.lang} />
<div className="main-content">{children}</div>
</div>
</DictionaryProvider>
</Providers>
);
}

View File

@@ -1,11 +1,13 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { useParams } from "next/navigation";
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";
import { useDictionary } from "@/components/DictionaryContext";
export interface MailFolder {
name: string;
@@ -34,6 +36,8 @@ export interface MailMessage extends MailEnvelope {
}
export default function MailPage() {
const params = useParams();
const lang = params.lang as string;
const [connected, setConnected] = useState<boolean | null>(null);
const [email, setEmail] = useState("");
const [folders, setFolders] = useState<MailFolder[]>([]);
@@ -44,6 +48,20 @@ export default function MailPage() {
const [loading, setLoading] = useState(false);
const [showCompose, setShowCompose] = useState(false);
const [replyTo, setReplyTo] = useState<MailMessage | null>(null);
const dict = useDictionary();
const getFolderLabel = (path: string): string => {
const folder = folders.find((f) => f.path === path);
const name = folder?.name || 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;
};
// Check connection
useEffect(() => {
@@ -130,7 +148,7 @@ export default function MailPage() {
<div className="mail-layout">
<div className="mail-sidebar">
<button className="btn btn-primary" style={{ width: "100%" }} onClick={() => { setReplyTo(null); setShowCompose(true); }}>
<ComposeIcon /> Yeni Mail
<ComposeIcon /> {dict.mailClient.newMail || "Yeni Mail"}
</button>
<FolderList
folders={folders}
@@ -140,7 +158,7 @@ export default function MailPage() {
<div className="mail-account">
<div className="mail-account-email">{email}</div>
<button className="btn btn-ghost btn-sm" onClick={handleDisconnect} style={{ fontSize: 11 }}>
Çıkış
{dict.mailClient.logout || ıkış"}
</button>
</div>
</div>
@@ -148,7 +166,7 @@ export default function MailPage() {
<div className="mail-list">
<div className="mail-list-header">
<h2>
{folders.find((f) => f.path === activeFolder)?.name ?? activeFolder}
{getFolderLabel(activeFolder)}
</h2>
<button className="btn btn-ghost btn-sm" onClick={() => loadMessages(activeFolder)}></button>
</div>
@@ -174,8 +192,8 @@ export default function MailPage() {
<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 style={{ fontWeight: 600, fontSize: 14, color: "var(--text-secondary)" }}>{dict.mailClient.selectMail || "Bir mail seçin"}</div>
<div style={{ fontSize: 12 }}>{dict.mailClient.selectMailDesc || "Okumak için soldaki listeden bir mail seçin"}</div>
</div>
)}
</div>

View File

@@ -3,6 +3,7 @@
import { useState, useEffect, useTransition, useCallback } from "react";
import { useSession } from "next-auth/react";
import { formatBytes } from "@/lib/format";
import { useDictionary } from "@/components/DictionaryContext";
interface Mailbox {
username: string;
@@ -32,6 +33,7 @@ export default function MailboxesPage() {
const [createForm, setCreateForm] = useState({ local_part: "", name: "", password: "", quota: 3072 });
const [newPassword, setNewPassword] = useState("");
const [formError, setFormError] = useState("");
const dict = useDictionary();
useEffect(() => {
fetch("/api/domains")
@@ -81,7 +83,7 @@ export default function MailboxesPage() {
};
const handleDelete = (username: string) => {
if (!confirm(`"${username}" hesabını silmek istediğinizden emin misiniz?`)) return;
if (!confirm(`"${username}" ${dict.mailboxes.deleteConfirm || "hesabını silmek istediğinizden emin misiniz?"}`)) return;
startTransition(async () => {
await fetch(`/api/mailboxes/${encodeURIComponent(username)}`, { method: "DELETE" });
setMailboxes((prev) => prev.filter((m) => m.username !== username));
@@ -128,11 +130,11 @@ export default function MailboxesPage() {
<>
<div className="page-header">
<div>
<h1 className="page-title">Mail Hesapları</h1>
<h1 className="page-title">{dict.mailboxes.title || "Mail Hesapları"}</h1>
<p className="page-subtitle">
{selectedDomain
? `${selectedDomain}${mailboxes.length} hesap`
: "Domain seçin"}
? `${selectedDomain}${mailboxes.length} ${dict.mailboxes.accounts || "hesap"}`
: (dict.mailboxes.selectDomain || "Domain seçin")}
</p>
</div>
<div style={{ display: "flex", gap: 10, alignItems: "center" }}>
@@ -160,7 +162,7 @@ export default function MailboxesPage() {
onClick={() => setShowCreateModal(true)}
disabled={!selectedDomain}
>
<PlusIcon /> Hesap Ekle
<PlusIcon /> {dict.mailboxes.addAccount || "Hesap Ekle"}
</button>
</div>
</div>
@@ -172,7 +174,7 @@ export default function MailboxesPage() {
<input
type="text"
className="input search-input"
placeholder="E-posta veya isim ara..."
placeholder={dict.mailboxes.searchPlaceholder || "E-posta veya isim ara..."}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
@@ -188,21 +190,21 @@ export default function MailboxesPage() {
<div className="empty-state">
<div className="empty-icon"><MailIcon size={24} /></div>
<div style={{ fontWeight: 600 }}>
{selectedDomain ? "Bu domainde mail hesabı yok" : "Domain seçin"}
{selectedDomain ? (dict.mailboxes.noMailboxes || "Bu domainde mail hesabı yok") : (dict.mailboxes.selectDomain || "Domain seçin")}
</div>
<div style={{ fontSize: 12, color: "var(--text-secondary)" }}>
{selectedDomain ? '"Hesap Ekle" butonuna tıklayın' : "Sol üstteki listeden domain seçin"}
{selectedDomain ? (dict.mailboxes.noMailboxesDesc || "\"Hesap Ekle\" butonuna tıklayın") : (dict.mailboxes.selectDomainDesc || "Sol üstteki listeden domain seçin")}
</div>
</div>
) : (
<table>
<thead>
<tr>
<th>E-posta</th>
<th>Ad Soyad</th>
<th>Kota</th>
<th>Durum</th>
<th>İşlemler</th>
<th>{dict.mailboxes.email || "E-posta"}</th>
<th>{dict.mailboxes.name || "Ad Soyad"}</th>
<th>{dict.mailboxes.quota || "Kota"}</th>
<th>{dict.mailboxes.status || "Durum"}</th>
<th>{dict.mailboxes.actions || "İşlemler"}</th>
</tr>
</thead>
<tbody>
@@ -237,7 +239,7 @@ export default function MailboxesPage() {
</td>
<td>
<span className={`badge ${String(m.active) === "1" ? "badge-green" : "badge-red"}`}>
{String(m.active) === "1" ? "● Aktif" : "● Pasif"}
{String(m.active) === "1" ? `${dict.mailboxes.active || "Aktif"}` : `${dict.mailboxes.inactive || "Pasif"}`}
</span>
</td>
<td>
@@ -245,28 +247,28 @@ export default function MailboxesPage() {
<button
className="btn btn-ghost btn-sm"
onClick={() => setShowInfoModal(m.username)}
title="Bağlantı Bilgileri"
title={dict.mailboxes.info || "Bağlantı Bilgileri"}
>
<InfoIcon />
</button>
<button
className="btn btn-ghost btn-sm"
onClick={() => setShowPasswordModal(m.username)}
title="Şifre Değiştir"
title={dict.mailboxes.changePassword || "Şifre Değiştir"}
>
<KeyIcon />
</button>
<button
className={`btn btn-sm ${String(m.active) === "1" ? "btn-ghost" : "btn-success"}`}
onClick={() => handleToggle(m.username, m.active)}
title={String(m.active) === "1" ? "Pasife Al" : "Aktif Et"}
title={String(m.active) === "1" ? (dict.mailboxes.deactivate || "Pasife Al") : (dict.mailboxes.activate || "Aktif Et")}
>
{String(m.active) === "1" ? <PauseIcon /> : <PlayIcon />}
</button>
<button
className="btn btn-danger btn-sm"
onClick={() => handleDelete(m.username)}
title="Sil"
title={dict.mailboxes.delete || "Sil"}
>
<TrashIcon />
</button>
@@ -286,14 +288,14 @@ export default function MailboxesPage() {
<div className="modal-overlay" onClick={(e) => e.target === e.currentTarget && setShowCreateModal(false)}>
<div className="modal">
<div className="modal-header">
<h2 className="modal-title">Yeni Mail Hesabı</h2>
<h2 className="modal-title">{dict.mailboxes.newAccount || "Yeni Mail Hesabı"}</h2>
<button className="modal-close" onClick={() => setShowCreateModal(false)}><XIcon /></button>
</div>
<form onSubmit={handleCreate}>
<div className="modal-body form-group">
{formError && <div className="error-msg">{formError}</div>}
<div>
<label className="label">Kullanıcı Adı</label>
<label className="label">{dict.mailboxes.username || "Kullanıcı Adı"}</label>
<div style={{ display: "flex" }}>
<input
type="text" className="input"
@@ -313,29 +315,29 @@ export default function MailboxesPage() {
</div>
</div>
<div>
<label className="label">Ad Soyad</label>
<label className="label">{dict.mailboxes.name || "Ad Soyad"}</label>
<input type="text" className="input" placeholder="Emina Karabudak"
value={createForm.name}
onChange={(e) => setCreateForm({ ...createForm, name: e.target.value })}
required />
</div>
<div>
<label className="label">Şifre</label>
<input type="password" className="input" placeholder="Güçlü bir şifre"
<label className="label">{dict.mailboxes.password || "Şifre"}</label>
<input type="password" className="input" placeholder="********"
value={createForm.password}
onChange={(e) => setCreateForm({ ...createForm, password: e.target.value })}
required />
</div>
<div>
<label className="label">Kota (MB)</label>
<label className="label">{dict.mailboxes.quotaMb || "Kota (MB)"}</label>
<input type="number" className="input" value={createForm.quota} min={100} max={102400}
onChange={(e) => setCreateForm({ ...createForm, quota: parseInt(e.target.value) || 3072 })} />
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-ghost" onClick={() => setShowCreateModal(false)}>İptal</button>
<button type="button" className="btn btn-ghost" onClick={() => setShowCreateModal(false)}>{dict.mailboxes.cancel || "İptal"}</button>
<button type="submit" className="btn btn-primary" disabled={isPending}>
{isPending ? <span className="spinner" /> : <PlusIcon />} Oluştur
{isPending ? <span className="spinner" /> : <PlusIcon />} {dict.mailboxes.create || "Oluştur"}
</button>
</div>
</form>
@@ -348,26 +350,26 @@ export default function MailboxesPage() {
<div className="modal-overlay" onClick={(e) => e.target === e.currentTarget && setShowPasswordModal(null)}>
<div className="modal">
<div className="modal-header">
<h2 className="modal-title">Şifre Değiştir</h2>
<h2 className="modal-title">{dict.mailboxes.changePassword || ifre Değiştir"}</h2>
<button className="modal-close" onClick={() => setShowPasswordModal(null)}><XIcon /></button>
</div>
<form onSubmit={handlePasswordChange}>
<div className="modal-body form-group">
<div style={{ fontSize: 13, color: "var(--text-secondary)" }}>
<strong style={{ color: "var(--text-primary)" }}>{showPasswordModal}</strong> için yeni şifre
<strong style={{ color: "var(--text-primary)" }}>{showPasswordModal}</strong> {dict.mailboxes.newPasswordFor || "için yeni şifre"}
</div>
<div>
<label className="label">Yeni Şifre</label>
<input type="password" className="input" placeholder="Yeni şifre"
<label className="label">{dict.mailboxes.password || "Yeni Şifre"}</label>
<input type="password" className="input" placeholder="********"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required autoFocus />
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-ghost" onClick={() => setShowPasswordModal(null)}>İptal</button>
<button type="button" className="btn btn-ghost" onClick={() => setShowPasswordModal(null)}>{dict.mailboxes.cancel || "İptal"}</button>
<button type="submit" className="btn btn-primary" disabled={isPending}>
{isPending ? <span className="spinner" /> : <KeyIcon />} Güncelle
{isPending ? <span className="spinner" /> : <KeyIcon />} {dict.mailboxes.update || "Güncelle"}
</button>
</div>
</form>
@@ -379,49 +381,49 @@ export default function MailboxesPage() {
<div className="modal-overlay" onClick={(e) => e.target === e.currentTarget && setShowInfoModal(null)}>
<div className="modal">
<div className="modal-header">
<h2 className="modal-title">İstemci Bağlantı Bilgileri</h2>
<h2 className="modal-title">{dict.mailboxes.connectionInfo || stemci Bağlantı Bilgileri"}</h2>
<button className="modal-close" onClick={() => setShowInfoModal(null)}><XIcon /></button>
</div>
<div className="modal-body form-group">
<div style={{ fontSize: 13, color: "var(--text-secondary)", marginBottom: 10 }}>
<strong>{showInfoModal}</strong> hesabını Apple Mail, Outlook veya telefonunuza kurmak için aşağıdaki bilgileri kullanın:
<strong>{showInfoModal}</strong> {dict.mailboxes.connectionInfoDesc || "hesabını kurmak için aşağıdaki bilgileri kullanın:"}
</div>
<div style={{ background: "var(--bg-hover)", padding: 12, borderRadius: "var(--radius)", border: "1px solid var(--border)", fontSize: 13 }}>
<div style={{ marginBottom: 12 }}>
<div style={{ color: "var(--text-secondary)", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: 0.5 }}>IMAP (Gelen Sunucu)</div>
<div style={{ color: "var(--text-secondary)", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: 0.5 }}>{dict.mailboxes.imap || "IMAP (Gelen Sunucu)"}</div>
<div style={{ display: "flex", justifyContent: "space-between", marginTop: 4, alignItems: "center" }}>
<span>Sunucu: <strong>mail.ayris.tech</strong></span>
<span>{dict.mailboxes.server || "Sunucu"}: <strong>mail.ayris.tech</strong></span>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<button className="btn btn-ghost btn-sm" style={{ padding: "4px 8px", fontSize: 11 }} onClick={() => navigator.clipboard.writeText("mail.ayris.tech")} title="Kopyala"><CopyIcon /> Kopyala</button>
<button className="btn btn-ghost btn-sm" style={{ padding: "4px 8px", fontSize: 11 }} onClick={() => navigator.clipboard.writeText("mail.ayris.tech")} title="Kopyala"><CopyIcon /> {dict.mailboxes.copy || "Kopyala"}</button>
</div>
</div>
<div style={{ marginTop: 2 }}>Port: <strong>993</strong> (SSL/TLS)</div>
<div style={{ marginTop: 2 }}>{dict.mailboxes.port || "Port"}: <strong>993</strong> (SSL/TLS)</div>
</div>
<div style={{ marginBottom: 12, paddingTop: 12, borderTop: "1px solid var(--border)" }}>
<div style={{ color: "var(--text-secondary)", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: 0.5 }}>SMTP (Giden Sunucu)</div>
<div style={{ color: "var(--text-secondary)", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: 0.5 }}>{dict.mailboxes.smtp || "SMTP (Giden Sunucu)"}</div>
<div style={{ display: "flex", justifyContent: "space-between", marginTop: 4, alignItems: "center" }}>
<span>Sunucu: <strong>mail.ayris.tech</strong></span>
<span>{dict.mailboxes.server || "Sunucu"}: <strong>mail.ayris.tech</strong></span>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<button className="btn btn-ghost btn-sm" style={{ padding: "4px 8px", fontSize: 11 }} onClick={() => navigator.clipboard.writeText("mail.ayris.tech")} title="Kopyala"><CopyIcon /> Kopyala</button>
<button className="btn btn-ghost btn-sm" style={{ padding: "4px 8px", fontSize: 11 }} onClick={() => navigator.clipboard.writeText("mail.ayris.tech")} title="Kopyala"><CopyIcon /> {dict.mailboxes.copy || "Kopyala"}</button>
</div>
</div>
<div style={{ marginTop: 2 }}>Port: <strong>587</strong> (STARTTLS) <span style={{ color: "var(--text-secondary)" }}>veya 465 (SSL)</span></div>
<div style={{ marginTop: 2 }}>{dict.mailboxes.port || "Port"}: <strong>587</strong> (STARTTLS) <span style={{ color: "var(--text-secondary)" }}>veya 465 (SSL)</span></div>
</div>
<div style={{ paddingTop: 12, borderTop: "1px solid var(--border)" }}>
<div style={{ color: "var(--text-secondary)", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: 0.5 }}>Kimlik Doğrulama</div>
<div style={{ color: "var(--text-secondary)", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: 0.5 }}>{dict.mailboxes.auth || "Kimlik Doğrulama"}</div>
<div style={{ display: "flex", justifyContent: "space-between", marginTop: 4, alignItems: "center" }}>
<span>Kullanıcı Adı: <strong>{showInfoModal}</strong></span>
<button className="btn btn-ghost btn-sm" style={{ padding: "4px 8px", fontSize: 11 }} onClick={() => navigator.clipboard.writeText(showInfoModal)} title="Kopyala"><CopyIcon /> Kopyala</button>
<span>{dict.mailboxes.username || "Kullanıcı Adı"}: <strong>{showInfoModal}</strong></span>
<button className="btn btn-ghost btn-sm" style={{ padding: "4px 8px", fontSize: 11 }} onClick={() => navigator.clipboard.writeText(showInfoModal)} title="Kopyala"><CopyIcon /> {dict.mailboxes.copy || "Kopyala"}</button>
</div>
<div style={{ color: "var(--text-secondary)", marginTop: 4 }}>Şifre: <span style={{ fontStyle: "italic" }}>Hesap oluştururken belirlediğiniz şifre</span></div>
<div style={{ color: "var(--text-secondary)", marginTop: 4 }}>{dict.mailboxes.password || "Şifre"}: <span style={{ fontStyle: "italic" }}>{dict.mailboxes.authPassword || "Hesap oluştururken belirlediğiniz şifre"}</span></div>
</div>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-primary" onClick={() => setShowInfoModal(null)}>Tamam</button>
<button type="button" className="btn btn-primary" onClick={() => setShowInfoModal(null)}>{dict.mailboxes.ok || "Tamam"}</button>
</div>
</div>
</div>

View File

@@ -2,11 +2,20 @@ import { auth } from "@/auth";
import { getDomains } from "@/lib/mailcow";
import { canAccessDomain } from "@/lib/users";
import { formatBytes } from "@/lib/format";
import { getDictionary, Locale } from "@/app/dictionaries";
export default async function DashboardPage(
props: {
params: Promise<{ lang: string }>;
}
) {
const params = await props.params;
export default async function DashboardPage() {
const session = await auth();
const role = session?.user?.role;
const userDomains = session?.user?.domains ?? [];
const lang = params.lang as Locale;
const dict = await getDictionary(lang);
const allDomains = await getDomains();
const visibleDomains = allDomains.filter((d) => canAccessDomain(userDomains, d.domain_name));
@@ -18,36 +27,36 @@ export default async function DashboardPage() {
<>
<div className="page-header">
<div>
<h1 className="page-title">Dashboard</h1>
<p className="page-subtitle">Hoş geldiniz, {session?.user?.name} 👋</p>
<h1 className="page-title">{dict.dashboard.title || "Dashboard"}</h1>
<p className="page-subtitle">{dict.dashboard.welcome || "Hoş geldiniz"}, {session?.user?.name} 👋</p>
</div>
</div>
<div className="page-body">
<div className="stats-grid">
<StatCard
label="Toplam Domain"
label={dict.dashboard.totalDomains || "Toplam Domain"}
value={visibleDomains.length}
color="var(--accent)"
icon={<GlobeIcon />}
/>
<StatCard
label="Mail Kutuları"
label={dict.dashboard.mailboxes || "Mail Kutuları"}
value={totalMailboxes}
color="var(--success)"
icon={<MailIcon />}
/>
<StatCard
label="Alias"
label={dict.dashboard.aliases || "Alias"}
value={totalAliases}
color="var(--warning)"
icon={<AtIcon />}
/>
{role === "SUPER_ADMIN" && (
<StatCard
label="Tanımlı Kullanıcı"
label={dict.dashboard.users || "Tanımlı Kullanıcı"}
value={"—"}
sub="Kullanıcılar .env'den yönetilir"
sub={dict.dashboard.usersSub || "Kullanıcılar .env'den yönetilir"}
color="var(--text-muted)"
icon={<UsersIcon />}
/>
@@ -59,17 +68,17 @@ export default async function DashboardPage() {
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
<div style={{ padding: "16px 20px", borderBottom: "1px solid var(--border)" }}>
<h2 style={{ fontSize: 14, fontWeight: 700, color: "var(--text-primary)" }}>
Domain Durumu
{dict.dashboard.domainStatus || "Domain Durumu"}
</h2>
</div>
<div className="table-wrap" style={{ border: "none", borderRadius: 0 }}>
<table>
<thead>
<tr>
<th>Domain</th>
<th>Mail Kutuları</th>
<th>Kota Kullanımı</th>
<th>Durum</th>
<th>{dict.dashboard.domain || "Domain"}</th>
<th>{dict.dashboard.mailboxes || "Mail Kutuları"}</th>
<th>{dict.dashboard.quotaUsage || "Kota Kullanımı"}</th>
<th>{dict.dashboard.status || "Durum"}</th>
</tr>
</thead>
<tbody>
@@ -104,7 +113,7 @@ export default async function DashboardPage() {
</td>
<td>
<span className={`badge ${String(d.active) === "1" ? "badge-green" : "badge-red"}`}>
{String(d.active) === "1" ? "● Aktif" : "● Pasif"}
{String(d.active) === "1" ? `${dict.dashboard.active || "Aktif"}` : `${dict.dashboard.inactive || "Pasif"}`}
</span>
</td>
</tr>
@@ -119,31 +128,31 @@ export default async function DashboardPage() {
{/* Quick actions */}
<div className="card">
<h2 style={{ fontSize: 14, fontWeight: 700, marginBottom: 16, color: "var(--text-primary)" }}>
Hızlı İşlemler
{dict.dashboard.quickActions || "Hızlı İşlemler"}
</h2>
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
{role === "SUPER_ADMIN" && (
<QuickItem
href="/dashboard/domains"
href={`/${lang}/dashboard/domains`}
icon={<GlobeIcon />}
title="Domain Yönetimi"
desc="Domain ekle, sil, yönet"
title={dict.dashboard.manageDomains || "Domain Yönetimi"}
desc={dict.dashboard.manageDomainsDesc || "Domain ekle, sil, yönet"}
color="var(--accent)"
/>
)}
<QuickItem
href="/dashboard/mailboxes"
href={`/${lang}/dashboard/mailboxes`}
icon={<MailIcon />}
title="Mail Hesapları"
desc="Yeni hesap oluştur, şifre değiştir, sil"
title={dict.dashboard.manageMailboxes || "Mail Hesapları"}
desc={dict.dashboard.manageMailboxesDesc || "Yeni hesap oluştur, şifre değiştir, sil"}
color="var(--success)"
/>
{role === "SUPER_ADMIN" && (
<QuickItem
href="/dashboard/users"
href={`/${lang}/dashboard/users`}
icon={<UsersIcon />}
title="Kullanıcılar"
desc=".env'den tanımlı panel kullanıcılarını görüntüle"
title={dict.dashboard.manageUsers || "Kullanıcılar"}
desc={dict.dashboard.manageUsersDesc || ".env'den tanımlı panel kullanıcılarını görüntüle"}
color="var(--warning)"
/>
)}

View File

@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect } from "react";
import { useDictionary } from "@/components/DictionaryContext";
interface User {
id: string;
@@ -14,6 +15,7 @@ export default function UsersPage() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const dict = useDictionary();
useEffect(() => {
fetch("/api/users")
@@ -35,8 +37,8 @@ export default function UsersPage() {
<>
<div className="page-header">
<div>
<h1 className="page-title">Kullanıcılar</h1>
<p className="page-subtitle">Panel kullanıcıları .env dosyasından yönetilir</p>
<h1 className="page-title">{dict.users.title || "Kullanıcılar"}</h1>
<p className="page-subtitle">{dict.users.subtitle || "Panel kullanıcıları .env dosyasından yönetilir"}</p>
</div>
</div>
@@ -47,12 +49,16 @@ export default function UsersPage() {
<InfoIcon />
</div>
<div>
<div style={{ fontWeight: 600, color: "var(--accent-hover)", marginBottom: 6 }}>Kullanıcı yönetimi hakkında</div>
<div style={{ fontWeight: 600, color: "var(--accent-hover)", marginBottom: 6 }}>{dict.users.info ? "Info" : "Kullanıcı yönetimi hakkında"}</div>
<div style={{ fontSize: 13, color: "var(--text-secondary)", lineHeight: 1.7 }}>
Kullanıcılar <code style={{ background: "var(--bg)", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>.env</code> dosyasındaki{" "}
<code style={{ background: "var(--bg)", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>USER_0_*</code>,{" "}
<code style={{ background: "var(--bg)", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>USER_1_*</code> değişkenleriyle tanımlanır.
Yeni kullanıcı eklemek için .env dosyasını düzenleyip uygulamayı yeniden başlatın.
{dict.users.info || (
<>
Kullanıcılar <code style={{ background: "var(--bg)", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>.env</code> dosyasındaki{" "}
<code style={{ background: "var(--bg)", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>USER_0_*</code>,{" "}
<code style={{ background: "var(--bg)", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>USER_1_*</code> değişkenleriyle tanımlanır.
Yeni kullanıcı eklemek için .env dosyasını düzenleyip uygulamayı yeniden başlatın.
</>
)}
</div>
<div style={{ marginTop: 12, padding: "10px 14px", background: "var(--bg)", borderRadius: "var(--radius)", fontSize: 12, fontFamily: "monospace", color: "var(--text-secondary)", lineHeight: 2 }}>
USER_2_NAME=&quot;Ahmet Yılmaz&quot;<br />
@@ -70,7 +76,7 @@ export default function UsersPage() {
<input
type="text"
className="input search-input"
placeholder="İsim veya e-posta ara..."
placeholder={dict.users.searchPlaceholder || "İsim veya e-posta ara..."}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
@@ -85,15 +91,15 @@ export default function UsersPage() {
) : filtered.length === 0 ? (
<div className="empty-state">
<div className="empty-icon"><UsersIcon /></div>
<div style={{ fontWeight: 600 }}>Kullanıcı bulunamadı</div>
<div style={{ fontWeight: 600 }}>{dict.users.noUsers || "Kullanıcı bulunamadı"}</div>
</div>
) : (
<table>
<thead>
<tr>
<th>Kullanıcı</th>
<th>Rol</th>
<th>İzin Verilen Domainler</th>
<th>{dict.users.username || "Kullanıcı"}</th>
<th>{dict.users.role || "Rol"}</th>
<th>{dict.users.domains || "İzin Verilen Domainler"}</th>
</tr>
</thead>
<tbody>
@@ -112,12 +118,12 @@ export default function UsersPage() {
</td>
<td>
<span className={`badge ${u.role === "SUPER_ADMIN" ? "badge-blue" : "badge-green"}`}>
{u.role === "SUPER_ADMIN" ? "★ Süper Admin" : "Domain Admin"}
{u.role === "SUPER_ADMIN" ? `${dict.users.superAdmin || "Süper Admin"}` : (dict.users.domainAdmin || "Domain Admin")}
</span>
</td>
<td>
{u.domains.includes("*") ? (
<span className="badge badge-blue">Tüm domainler</span>
<span className="badge badge-blue">{dict.users.allDomains || "Tüm domainler"}</span>
) : (
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
{u.domains.map((d) => (

View File

@@ -23,7 +23,7 @@ export default function LoginForm({ dict, lang }: { dict: any; lang: string }) {
});
if (res?.error) {
setError("E-posta veya şifre hatalı."); // We can translate this later
setError(dict.errorInvalid || "E-posta veya şifre hatalı.");
} else {
router.push(`/${lang}/dashboard`);
router.refresh();

View File

@@ -30,5 +30,13 @@ export async function POST(req: NextRequest) {
if (!domain) return NextResponse.json({ error: "domain gerekli" }, { status: 400 });
const result = await createDomain({ domain, description, mailboxes, quota, maxquota });
if (result.ok && Array.isArray(result.data)) {
const hasError = result.data.some((item: any) => item.type === "error");
if (hasError) {
return NextResponse.json(result.data, { status: 400 });
}
}
return NextResponse.json(result.data, { status: result.ok ? 200 : 502 });
}

View File

@@ -0,0 +1,61 @@
import { NextRequest, NextResponse } from "next/server";
import { getUsers } from "@/lib/users";
/**
* app/api/webhooks/mail/route.ts
*
* Webhook endpoint for incoming mail notifications (e.g. from Rspamd or Mailcow).
* Sends notifications to Telegram based on the recipient email.
*/
export async function POST(req: NextRequest) {
try {
const data = await req.json();
// Extract basic info from the incoming payload
const recipient = (data.to || data.rcpt || "").toLowerCase();
const sender = data.from || "Bilinmiyor";
const subject = data.subject || "(Konu Yok)";
console.log(`[Mail Webhook] Yeni mail geldi: ${sender} -> ${recipient}`);
const users = getUsers();
const user = users.find(u => u.email.toLowerCase() === recipient);
const targetChatId = user?.telegramId;
if (targetChatId && process.env.TELEGRAM_BOT_TOKEN) {
const message = `🔔 *Yeni Mail!*\n\n*Kimden:* ${sender}\n*Konu:* ${subject}`;
const telegramUrl = `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`;
const res = await fetch(telegramUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: targetChatId,
text: message,
parse_mode: "Markdown",
}),
});
if (!res.ok) {
const errorText = await res.text();
console.error(`[Mail Webhook] Telegram API hatası: ${res.status} ${errorText}`);
} else {
console.log(`[Mail Webhook] Telegram bildirimi gönderildi: ${recipient}`);
}
} else {
if (!targetChatId) {
console.log(`[Mail Webhook] Alıcı için eşleşen Telegram ID bulunamadı: ${recipient}`);
}
if (!process.env.TELEGRAM_BOT_TOKEN) {
console.error("[Mail Webhook] TELEGRAM_BOT_TOKEN ayarlı değil!");
}
}
return NextResponse.json({ status: "ok" });
} catch (error: any) {
console.error(`[Mail Webhook] Hata: ${error.message}`);
return NextResponse.json({ error: "İşlem başarısız" }, { status: 500 });
}
}

View File

@@ -7,12 +7,148 @@
"passwordLabel": "Password",
"passwordPlaceholder": "Enter your password",
"signInButton": "Sign In",
"signingIn": "Signing In..."
"signingIn": "Signing In...",
"errorInvalid": "Invalid email or password."
},
"sidebar": {
"mailboxes": "Mailboxes",
"mailClient": "Mail Client",
"users": "User Management",
"logout": "Log Out"
"logout": "Log Out",
"general": "GENERAL",
"management": "MANAGEMENT",
"superAdmin": "Super Admin",
"domainAdmin": "Domain Admin"
},
"dashboard": {
"title": "Dashboard",
"welcome": "Welcome",
"totalDomains": "Total Domains",
"mailboxes": "Mailboxes",
"aliases": "Aliases",
"users": "Defined Users",
"usersSub": "Users are managed from .env",
"domainStatus": "Domain Status",
"domain": "Domain",
"quotaUsage": "Quota Usage",
"status": "Status",
"active": "Active",
"inactive": "Inactive",
"quickActions": "Quick Actions",
"manageDomains": "Domain Management",
"manageDomainsDesc": "Add, delete, manage domains",
"manageMailboxes": "Mailboxes",
"manageMailboxesDesc": "Create new account, change password, delete",
"manageUsers": "Users",
"manageUsersDesc": "View defined panel users from .env"
},
"domains": {
"title": "Domains",
"subtitle": "domain listed",
"searchPlaceholder": "Search domain...",
"domain": "Domain",
"description": "Description",
"mailboxes": "Mailboxes",
"aliases": "Aliases",
"quota": "Quota",
"status": "Status",
"actions": "Actions",
"active": "Active",
"inactive": "Inactive",
"addDomain": "Add Domain",
"refresh": "Refresh",
"noDomains": "No domains found",
"tryDiffSearch": "Try a different search term"
},
"users": {
"title": "System Users",
"subtitle": "Admin users defined via .env file",
"info": "User management is done securely via the environment variables (.env). This panel is read-only.",
"username": "Username",
"name": "Name",
"role": "Role",
"domains": "Authorized Domains",
"superAdmin": "Super Admin",
"domainAdmin": "Domain Admin",
"allDomains": "All Domains"
},
"mailboxes": {
"title": "Mailboxes",
"selectDomain": "Select domain",
"accounts": "accounts",
"addAccount": "Add Account",
"searchPlaceholder": "Search email or name...",
"email": "Email",
"name": "Name",
"quota": "Quota",
"status": "Status",
"actions": "Actions",
"active": "Active",
"inactive": "Inactive",
"noMailboxes": "No mailboxes in this domain",
"noMailboxesDesc": "Click 'Add Account'",
"selectDomainDesc": "Select a domain from the top left",
"deleteConfirm": "Are you sure you want to delete this account?",
"info": "Connection Info",
"changePassword": "Change Password",
"deactivate": "Deactivate",
"activate": "Activate",
"delete": "Delete",
"newAccount": "New Mailbox",
"username": "Username",
"password": "Password",
"quotaMb": "Quota (MB)",
"cancel": "Cancel",
"create": "Create",
"newPasswordFor": "New password for",
"update": "Update",
"connectionInfo": "Client Connection Info",
"connectionInfoDesc": "Use the following details to set up the account on Apple Mail, Outlook or your phone:",
"imap": "IMAP (Incoming Server)",
"server": "Server",
"port": "Port",
"copy": "Copy",
"smtp": "SMTP (Outgoing Server)",
"auth": "Authentication",
"authPassword": "The password you set when creating the account",
"ok": "OK"
},
"mailClient": {
"loginTitle": "IMAP Login",
"loginSubtitle": "Connect to your mailbox",
"emailLabel": "Email",
"emailPlaceholder": "name@domain.com",
"passwordLabel": "Password",
"passwordPlaceholder": "********",
"connect": "Connect",
"connecting": "Connecting...",
"newMail": "New Mail",
"logout": "Logout",
"selectMail": "Select a mail",
"selectMailDesc": "Select a mail from the left list to read",
"inbox": "Inbox",
"sent": "Sent",
"drafts": "Drafts",
"trash": "Trash",
"junk": "Junk",
"archive": "Archive",
"searchMail": "Search mail...",
"emptyFolder": "This folder is empty",
"to": "To",
"cc": "Cc",
"subject": "Subject",
"date": "Date",
"attachments": "Attachments",
"reply": "Reply",
"delete": "Delete",
"download": "Download",
"composeTitle": "New Message",
"send": "Send",
"sending": "Sending...",
"cancel": "Cancel",
"dropFiles": "Drop files here or click to attach",
"sendError": "Failed to send message",
"noSubject": "(No Subject)",
"me": "Me"
}
}

View File

@@ -7,12 +7,148 @@
"passwordLabel": "Şifre",
"passwordPlaceholder": "Şifrenizi girin",
"signInButton": "Giriş Yap",
"signingIn": "Giriş yapılıyor..."
"signingIn": "Giriş yapılıyor...",
"errorInvalid": "E-posta veya şifre hatalı."
},
"sidebar": {
"mailboxes": "Mail Hesapları",
"mailClient": "Web Mail Client",
"mailClient": "Mail İstemcisi",
"users": "Kullanıcı Yönetimi",
"logout": ıkış Yap"
"logout": ıkış Yap",
"general": "GENEL",
"management": "YÖNETİM",
"superAdmin": "Süper Admin",
"domainAdmin": "Domain Admin"
},
"dashboard": {
"title": "Dashboard",
"welcome": "Hoş geldiniz",
"totalDomains": "Toplam Domain",
"mailboxes": "Mail Kutuları",
"aliases": "Alias",
"users": "Tanımlı Kullanıcı",
"usersSub": "Kullanıcılar .env'den yönetilir",
"domainStatus": "Domain Durumu",
"domain": "Domain",
"quotaUsage": "Kota Kullanımı",
"status": "Durum",
"active": "Aktif",
"inactive": "Pasif",
"quickActions": "Hızlı İşlemler",
"manageDomains": "Domain Yönetimi",
"manageDomainsDesc": "Domain ekle, sil, yönet",
"manageMailboxes": "Mail Hesapları",
"manageMailboxesDesc": "Yeni hesap oluştur, şifre değiştir, sil",
"manageUsers": "Kullanıcılar",
"manageUsersDesc": ".env'den tanımlı panel kullanıcılarını görüntüle"
},
"domains": {
"title": "Domainler",
"subtitle": "domain listeleniyor",
"searchPlaceholder": "Domain ara...",
"domain": "Domain",
"description": "Açıklama",
"mailboxes": "Mail Kutuları",
"aliases": "Alias",
"quota": "Kota",
"status": "Durum",
"actions": "İşlemler",
"active": "Aktif",
"inactive": "Pasif",
"addDomain": "Domain Ekle",
"refresh": "Yenile",
"noDomains": "Domain bulunamadı",
"tryDiffSearch": "Farklı bir arama yapın"
},
"users": {
"title": "Sistem Kullanıcıları",
"subtitle": ".env dosyası üzerinden tanımlanmış yetkili kullanıcılar",
"info": "Kullanıcı yönetimi güvenlik nedeniyle sadece çevresel değişkenler (.env) üzerinden yapılmaktadır. Bu ekran salt okunurdur.",
"username": "Kullanıcı Adı",
"name": "Ad Soyad",
"role": "Rol",
"domains": "Yetkili Domainler",
"superAdmin": "Süper Admin",
"domainAdmin": "Domain Admin",
"allDomains": "Tüm Domainler"
},
"mailboxes": {
"title": "Mail Hesapları",
"selectDomain": "Domain seçin",
"accounts": "hesap",
"addAccount": "Hesap Ekle",
"searchPlaceholder": "E-posta veya isim ara...",
"email": "E-posta",
"name": "Ad Soyad",
"quota": "Kota",
"status": "Durum",
"actions": "İşlemler",
"active": "Aktif",
"inactive": "Pasif",
"noMailboxes": "Bu domainde mail hesabı yok",
"noMailboxesDesc": "'Hesap Ekle' butonuna tıklayın",
"selectDomainDesc": "Sol üstteki listeden domain seçin",
"deleteConfirm": "hesabını silmek istediğinizden emin misiniz?",
"info": "Bağlantı Bilgileri",
"changePassword": "Şifre Değiştir",
"deactivate": "Pasife Al",
"activate": "Aktif Et",
"delete": "Sil",
"newAccount": "Yeni Mail Hesabı",
"username": "Kullanıcı Adı",
"password": "Şifre",
"quotaMb": "Kota (MB)",
"cancel": "İptal",
"create": "Oluştur",
"newPasswordFor": "için yeni şifre",
"update": "Güncelle",
"connectionInfo": "İstemci Bağlantı Bilgileri",
"connectionInfoDesc": "hesabını Apple Mail, Outlook veya telefonunuza kurmak için aşağıdaki bilgileri kullanın:",
"imap": "IMAP (Gelen Sunucu)",
"server": "Sunucu",
"port": "Port",
"copy": "Kopyala",
"smtp": "SMTP (Giden Sunucu)",
"auth": "Kimlik Doğrulama",
"authPassword": "Hesap oluştururken belirlediğiniz şifre",
"ok": "Tamam"
},
"mailClient": {
"loginTitle": "IMAP Girişi",
"loginSubtitle": "Mail kutunuza bağlanın",
"emailLabel": "E-posta",
"emailPlaceholder": "isim@domain.com",
"passwordLabel": "Şifre",
"passwordPlaceholder": "********",
"connect": "Bağlan",
"connecting": "Bağlanıyor...",
"newMail": "Yeni Mail",
"logout": ıkış",
"selectMail": "Bir mail seçin",
"selectMailDesc": "Okumak için soldaki listeden bir mail seçin",
"inbox": "Gelen Kutusu",
"sent": "Gönderilmiş",
"drafts": "Taslaklar",
"trash": "Çöp Kutusu",
"junk": "Gereksiz",
"archive": "Arşiv",
"searchMail": "Mail ara...",
"emptyFolder": "Bu klasör boş",
"to": "Kime",
"cc": "Bilgi",
"subject": "Konu",
"date": "Tarih",
"attachments": "Ekler",
"reply": "Yanıtla",
"delete": "Sil",
"download": "İndir",
"composeTitle": "Yeni Mesaj",
"send": "Gönder",
"sending": "Gönderiliyor...",
"cancel": "İptal",
"dropFiles": "Dosyaları sürükleyin veya dosya eklemek için tıklayın",
"sendError": "Mesaj gönderilemedi",
"noSubject": "(Konu Yok)",
"me": "Ben"
}
}

View File

@@ -1105,3 +1105,18 @@ tr:hover td {
.page-header { padding: 16px; }
.stats-grid { grid-template-columns: 1fr 1fr; }
}
/* ── Language Switcher ── */
.lang-switcher .btn-ghost {
border-radius: var(--radius) !important;
font-weight: 400 !important;
}
.lang-option:hover {
background: var(--bg-hover) !important;
color: var(--text-primary) !important;
}
.lang-option.active {
color: var(--accent-hover) !important;
font-weight: 600;
}