Add i18n support with Next.js App Router and Dictionaries
This commit is contained in:
211
app/[lang]/dashboard/page.tsx
Normal file
211
app/[lang]/dashboard/page.tsx
Normal 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>; }
|
||||
Reference in New Issue
Block a user