Files

213 lines
7.7 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 } from "react";
import { useDictionary } from "@/components/DictionaryContext";
interface User {
id: string;
name: string;
email: string;
}
interface Mapping {
id: string;
email: string;
userId: string;
user: User;
}
export default function MappingsPage() {
const [mappings, setMappings] = useState<Mapping[]>([]);
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const [isModalOpen, setIsModalOpen] = useState(false);
// Form state
const [newEmail, setNewEmail] = useState("");
const [newUserId, setNewUserId] = useState("");
const [saving, setSaving] = useState(false);
const dict = useDictionary();
const fetchData = async () => {
setLoading(true);
try {
const [mRes, uRes] = await Promise.all([
fetch("/api/mappings"),
fetch("/api/users")
]);
const [mData, uData] = await Promise.all([mRes.json(), uRes.json()]);
setMappings(Array.isArray(mData) ? mData : []);
setUsers(Array.isArray(uData) ? uData : []);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
const handleAdd = async (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
try {
const res = await fetch("/api/mappings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: newEmail, userId: newUserId })
});
if (res.ok) {
setIsModalOpen(false);
setNewEmail("");
setNewUserId("");
fetchData();
}
} catch (e) {
console.error(e);
} finally {
setSaving(false);
}
};
const handleDelete = async (id: string) => {
if (!confirm("Bu eşleştirmeyi silmek istediğinize emin misiniz?")) return;
try {
const res = await fetch(`/api/mappings/${id}`, { method: "DELETE" });
if (res.ok) fetchData();
} catch (e) {
console.error(e);
}
};
const filtered = mappings.filter(m =>
m.email.toLowerCase().includes(search.toLowerCase()) ||
m.user.name.toLowerCase().includes(search.toLowerCase())
);
return (
<>
<div className="page-header">
<div>
<h1 className="page-title">{dict.mappings.title}</h1>
<p className="page-subtitle">{dict.mappings.subtitle}</p>
</div>
<button className="btn btn-primary" onClick={() => setIsModalOpen(true)}>
<PlusIcon />
{dict.mappings.addMapping}
</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.mappings.searchPlaceholder}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
<div className="table-wrap">
{loading ? (
<div className="empty-state"><span className="spinner" /></div>
) : filtered.length === 0 ? (
<div className="empty-state">
<div className="empty-icon"><LinkIcon /></div>
<div style={{ fontWeight: 600 }}>{dict.mappings.noMappings}</div>
</div>
) : (
<table>
<thead>
<tr>
<th>{dict.mappings.email}</th>
<th>{dict.mappings.user}</th>
<th style={{ width: 80 }}></th>
</tr>
</thead>
<tbody>
{filtered.map((m) => (
<tr key={m.id}>
<td>
<div style={{ fontWeight: 500 }}>{m.email}</div>
</td>
<td>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<div className="user-avatar" style={{ width: 24, height: 24, fontSize: 10 }}>
{m.user.name[0]?.toUpperCase()}
</div>
<span>{m.user.name}</span>
<span style={{ fontSize: 11, color: "var(--text-secondary)" }}>({m.user.email})</span>
</div>
</td>
<td>
<button className="btn btn-ghost" onClick={() => handleDelete(m.id)} style={{ color: "var(--error)" }}>
<TrashIcon />
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
{isModalOpen && (
<div className="modal-overlay">
<div className="modal-content" style={{ maxWidth: 450 }}>
<div className="modal-header">
<h2 className="modal-title">{dict.mappings.addMapping}</h2>
<button className="modal-close" onClick={() => setIsModalOpen(false)}>×</button>
</div>
<form onSubmit={handleAdd} className="form-group" style={{ padding: 20 }}>
<div>
<label className="label">{dict.mappings.email}</label>
<input
className="input"
placeholder="info@domain.com"
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
required
/>
</div>
<div>
<label className="label">{dict.mappings.user}</label>
<select
className="input"
value={newUserId}
onChange={(e) => setNewUserId(e.target.value)}
required
>
<option value="">{dict.mappings.user} seçin...</option>
{users.map(u => (
<option key={u.id} value={u.id}>{u.name} ({u.email})</option>
))}
</select>
</div>
<div style={{ display: "flex", gap: 10, marginTop: 10 }}>
<button type="button" className="btn btn-ghost" style={{ flex: 1 }} onClick={() => setIsModalOpen(false)}>İptal</button>
<button type="submit" className="btn btn-primary" style={{ flex: 1 }} disabled={saving}>
{saving ? <span className="spinner" /> : "Kaydet"}
</button>
</div>
</form>
</div>
</div>
)}
</>
);
}
function PlusIcon() { return <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" 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 LinkIcon() { return <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>; }
function TrashIcon() { return <svg width="14" height="14" 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>; }