chrysopedia/frontend/src/pages/AdminUsers.tsx
jlightner 4969935c76 feat: Added ConfirmModal component, Edit As button with write-mode conf…
- "frontend/src/components/ConfirmModal.tsx"
- "frontend/src/components/ConfirmModal.module.css"
- "frontend/src/api/auth.ts"
- "frontend/src/context/AuthContext.tsx"
- "frontend/src/pages/AdminUsers.tsx"
- "frontend/src/pages/AdminUsers.module.css"
- "frontend/src/components/ImpersonationBanner.tsx"
- "frontend/src/components/ImpersonationBanner.module.css"

GSD-Task: S07/T02
2026-04-04 06:27:38 +00:00

128 lines
4 KiB
TypeScript

import { useEffect, useState } from "react";
import { useAuth } from "../context/AuthContext";
import { fetchUsers, type UserListItem } from "../api";
import { useDocumentTitle } from "../hooks/useDocumentTitle";
import ConfirmModal from "../components/ConfirmModal";
import styles from "./AdminUsers.module.css";
export default function AdminUsers() {
useDocumentTitle("Users — Admin");
const { token, startImpersonation, user: currentUser } = useAuth();
const [users, setUsers] = useState<UserListItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [impersonating, setImpersonating] = useState<string | null>(null);
const [editTarget, setEditTarget] = useState<UserListItem | null>(null);
useEffect(() => {
if (!token) return;
setLoading(true);
fetchUsers(token)
.then(setUsers)
.catch((e) => setError(e.message || "Failed to load users"))
.finally(() => setLoading(false));
}, [token]);
async function handleViewAs(userId: string) {
setImpersonating(userId);
try {
await startImpersonation(userId);
} catch (e: any) {
setError(e.message || "Failed to start impersonation");
setImpersonating(null);
}
}
async function handleEditAsConfirm() {
if (!editTarget) return;
setEditTarget(null);
setImpersonating(editTarget.id);
try {
await startImpersonation(editTarget.id, true);
} catch (e: any) {
setError(e.message || "Failed to start write-mode impersonation");
setImpersonating(null);
}
}
if (loading) {
return (
<div className={styles.page}>
<h1 className={styles.title}>Users</h1>
<p className={styles.loading}>Loading users</p>
</div>
);
}
if (error) {
return (
<div className={styles.page}>
<h1 className={styles.title}>Users</h1>
<p className={styles.error}>{error}</p>
</div>
);
}
return (
<div className={styles.page}>
<h1 className={styles.title}>Users</h1>
{users.length === 0 ? (
<p className={styles.empty}>No users found.</p>
) : (
<table className={styles.table}>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{users.map((u) => (
<tr key={u.id}>
<td>{u.display_name}</td>
<td>{u.email}</td>
<td>
<span className={styles.roleBadge} data-role={u.role}>
{u.role}
</span>
</td>
<td>
{u.role === "creator" && u.id !== currentUser?.id && (
<div className={styles.actionBtns}>
<button
className={styles.viewAsBtn}
disabled={impersonating === u.id}
onClick={() => handleViewAs(u.id)}
>
{impersonating === u.id ? "Switching…" : "View As"}
</button>
<button
className={styles.editAsBtn}
disabled={impersonating === u.id}
onClick={() => setEditTarget(u)}
>
Edit As
</button>
</div>
)}
</td>
</tr>
))}
</tbody>
</table>
)}
<ConfirmModal
open={!!editTarget}
title="Enable Write Mode"
message={`You are about to edit as ${editTarget?.display_name ?? "this user"}. Changes will be attributed to this creator and logged. Continue?`}
confirmLabel="Enable Write Mode"
cancelLabel="Cancel"
variant="danger"
onConfirm={handleEditAsConfirm}
onCancel={() => setEditTarget(null)}
/>
</div>
);
}