ADD: added function to update db scheme automaticly
This commit is contained in:
421
frontend/src/pages/Administration.tsx
Normal file
421
frontend/src/pages/Administration.tsx
Normal file
@@ -0,0 +1,421 @@
|
||||
import React, { JSX, useEffect, useState } from "react";
|
||||
import { fetchPlayers } from "./api";
|
||||
import { User, UserRole } from "../components/interfaces/users";
|
||||
|
||||
// type User = {
|
||||
// id: string;
|
||||
// name: string;
|
||||
// email: string;
|
||||
// role: "user" | "admin";
|
||||
// banned?: boolean;
|
||||
// };
|
||||
|
||||
type Team = {
|
||||
id: string;
|
||||
name: string;
|
||||
members: number;
|
||||
};
|
||||
|
||||
type EventItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
date: string; // ISO
|
||||
location?: string;
|
||||
};
|
||||
|
||||
const containerStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
height: "100%",
|
||||
gap: 20,
|
||||
padding: 20,
|
||||
boxSizing: "border-box",
|
||||
fontFamily: "Inter, Roboto, Arial, sans-serif",
|
||||
};
|
||||
|
||||
const sidebarStyle: React.CSSProperties = {
|
||||
width: 220,
|
||||
borderRight: "1px solid #e6e6e6",
|
||||
paddingRight: 12,
|
||||
};
|
||||
|
||||
const tabButtonStyle = (active: boolean): React.CSSProperties => ({
|
||||
display: "block",
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
padding: "8px 10px",
|
||||
marginBottom: 6,
|
||||
cursor: "pointer",
|
||||
borderRadius: 6,
|
||||
background: active ? "#0b5fff1a" : "transparent",
|
||||
border: active ? "1px solid #0b5fff33" : "1px solid transparent",
|
||||
});
|
||||
|
||||
const contentStyle: React.CSSProperties = {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
overflowY: "auto",
|
||||
};
|
||||
|
||||
export default function Administration(): JSX.Element {
|
||||
const [activeTab, setActiveTab] = useState<
|
||||
"users" | "teams" | "events" | "settings"
|
||||
>("users");
|
||||
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [teams, setTeams] = useState<Team[]>([]);
|
||||
const [events, setEvents] = useState<EventItem[]>([]);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Form state for creating event
|
||||
const [newEventTitle, setNewEventTitle] = useState("");
|
||||
const [newEventDate, setNewEventDate] = useState("");
|
||||
const [newEventLocation, setNewEventLocation] = useState("");
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
useEffect(() => {
|
||||
// fetch all admin resources when component mounts
|
||||
if (token) refreshAll();
|
||||
}, []);
|
||||
|
||||
async function refreshAll() {
|
||||
if (!token) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
// Replace these endpoints with your backend API
|
||||
|
||||
const users = await fetchPlayers(token);
|
||||
setUsers(users);
|
||||
console.log("Fetched users:", users);
|
||||
const [uRes, tRes, eRes] = await Promise.all([
|
||||
fetch("/api/admin/players"),
|
||||
fetch("/api/admin/teams"),
|
||||
fetch("/api/admin/events"),
|
||||
]);
|
||||
|
||||
if (!uRes.ok || !tRes.ok || !eRes.ok) {
|
||||
throw new Error("Failed to fetch admin resources");
|
||||
}
|
||||
|
||||
// console.log(uRes);
|
||||
const [uJson, tJson, eJson] = await Promise.all([
|
||||
uRes.json(),
|
||||
tRes.json(),
|
||||
eRes.json(),
|
||||
]);
|
||||
|
||||
// setUsers(uRes as User[]);
|
||||
setTeams(tJson as Team[]);
|
||||
setEvents(eJson as EventItem[]);
|
||||
|
||||
|
||||
} catch (err: any) {
|
||||
setError(err?.message ?? "Unknown error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleAdmin(user: User) {
|
||||
const promote = user.Roles.includes(UserRole.Admin) ? false : true;
|
||||
if (
|
||||
!window.confirm(
|
||||
`${promote ? "Promote" : "Demote"} ${user.Username} to ${
|
||||
promote ? "admin" : "user"
|
||||
}?`
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/admin/users/${user.UUID}/role`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ role: promote ? "admin" : "user" }),
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to change role");
|
||||
setUsers((prev) =>
|
||||
prev.map((u) => (u.UUID === user.UUID ? { ...u, role: promote ? "admin" : "user" } : u))
|
||||
);
|
||||
} catch (err: any) {
|
||||
alert(err?.message ?? "Failed to update role");
|
||||
}
|
||||
}
|
||||
|
||||
// async function toggleBan(user: User) {
|
||||
// const ban = user.isActive;
|
||||
// if (!window.confirm(`${ban ? "Ban" : "Unban"} ${user.username}?`)) return;
|
||||
// try {
|
||||
// const res = await fetch(`/api/admin/users/${user.id}/ban`, {
|
||||
// method: "PATCH",
|
||||
// headers: { "Content-Type": "application/json" },
|
||||
// body: JSON.stringify({ banned: ban }),
|
||||
// });
|
||||
// if (!res.ok) throw new Error("Failed to update ban");
|
||||
// setUsers((prev) => prev.map((u) => (u.id === user.id ? { ...u, banned: ban } : u)));
|
||||
// } catch (err: any) {
|
||||
// alert(err?.message ?? "Failed to update ban");
|
||||
// }
|
||||
// }
|
||||
|
||||
async function deleteTeam(team: Team) {
|
||||
if (!window.confirm(`Delete team "${team.name}"? This cannot be undone.`)) return;
|
||||
try {
|
||||
const res = await fetch(`/api/admin/teams/${team.id}`, { method: "DELETE" });
|
||||
if (!res.ok) throw new Error("Failed to delete team");
|
||||
setTeams((prev) => prev.filter((t) => t.id !== team.id));
|
||||
} catch (err: any) {
|
||||
alert(err?.message ?? "Failed to delete team");
|
||||
}
|
||||
}
|
||||
|
||||
async function createEvent(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!newEventTitle || !newEventDate) {
|
||||
alert("Title and date required");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const payload = {
|
||||
title: newEventTitle,
|
||||
date: new Date(newEventDate).toISOString(),
|
||||
location: newEventLocation || undefined,
|
||||
};
|
||||
const res = await fetch("/api/admin/events", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to create event");
|
||||
const created: EventItem = await res.json();
|
||||
setEvents((prev) => [created, ...prev]);
|
||||
setNewEventTitle("");
|
||||
setNewEventDate("");
|
||||
setNewEventLocation("");
|
||||
setActiveTab("events");
|
||||
} catch (err: any) {
|
||||
alert(err?.message ?? "Failed to create event");
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteEvent(ev: EventItem) {
|
||||
if (!window.confirm(`Delete event "${ev.title}"?`)) return;
|
||||
try {
|
||||
const res = await fetch(`/api/admin/events/${ev.id}`, { method: "DELETE" });
|
||||
if (!res.ok) throw new Error("Failed to delete event");
|
||||
setEvents((prev) => prev.filter((x) => x.id !== ev.id));
|
||||
} catch (err: any) {
|
||||
alert(err?.message ?? "Failed to delete event");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<aside style={sidebarStyle}>
|
||||
<h3>Administration</h3>
|
||||
<div>
|
||||
<button
|
||||
style={tabButtonStyle(activeTab === "users")}
|
||||
onClick={() => setActiveTab("users")}
|
||||
>
|
||||
Users
|
||||
</button>
|
||||
<button
|
||||
style={tabButtonStyle(activeTab === "teams")}
|
||||
onClick={() => setActiveTab("teams")}
|
||||
>
|
||||
Teams
|
||||
</button>
|
||||
<button
|
||||
style={tabButtonStyle(activeTab === "events")}
|
||||
onClick={() => setActiveTab("events")}
|
||||
>
|
||||
Events
|
||||
</button>
|
||||
<button
|
||||
style={tabButtonStyle(activeTab === "settings")}
|
||||
onClick={() => setActiveTab("settings")}
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<button onClick={refreshAll} style={{ padding: "6px 10px", cursor: "pointer" }}>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading && <p style={{ marginTop: 12 }}>Loading...</p>}
|
||||
{error && <p style={{ marginTop: 12, color: "red" }}>{error}</p>}
|
||||
</aside>
|
||||
|
||||
<main style={contentStyle}>
|
||||
{activeTab === "users" && (
|
||||
<section>
|
||||
<h2>Users</h2>
|
||||
<p>Manage user accounts, roles and bans.</p>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
{users.length === 0 ? (
|
||||
<p>No users found.</p>
|
||||
) : (
|
||||
<table style={{ width: "100%", borderCollapse: "collapse" }}>
|
||||
<thead>
|
||||
<tr style={{ textAlign: "left", borderBottom: "1px solid #eee" }}>
|
||||
<th style={{ padding: "8px 6px" }}>Name</th>
|
||||
<th style={{ padding: "8px 6px" }}>Email</th>
|
||||
<th style={{ padding: "8px 6px" }}>Role</th>
|
||||
<th style={{ padding: "8px 6px" }}>Status</th>
|
||||
<th style={{ padding: "8px 6px" }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((u) => (
|
||||
<tr key={u.UUID} style={{ borderBottom: "1px solid #fafafa" }}>
|
||||
<td style={{ padding: "8px 6px" }}>{u.Username}</td>
|
||||
<td style={{ padding: "8px 6px" }}>{u.Email}</td>
|
||||
<td style={{ padding: "8px 6px" }}>{u.Roles}</td>
|
||||
{/* <td style={{ padding: "8px 6px" }}>{u.banned ? "Banned" : "Active"}</td> */}
|
||||
{/* <td style={{ padding: "8px 6px" }}>
|
||||
<button onClick={() => toggleAdmin(u)} style={{ marginRight: 8 }}>
|
||||
{u.roles.includes(UserRole.Admin) ? "Demote" : "Promote"}
|
||||
</button> */}
|
||||
{/* <button onClick={() => toggleBan(u)}>{u.banned ? "Unban" : "Ban"}</button> */}
|
||||
{/* </td> */}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{activeTab === "teams" && (
|
||||
<section>
|
||||
<h2>Teams</h2>
|
||||
<p>Manage teams and membership.</p>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
{teams.length === 0 ? (
|
||||
<p>No teams found.</p>
|
||||
) : (
|
||||
<ul style={{ paddingLeft: 0, listStyle: "none" }}>
|
||||
{teams.map((t) => (
|
||||
<li
|
||||
key={t.id}
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
padding: "8px 0",
|
||||
borderBottom: "1px solid #f2f2f2",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<strong>{t.name}</strong>
|
||||
<div style={{ fontSize: 13, color: "#666" }}>{t.members} members</div>
|
||||
</div>
|
||||
<div>
|
||||
<button style={{ marginRight: 8 }}>Edit</button>
|
||||
<button onClick={() => deleteTeam(t)}>Delete</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{activeTab === "events" && (
|
||||
<section>
|
||||
<h2>Events</h2>
|
||||
<p>Create and manage events</p>
|
||||
|
||||
<form onSubmit={createEvent} style={{ marginTop: 12, marginBottom: 24 }}>
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
||||
<input
|
||||
placeholder="Event title"
|
||||
value={newEventTitle}
|
||||
onChange={(e) => setNewEventTitle(e.target.value)}
|
||||
style={{ padding: 8, flex: 1 }}
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
value={newEventDate}
|
||||
onChange={(e) => setNewEventDate(e.target.value)}
|
||||
style={{ padding: 8 }}
|
||||
/>
|
||||
<input
|
||||
placeholder="Location (optional)"
|
||||
value={newEventLocation}
|
||||
onChange={(e) => setNewEventLocation(e.target.value)}
|
||||
style={{ padding: 8 }}
|
||||
/>
|
||||
<button type="submit" style={{ padding: "8px 12px" }}>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
{events.length === 0 ? (
|
||||
<p>No events.</p>
|
||||
) : (
|
||||
<ul style={{ paddingLeft: 0, listStyle: "none" }}>
|
||||
{events.map((ev) => (
|
||||
<li
|
||||
key={ev.id}
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
padding: "8px 0",
|
||||
borderBottom: "1px solid #f2f2f2",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<strong>{ev.title}</strong>
|
||||
<div style={{ fontSize: 13, color: "#666" }}>
|
||||
{new Date(ev.date).toLocaleString()} {ev.location ? `• ${ev.location}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button style={{ marginRight: 8 }}>Edit</button>
|
||||
<button onClick={() => deleteEvent(ev)}>Delete</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{activeTab === "settings" && (
|
||||
<section>
|
||||
<h2>Settings</h2>
|
||||
<p>Application-wide administrative settings.</p>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<p style={{ fontSize: 14, color: "#444" }}>
|
||||
Use these settings to control global features. Implement actual controls as needed.
|
||||
</p>
|
||||
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<label style={{ display: "block", marginBottom: 8 }}>
|
||||
<input type="checkbox" /> Allow public registration
|
||||
</label>
|
||||
<label style={{ display: "block", marginBottom: 8 }}>
|
||||
<input type="checkbox" /> Require email verification
|
||||
</label>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<button onClick={() => alert("Settings saved (stub)")}>Save settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user