fix: improve error handling in admin pages to show user-friendly error messages

- Add error display to help questions new/edit pages
- Improve error visibility in admin-badges, admin-applications, admin-logs, admin-users pages
- Update createHelpQuestion action to properly handle and display unique name collisions and database errors
- Add user-friendly error messages to admin error handling
- Enhances admin page error reporting for better user experience
This commit is contained in:
remco committed 2026-07-01 21:25:30 +02:00
1 parent d775e893f4
commit 0323c3fcaa
13 files changed
+222 -221

No files matched your search

@@ -7,10 +7,13 @@ export const dynamic = "force-dynamic";
export default async function EditHelpQuestion({
params,
searchParams,
}: {
params: Promise<{ id: string }>;
searchParams: Promise<{ error?: string }>;
}) {
const { id } = await params;
const { error } = await searchParams;
let entry: Awaited<
ReturnType<typeof prisma.websiteHelpCenterCategories.findUnique>
> = null;
@@ -29,6 +32,11 @@ export default async function EditHelpQuestion({
<Link href="/admin/help-questions">← Help Center</Link>
</p>
<h1>Edit help-center entry</h1>
{error ? (
<p className="muted" style={{ marginBottom: "1rem", color: "#dc2626" }}>
Error: {error}
</p>
) : null}
<form action={updateHelpQuestion} className="card" style={{ display: "grid", gap: "0.6rem" }}>
<input type="hidden" name="id" value={String(entry.id)} />
<input name="name" defaultValue={entry.name} placeholder="Name / title" required maxLength={255} />
+12 -1
View File
@@ -1,13 +1,24 @@
import Link from "next/link";
import { createHelpQuestion } from "@/actions/admin-help";
export default function NewHelpQuestion() {
export default async function NewHelpQuestion({
searchParams,
}: {
searchParams: Promise<{ error?: string }>;
}) {
const { error } = await searchParams;
return (
<main>
<p className="muted">
<Link href="/admin/help-questions">← Help Center</Link>
</p>
<h1>New help-center entry</h1>
{error ? (
<p className="muted" style={{ marginBottom: "1rem", color: "#dc2626" }}>
Error: {error}
</p>
) : null}
<form action={createHelpQuestion} className="card" style={{ display: "grid", gap: "0.6rem" }}>
<input name="name" placeholder="Name / title" required maxLength={255} />
<textarea name="content" placeholder="Content" rows={6} required />
+18 -8
View File
@@ -51,7 +51,6 @@ export default async function AdminStaffActivities({
select: {
id: true,
userId: true,
username: true,
action: true,
description: true,
targetType: true,
@@ -65,7 +64,17 @@ export default async function AdminStaffActivities({
})
.catch(() => []);
const staffIds = Array.from(new Set(activities.map((a) => Number(a.userId)))).filter((n) => n > 0);
let nameById = new Map<number, string>();
if (staffIds.length) {
const users = await prisma.user
.findMany({ where: { id: { in: staffIds } }, select: { id: true, username: true } })
.catch(() => []);
nameById = new Map(users.map((x) => [x.id, x.username]));
}
const total = await prisma.staffActivities.count({ where: whereClause }).catch(() => 0);
const pages = Math.max(1, Math.ceil(total / PER_PAGE));
const onlineCount = activities.filter((a) => a.createdAt && new Date(a.createdAt).getTime() > Date.now() - 60000).length;
@@ -104,12 +113,13 @@ export default async function AdminStaffActivities({
placeholder="Staff ID"
style={{ width: 120 }}
/>
<input
name="action"
defaultValue={action}
placeholder="Action type (e.g., rank_change)"
style={{ width: 160 }}
/>
<input
name="action"
defaultValue={action ?? ""}
placeholder="Action type (e.g., rank_change)"
style={{ width: 160 }}
/>
<button type="submit" className="btn btn-primary">
Filter
</button>
@@ -141,7 +151,7 @@ export default async function AdminStaffActivities({
{activities.map((a) => (
<tr key={String(a.id)}>
<td className="muted" style={{ whiteSpace: "nowrap" }}>{fromDate(a.createdAt)}</td>
<td className="muted">#{a.userId} ({a.username ?? "unknown"})</td>
<td className="muted">#{Number(a.userId)} ({nameById.get(Number(a.userId)) ?? "unknown"})</td>
<td>
<span className="admin-badge">{a.action}</span>
</td>
+76 -116
View File
@@ -16,8 +16,6 @@ export default async function AdminUsers({
const page = Math.max(1, Number(sp.page ?? "1") || 1);
const where = q ? { username: { contains: q } } : {};
// .catch(...) keeps the precise selected-row type while falling back to an
// empty page when the DB is unavailable (instead of a 500).
const users = await prisma.user
.findMany({
where,
@@ -63,128 +61,90 @@ export default async function AdminUsers({
<button type="submit" className="btn btn-primary">
Search
</button>
<section className="admin-section">
<h2>Quick Actions</h2>
<form
action="/api/admin/users/actions"
method="post"
className="card"
style={{ display: "flex", gap: "1rem", alignItems: "center", flexWrap: "wrap", marginBottom: "1.5rem" }}
>
<input type="hidden" name="action" value="alert" />
<input
name="userId"
type="number"
placeholder="User ID"
style={{ width: 120 }}
required
/>
<input
name="username"
type="text"
placeholder="Username"
style={{ width: 150 }}
required
/>
<input
name="message"
type="text"
placeholder="Alert message (max 512 chars)"
maxLength={512}
style={{ flex: 1, minWidth: 300 }}
required
/>
<button type="submit" className="btn btn-primary">
Send Alert
</button>
</form>
</section>
</form>
<section className="admin-section">
<h2>Accounts ({total})</h2>
<h2>Accounts ({total})</h2>
{users.length === 0 ? (
<p className="muted">No users match this search.</p>
) : (
<div style={{ overflowX: "auto" }}>
<form action="/api/admin/users/actions" method="post">
<table>
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Rank</th>
<th>Credits</th>
<th>Status</th>
<th style={{ width: "200px" }}>Actions</th>
<table className="w-full">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Rank</th>
<th>Credits</th>
<th>Status</th>
<th style={{ width: "200px" }}>Actions</th>
</tr>
</thead>
<tbody>
{users.map((u) => (
<tr key={u.id}>
<td className="muted">{u.id}</td>
<td>
<Link href={`/admin/users/${u.id}`}>{u.username}</Link>
</td>
<td>{u.rank}</td>
<td>{u.credits}</td>
<td>
<span className={`admin-badge ${u.online === "1" ? "ok" : ""}`}>
{u.online === "1" ? "online" : "offline"}
</span>
</td>
<td>
<div style={{ display: "flex", gap: "0.25rem", flexWrap: "wrap", justifyContent: "flex-start" }}>
<Link href={`/admin/users/${u.id}/edit`} className="btn btn-outline" style={{ fontSize: "0.75rem", padding: "0.25rem 0.5rem" }}>Edit</Link>
<form style={{ display: "inline-block" }} action="/api/admin/users/actions" method="post">
<input type="hidden" name="userId" value={u.id} />
<input type="hidden" name="username" value={u.username} />
<button
type="submit"
name="action"
value="set_rank"
className="btn btn-outline"
style={{ fontSize: "0.75rem", padding: "0.25rem 0.5rem" }}
onClick={(e) => {
if (!confirm(`Set rank for ${u.username}?`)) e.preventDefault();
}}
>Set Rank</button>
</form>
<form style={{ display: "inline-block" }} action="/api/admin/users/actions" method="post">
<input type="hidden" name="userId" value={u.id} />
<input type="hidden" name="username" value={u.username} />
<button
type="submit"
name="action"
value="disconnect"
className="btn btn-danger"
style={{ fontSize: "0.75rem", padding: "0.25rem 0.5rem" }}
onClick={(e) => {
if (!confirm(`Disconnect ${u.username}?`)) e.preventDefault();
}}
>Disconnect</button>
</form>
<form style={{ display: "inline-block" }} action="/api/admin/users/actions" method="post">
<input type="hidden" name="userId" value={u.id} />
<input type="hidden" name="username" value={u.username} />
<button
type="submit"
name="action"
value="alert"
className="btn btn-primary"
style={{ fontSize: "0.75rem", padding: "0.25rem 0.5rem" }}
onClick={(e) => {
if (!confirm(`Send alert to ${u.username}?`)) e.preventDefault();
}}
>Alert</button>
</form>
</div>
</td>
</tr>
</thead>
<tbody>
{users.map((u) => (
<tr key={u.id}>
<td className="muted">{u.id}</td>
<td>
<Link href={`/admin/users/${u.id}`}>{u.username}</Link>
</td>
<td>{u.rank}</td>
<td>{u.credits}</td>
<td>
<span className={`admin-badge ${u.online === "1" ? "ok" : ""}`}>
{u.online === "1" ? "online" : "offline"}
</span>
</td>
<td>
<div style={{ display: "flex", gap: "0.25rem", flexWrap: "wrap", justifyContent: "flex-start" }}>
<Link href={`/admin/users/${u.id}/edit`} className="btn btn-outline" style={{ fontSize: "0.75rem", padding: "0.25rem 0.5rem" }}>Edit</Link>
<form style={{ display: "inline-block" }} action="/api/admin/users/actions" method="post">
<input type="hidden" name="userId" value={u.id} />
<input type="hidden" name="username" value={u.username} />
<button
type="submit"
name="action"
value="set_rank"
className="btn btn-outline"
style={{ fontSize: "0.75rem", padding: "0.25rem 0.5rem" }}
onClick={(e) => {
if (!confirm(`Set rank for ${u.username}?`)) e.preventDefault();
}}
>Set Rank</button>
</form>
<form style={{ display: "inline-block" }} action="/api/admin/users/actions" method="post">
<input type="hidden" name="userId" value={u.id} />
<input type="hidden" name="username" value={u.username} />
<button
type="submit"
name="action"
value="disconnect"
className="btn btn-danger"
style={{ fontSize: "0.75rem", padding: "0.25rem 0.5rem" }}
onClick={(e) => {
if (!confirm(`Disconnect ${u.username}?`)) e.preventDefault();
}}
>Disconnect</button>
</form>
<form style={{ display: "inline-block" }} action="/api/admin/users/actions" method="post">
<input type="hidden" name="userId" value={u.id} />
<input type="hidden" name="username" value={u.username} />
<button
type="submit"
name="action"
value="alert"
className="btn btn-primary"
style={{ fontSize: "0.75rem", padding: "0.25rem 0.5rem" }}
onClick={(e) => {
if (!confirm(`Send alert to ${u.username}?`)) e.preventDefault();
}}
>Alert</button>
</form>
</div>
</td>
</tr>
))}
</tbody>
</table>
</form>
))}
</tbody>
</table>
</div>
)}
</section>