244 lines
12 KiB
TypeScript
244 lines
12 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect, useTransition } from "react";
|
||
import { formatBytes } from "@/lib/format";
|
||
import { useDictionary } from "@/components/DictionaryContext";
|
||
|
||
interface Domain {
|
||
domain_name: string;
|
||
description: string;
|
||
active: string;
|
||
mboxes_in_domain: number;
|
||
mboxes_left: number;
|
||
max_num_mboxes_for_domain: number;
|
||
aliases_in_domain: number;
|
||
quota_used_in_domain: string;
|
||
max_quota_for_domain: number;
|
||
}
|
||
|
||
export default function DomainsPage() {
|
||
const [domains, setDomains] = useState<Domain[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [showModal, setShowModal] = useState(false);
|
||
const [isPending, startTransition] = useTransition();
|
||
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);
|
||
const res = await fetch("/api/domains");
|
||
if (res.ok) setDomains(await res.json());
|
||
setLoading(false);
|
||
};
|
||
|
||
useEffect(() => { fetchDomains(); }, []);
|
||
|
||
const handleCreate = (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
setFormError("");
|
||
startTransition(async () => {
|
||
const res = await fetch("/api/domains", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
domain: form.domain,
|
||
description: form.description,
|
||
mailboxes: parseInt(form.mailboxes),
|
||
quota: parseInt(form.quota),
|
||
maxquota: parseInt(form.maxquota),
|
||
}),
|
||
});
|
||
if (res.ok) {
|
||
setShowModal(false);
|
||
setForm({ domain: "", description: "", mailboxes: "10", quota: "10240", maxquota: "10240" });
|
||
fetchDomains();
|
||
} else {
|
||
const data = await res.json();
|
||
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 silmek istediğinizden emin misiniz?\n\nBu işlem geri alınamaz!`)) return;
|
||
startTransition(async () => {
|
||
await fetch(`/api/domains/${encodeURIComponent(domain)}`, { method: "DELETE" });
|
||
fetchDomains();
|
||
});
|
||
};
|
||
|
||
const filtered = domains.filter(
|
||
(d) =>
|
||
d.domain_name.toLowerCase().includes(search.toLowerCase()) ||
|
||
d.description?.toLowerCase().includes(search.toLowerCase())
|
||
);
|
||
|
||
return (
|
||
<>
|
||
<div className="page-header">
|
||
<div>
|
||
<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 /> {dict.domains.addDomain || "Domain Ekle"}
|
||
</button>
|
||
</div>
|
||
|
||
<div className="page-body">
|
||
<div className="search-bar">
|
||
<div className="search-input-wrap">
|
||
<span className="search-icon"><SearchIcon /></span>
|
||
<input
|
||
type="text"
|
||
className="input search-input"
|
||
placeholder={dict.domains.searchPlaceholder || "Domain ara..."}
|
||
value={search}
|
||
onChange={(e) => setSearch(e.target.value)}
|
||
/>
|
||
</div>
|
||
<button className="btn btn-ghost" onClick={fetchDomains}>
|
||
<RefreshIcon /> {dict.domains.refresh || "Yenile"}
|
||
</button>
|
||
</div>
|
||
|
||
<div className="table-wrap">
|
||
{loading ? (
|
||
<div className="empty-state"><span className="spinner" style={{ width: 24, height: 24 }} /></div>
|
||
) : filtered.length === 0 ? (
|
||
<div className="empty-state">
|
||
<div className="empty-icon"><GlobeIcon size={24} /></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>{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>
|
||
{filtered.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>
|
||
<div>
|
||
<div style={{ fontWeight: 500 }}>{d.domain_name}</div>
|
||
{d.description && <div style={{ fontSize: 11, color: "var(--text-secondary)" }}>{d.description}</div>}
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
{d.mboxes_in_domain}
|
||
<span style={{ color: "var(--text-muted)", fontSize: 12 }}> / {d.max_num_mboxes_for_domain}</span>
|
||
</td>
|
||
<td>{d.aliases_in_domain}</td>
|
||
<td style={{ minWidth: 140 }}>
|
||
<div style={{ fontSize: 11, color: "var(--text-secondary)", marginBottom: 4 }}>
|
||
{formatBytes(quotaUsed)} / {formatBytes(quotaTotal)}
|
||
</div>
|
||
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||
<div className="progress-bar">
|
||
<div className={`progress-fill ${pct > 80 ? "danger" : ""}`} style={{ width: `${pct}%` }} />
|
||
</div>
|
||
<span style={{ fontSize: 11, color: "var(--text-muted)" }}>{Math.round(pct)}%</span>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<span className={`badge ${String(d.active) === "1" ? "badge-green" : "badge-red"}`}>
|
||
{String(d.active) === "1" ? `● ${dict.domains.active || "Aktif"}` : `● ${dict.domains.inactive || "Pasif"}`}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(d.domain_name)} disabled={isPending}>
|
||
<TrashIcon /> Sil
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{showModal && (
|
||
<div className="modal-overlay" onClick={(e) => e.target === e.currentTarget && setShowModal(false)}>
|
||
<div className="modal">
|
||
<div className="modal-header">
|
||
<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">{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">{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">
|
||
<div>
|
||
<label className="label">Maks. Mailbox</label>
|
||
<input type="number" className="input" value={form.mailboxes} min={1}
|
||
onChange={(e) => setForm({ ...form, mailboxes: e.target.value })} />
|
||
</div>
|
||
<div>
|
||
<label className="label">Toplam Kota (MB)</label>
|
||
<input type="number" className="input" value={form.quota} min={1}
|
||
onChange={(e) => setForm({ ...form, quota: e.target.value })} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="modal-footer">
|
||
<button type="button" className="btn btn-ghost" onClick={() => setShowModal(false)}>İptal</button>
|
||
<button type="submit" className="btn btn-primary" disabled={isPending}>
|
||
{isPending ? <span className="spinner" /> : <PlusIcon />} Ekle
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
);
|
||
}
|
||
|
||
function PlusIcon() { return <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>; }
|
||
function SearchIcon() { return <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>; }
|
||
function RefreshIcon() { return <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M3 21v-5h5"/></svg>; }
|
||
function GlobeIcon({ size = 13 }: { size?: number }) { return <svg width={size} height={size} 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 TrashIcon() { return <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>; }
|
||
function XIcon() { return <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>; }
|