Files

244 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>; }