149 lines
6.6 KiB
TypeScript
149 lines
6.6 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect } from "react";
|
||
import { useDictionary } from "@/components/DictionaryContext";
|
||
|
||
interface User {
|
||
id: string;
|
||
name: string;
|
||
email: string;
|
||
role: string;
|
||
domains: string[];
|
||
}
|
||
|
||
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")
|
||
.then((r) => r.json())
|
||
.then((data) => {
|
||
setUsers(Array.isArray(data) ? data : []);
|
||
setLoading(false);
|
||
})
|
||
.catch(() => setLoading(false));
|
||
}, []);
|
||
|
||
const filtered = users.filter(
|
||
(u) =>
|
||
u.name.toLowerCase().includes(search.toLowerCase()) ||
|
||
u.email.toLowerCase().includes(search.toLowerCase())
|
||
);
|
||
|
||
return (
|
||
<>
|
||
<div className="page-header">
|
||
<div>
|
||
<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>
|
||
|
||
<div className="page-body">
|
||
{/* Info card */}
|
||
<div className="card" style={{ display: "flex", gap: 14, alignItems: "flex-start", border: "1px solid var(--accent-dim)", background: "var(--accent-dim)" }}>
|
||
<div style={{ color: "var(--accent-hover)", flexShrink: 0, paddingTop: 2 }}>
|
||
<InfoIcon />
|
||
</div>
|
||
<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 }}>
|
||
{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="Ahmet Yılmaz"<br />
|
||
USER_2_EMAIL="ahmet@ayristech.com"<br />
|
||
USER_2_PASSWORD="güçlü-şifre"<br />
|
||
USER_2_ROLE="DOMAIN_ADMIN"<br />
|
||
USER_2_DOMAINS="yenidomain.com"
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="search-bar">
|
||
<div className="search-input-wrap">
|
||
<span className="search-icon"><SearchIcon /></span>
|
||
<input
|
||
type="text"
|
||
className="input search-input"
|
||
placeholder={dict.users.searchPlaceholder || "İsim veya e-posta ara..."}
|
||
value={search}
|
||
onChange={(e) => setSearch(e.target.value)}
|
||
/>
|
||
</div>
|
||
</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"><UsersIcon /></div>
|
||
<div style={{ fontWeight: 600 }}>{dict.users.noUsers || "Kullanıcı bulunamadı"}</div>
|
||
</div>
|
||
) : (
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>{dict.users.username || "Kullanıcı"}</th>
|
||
<th>{dict.users.role || "Rol"}</th>
|
||
<th>{dict.users.domains || "İzin Verilen Domainler"}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{filtered.map((u) => (
|
||
<tr key={u.id}>
|
||
<td>
|
||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||
<div className="user-avatar" style={{ width: 32, height: 32, fontSize: 13 }}>
|
||
{u.name[0]?.toUpperCase()}
|
||
</div>
|
||
<div>
|
||
<div style={{ fontWeight: 500 }}>{u.name}</div>
|
||
<div style={{ fontSize: 11, color: "var(--text-secondary)" }}>{u.email}</div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<span className={`badge ${u.role === "SUPER_ADMIN" ? "badge-blue" : "badge-green"}`}>
|
||
{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">{dict.users.allDomains || "Tüm domainler"}</span>
|
||
) : (
|
||
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
|
||
{u.domains.map((d) => (
|
||
<span key={d} className="badge badge-green">{d}</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|
||
|
||
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 UsersIcon() { return <svg width="20" height="20" 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>; }
|
||
function InfoIcon() { return <svg width="16" height="16" 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 16v-4"/><path d="M12 8h.01"/></svg>; }
|