Add i18n support with Next.js App Router and Dictionaries

This commit is contained in:
AyrisAI
2026-05-14 12:56:43 +03:00
parent 89d74ce3fe
commit 4c9a07e3ef
24 changed files with 244 additions and 91 deletions

View File

@@ -0,0 +1,211 @@
import { auth } from "@/auth";
import { getDomains } from "@/lib/mailcow";
import { canAccessDomain } from "@/lib/users";
import { formatBytes } from "@/lib/format";
export default async function DashboardPage() {
const session = await auth();
const role = session?.user?.role;
const userDomains = session?.user?.domains ?? [];
const allDomains = await getDomains();
const visibleDomains = allDomains.filter((d) => canAccessDomain(userDomains, d.domain_name));
const totalMailboxes = visibleDomains.reduce((sum, d) => sum + d.mboxes_in_domain, 0);
const totalAliases = visibleDomains.reduce((sum, d) => sum + d.aliases_in_domain, 0);
return (
<>
<div className="page-header">
<div>
<h1 className="page-title">Dashboard</h1>
<p className="page-subtitle">Hoş geldiniz, {session?.user?.name} 👋</p>
</div>
</div>
<div className="page-body">
<div className="stats-grid">
<StatCard
label="Toplam Domain"
value={visibleDomains.length}
color="var(--accent)"
icon={<GlobeIcon />}
/>
<StatCard
label="Mail Kutuları"
value={totalMailboxes}
color="var(--success)"
icon={<MailIcon />}
/>
<StatCard
label="Alias"
value={totalAliases}
color="var(--warning)"
icon={<AtIcon />}
/>
{role === "SUPER_ADMIN" && (
<StatCard
label="Tanımlı Kullanıcı"
value={"—"}
sub="Kullanıcılar .env'den yönetilir"
color="var(--text-muted)"
icon={<UsersIcon />}
/>
)}
</div>
{/* Domain durum tablosu */}
{visibleDomains.length > 0 && (
<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
</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>
</tr>
</thead>
<tbody>
{visibleDomains.map((d) => {
const quotaUsed = Number(d.quota_used_in_domain);
const quotaTotal = d.max_quota_for_domain;
const pct = quotaTotal > 0 ? Math.min((quotaUsed / quotaTotal) * 100, 100) : 0;
return (
<tr key={d.domain_name}>
<td>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<div style={{ width: 28, height: 28, borderRadius: 6, background: "var(--accent-dim)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--accent-hover)", flexShrink: 0 }}>
<GlobeIcon />
</div>
<span style={{ fontWeight: 500 }}>{d.domain_name}</span>
</div>
</td>
<td>
<span>{d.mboxes_in_domain}</span>
<span style={{ color: "var(--text-muted)", fontSize: 12 }}> / {d.max_num_mboxes_for_domain}</span>
</td>
<td style={{ minWidth: 160 }}>
<div style={{ fontSize: 11, color: "var(--text-secondary)", marginBottom: 4 }}>
{formatBytes(quotaUsed)} / {formatBytes(quotaTotal)}
</div>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<div className="progress-bar">
<div className={`progress-fill ${pct > 80 ? "danger" : ""}`} style={{ width: `${pct}%` }} />
</div>
<span style={{ fontSize: 11, color: "var(--text-muted)", flexShrink: 0 }}>{Math.round(pct)}%</span>
</div>
</td>
<td>
<span className={`badge ${String(d.active) === "1" ? "badge-green" : "badge-red"}`}>
{String(d.active) === "1" ? "● Aktif" : "● Pasif"}
</span>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
{/* Quick actions */}
<div className="card">
<h2 style={{ fontSize: 14, fontWeight: 700, marginBottom: 16, color: "var(--text-primary)" }}>
Hızlı İşlemler
</h2>
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
{role === "SUPER_ADMIN" && (
<QuickItem
href="/dashboard/domains"
icon={<GlobeIcon />}
title="Domain Yönetimi"
desc="Domain ekle, sil, yönet"
color="var(--accent)"
/>
)}
<QuickItem
href="/dashboard/mailboxes"
icon={<MailIcon />}
title="Mail Hesapları"
desc="Yeni hesap oluştur, şifre değiştir, sil"
color="var(--success)"
/>
{role === "SUPER_ADMIN" && (
<QuickItem
href="/dashboard/users"
icon={<UsersIcon />}
title="Kullanıcılar"
desc=".env'den tanımlı panel kullanıcılarını görüntüle"
color="var(--warning)"
/>
)}
</div>
</div>
</div>
</>
);
}
function StatCard({ label, value, sub, color, icon }: {
label: string;
value: number | string;
sub?: string;
color: string;
icon: React.ReactNode;
}) {
return (
<div className="stat-card">
<div className="stat-icon" style={{ background: `${color}20`, color }}>
{icon}
</div>
<div className="stat-label">{label}</div>
<div className="stat-value">{value}</div>
{sub && <div style={{ fontSize: 11, color: "var(--text-muted)" }}>{sub}</div>}
</div>
);
}
function QuickItem({ href, icon, title, desc, color }: {
href: string;
icon: React.ReactNode;
title: string;
desc: string;
color: string;
}) {
return (
<a
href={href}
style={{
display: "flex", alignItems: "center", gap: 14, padding: 14,
borderRadius: "var(--radius)", border: "1px solid var(--border)",
background: "var(--bg)", textDecoration: "none",
transition: "all 0.15s ease",
}}
>
<div style={{ width: 36, height: 36, borderRadius: 8, background: `${color}20`, color, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
{icon}
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 13, fontWeight: 600, color: "var(--text-primary)" }}>{title}</div>
<div style={{ fontSize: 12, color: "var(--text-secondary)", marginTop: 2 }}>{desc}</div>
</div>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--text-muted)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m9 18 6-6-6-6" />
</svg>
</a>
);
}
// Icons
function GlobeIcon() { return <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/><path d="M2 12h20"/></svg>; }
function MailIcon() { return <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><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 AtIcon() { return <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.92 7.94"/></svg>; }
function UsersIcon() { return <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>; }