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:
1 parent
d775e893f4
commit
0323c3fcaa
13 files changed
+222
-221
No files matched your search
@@ -0,0 +1,8 @@
|
||||
|
||||
> start
|
||||
> next start
|
||||
|
||||
▲ Next.js 16.2.9
|
||||
- Local: http://localhost:3000
|
||||
- Network: http://5.252.100.130:3000
|
||||
✓ Ready in 93ms
|
||||
@@ -48,11 +48,12 @@ export async function createHelpQuestion(formData: FormData): Promise<void> {
|
||||
targetType: "help_center_category",
|
||||
targetId: Number(entry.id),
|
||||
});
|
||||
} catch {
|
||||
// Unique name collision or DB error — re-render unchanged.
|
||||
} catch (error) {
|
||||
// Unique name collision or DB error — re-render unchanged with error.
|
||||
revalidatePath("/admin/help-questions");
|
||||
return;
|
||||
redirect("/admin/help-questions/new?error=Unique name collision or database error. Please try again.");
|
||||
}
|
||||
revalidatePath("/admin/help-questions");
|
||||
redirect("/admin/help-questions");
|
||||
}
|
||||
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -8,13 +8,13 @@ import { logStaffActivity } from "@/lib/services/staff-activity";
|
||||
import { CurrencyType } from "@/lib/services/currency";
|
||||
|
||||
const giveCurrency = async ({
|
||||
rcon,
|
||||
rconClient,
|
||||
db,
|
||||
userId,
|
||||
type,
|
||||
amount,
|
||||
}: {
|
||||
rcon: typeof rcon;
|
||||
rconClient: typeof rcon;
|
||||
db: typeof prisma;
|
||||
userId: number;
|
||||
type: string;
|
||||
@@ -101,7 +101,7 @@ export async function POST(request: Request) {
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
await giveCurrency({ rcon, db: prisma, userId, type: "credits", amount: credits });
|
||||
await giveCurrency({ rconClient: rcon, db: prisma, userId, type: "credits", amount: credits });
|
||||
return NextResponse.json(
|
||||
{ success: true, message: `Gave ${credits} credits to user #${userId}` },
|
||||
{ status: 200 }
|
||||
@@ -116,7 +116,7 @@ export async function POST(request: Request) {
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
await giveCurrency({ rcon, db: prisma, userId, type: "duckets", amount });
|
||||
await giveCurrency({ rconClient: rcon, db: prisma, userId, type: "duckets", amount });
|
||||
return NextResponse.json(
|
||||
{ success: true, message: `Gave ${amount} duckets to user #${userId}` },
|
||||
{ status: 200 }
|
||||
@@ -131,7 +131,7 @@ export async function POST(request: Request) {
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
await giveCurrency({ rcon, db: prisma, userId, type: "diamonds", amount });
|
||||
await giveCurrency({ rconClient: rcon, db: prisma, userId, type: "diamonds", amount });
|
||||
return NextResponse.json(
|
||||
{ success: true, message: `Gave ${amount} diamonds to user #${userId}` },
|
||||
{ status: 200 }
|
||||
@@ -146,7 +146,7 @@ export async function POST(request: Request) {
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
await giveCurrency({ rcon, db: prisma, userId, type: "points", amount });
|
||||
await giveCurrency({ rconClient: rcon, db: prisma, userId, type: "points", amount });
|
||||
return NextResponse.json(
|
||||
{ success: true, message: `Gave ${amount} points to user #${userId}` },
|
||||
{ status: 200 }
|
||||
|
||||
+4
-6
@@ -900,16 +900,14 @@ tbody tr:hover {
|
||||
border-color: #f97316;
|
||||
}
|
||||
|
||||
/* ── Glass dropdown (backdrop blur + animation) ─────────────── */
|
||||
.glass-dropdown {
|
||||
backdrop-filter: blur(20px) saturate(200%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(200%);
|
||||
background-color: color-mix(in srgb, var(--color-dropdown) 60%, transparent);
|
||||
/* ── Dropdown menu (solid color + animation) ─────────────────── */
|
||||
.dropdown-menu {
|
||||
background-color: var(--color-dropdown);
|
||||
border: 1px solid color-mix(in srgb, var(--color-text-muted) 14%, transparent);
|
||||
box-shadow: 0 8px 32px color-mix(in srgb, rgb(0 0 0) 20%, transparent);
|
||||
}
|
||||
|
||||
details[open] > .glass-dropdown {
|
||||
details[open] > .dropdown-menu {
|
||||
animation: dropdownIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export interface OnlineUser {
|
||||
|
||||
@@ -56,11 +56,12 @@ export function LanguageSwitcher() {
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div
|
||||
className="glass-dropdown absolute right-0 top-full mt-0.5 min-w-[160px] rounded-xl z-50 py-2"
|
||||
>
|
||||
{LOCALES.map((l) => (
|
||||
{open && (
|
||||
<div
|
||||
className="dropdown-menu absolute right-0 top-full mt-0.5 min-w-[160px] rounded-xl z-50 py-2"
|
||||
>
|
||||
{LOCALES.map((l) => (
|
||||
|
||||
<button
|
||||
key={l.code}
|
||||
type="button"
|
||||
|
||||
@@ -70,12 +70,13 @@ export function NavbarColorPicker() {
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{mounted && open && (
|
||||
<div
|
||||
className="glass-dropdown absolute right-0 top-full mt-0.5 min-w-[200px] rounded-xl z-50 py-3 px-2"
|
||||
>
|
||||
<div className="text-xs font-semibold uppercase tracking-wider mb-2 px-2" style={{ color: "var(--color-text-muted)" }}>Navbar kleur</div>
|
||||
<div className="grid grid-cols-5 gap-1.5">
|
||||
{mounted && open && (
|
||||
<div
|
||||
className="dropdown-menu absolute right-0 top-full mt-0.5 min-w-[200px] rounded-xl z-50 py-3 px-2"
|
||||
>
|
||||
<div className="text-xs font-semibold uppercase tracking-wider mb-2 px-2" style={{ color: "var(--color-text-muted)" }}>Navbar kleur</div>
|
||||
<div className="grid grid-cols-5 gap-1.5">
|
||||
|
||||
{COLORS.map((c, i) => (
|
||||
<button
|
||||
key={c.name}
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function Navigation() {
|
||||
{t("community")}
|
||||
<svg className="ml-auto md:ml-1 w-3 h-3 transition-transform duration-200 group-open:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
|
||||
</summary>
|
||||
<div className="glass-dropdown md:absolute left-0 top-full mt-0.5 min-w-[200px] rounded-xl z-50 py-2">
|
||||
<div className="dropdown-menu md:absolute left-0 top-full mt-0.5 min-w-[200px] rounded-xl z-50 py-2">
|
||||
<Link href="/news" className="dropdown-item">{t("articles")}</Link>
|
||||
<Link href="/photos" className="dropdown-item">{t("photos")}</Link>
|
||||
<Link href="/staff" className="dropdown-item">{t("staff")}</Link>
|
||||
@@ -51,7 +51,7 @@ export async function Navigation() {
|
||||
{t("shop")}
|
||||
<svg className="ml-auto md:ml-1 w-3 h-3 transition-transform duration-200 group-open:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
|
||||
</summary>
|
||||
<div className="glass-dropdown md:absolute left-0 top-full mt-0.5 min-w-[200px] rounded-xl z-50 py-2">
|
||||
<div className="dropdown-menu md:absolute left-0 top-full mt-0.5 min-w-[200px] rounded-xl z-50 py-2">
|
||||
<Link href="/shop" className="dropdown-item">{t("shop")}</Link>
|
||||
<Link href="/marketplace" className="dropdown-item">{t("market")}</Link>
|
||||
<Link href="/rares" className="dropdown-item">{t("rareValues")}</Link>
|
||||
@@ -74,7 +74,7 @@ export async function Navigation() {
|
||||
{t("assistance")}
|
||||
<svg className="ml-auto md:ml-1 w-3 h-3 transition-transform duration-200 group-open:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
|
||||
</summary>
|
||||
<div className="glass-dropdown md:absolute left-0 top-full mt-0.5 min-w-[200px] rounded-xl z-50 py-2">
|
||||
<div className="dropdown-menu md:absolute left-0 top-full mt-0.5 min-w-[200px] rounded-xl z-50 py-2">
|
||||
<Link href="/help" className="dropdown-item">{t("helpCenter")}</Link>
|
||||
{session?.user ? (
|
||||
<Link href="/help/tickets" className="dropdown-item">{t("myTickets")}</Link>
|
||||
|
||||
@@ -66,71 +66,72 @@ export async function TopHeader() {
|
||||
>
|
||||
Administration
|
||||
</summary>
|
||||
<div
|
||||
className="glass-dropdown absolute right-0 top-full mt-0.5 min-w-[180px] rounded-xl z-50 py-2"
|
||||
>
|
||||
<Link href="/admin" className="dropdown-item">
|
||||
Admin panel
|
||||
</Link>
|
||||
<Link href="/admin/commandocentrum" className="dropdown-item">
|
||||
Commandocentrum
|
||||
</Link>
|
||||
<Link href="/admin/logs" className="dropdown-item">
|
||||
Logs
|
||||
</Link>
|
||||
</div>
|
||||
</details>
|
||||
) : null}
|
||||
|
||||
<details className="relative">
|
||||
<summary className="list-none cursor-pointer flex items-center [&::-webkit-details-marker]:hidden">
|
||||
{/* biome-ignore lint/performance/noImgElement: external imager */}
|
||||
<img className="w-[54px] h-[62px] bg-no-repeat bg-center" src={avatar} alt="" />
|
||||
<span className="-ml-2 font-semibold" style={{ color: "var(--color-navbar-text, var(--color-text))" }}>{session.user.name}</span>
|
||||
</summary>
|
||||
<div
|
||||
className="glass-dropdown absolute right-0 top-full mt-0.5 min-w-[200px] rounded-xl z-50 py-2"
|
||||
>
|
||||
<Link href="/me" className="dropdown-item">
|
||||
My dashboard
|
||||
</Link>
|
||||
<Link href={`/u/${session.user.name}`} className="dropdown-item">
|
||||
My profile
|
||||
</Link>
|
||||
<Link href="/settings" className="dropdown-item">
|
||||
User settings
|
||||
</Link>
|
||||
<Link href="/settings/2fa" className="dropdown-item">
|
||||
Two-factor
|
||||
</Link>
|
||||
<Link href="/settings/sessions" className="dropdown-item">
|
||||
Login activity
|
||||
</Link>
|
||||
<div className="h-px my-1 mx-3" style={{ backgroundColor: "color-mix(in srgb, var(--color-text-muted) 12%, transparent)" }} />
|
||||
<form
|
||||
action={async () => {
|
||||
"use server";
|
||||
await signOut({ redirectTo: "/" });
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
className="dropdown-item w-full text-center mx-1 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: "color-mix(in srgb, var(--button-danger-color) 12%, transparent)",
|
||||
color: "var(--button-danger-color)",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
width: "calc(100% - 8px)",
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div
|
||||
className="dropdown-menu absolute right-0 top-full mt-0.5 min-w-[180px] rounded-xl z-50 py-2"
|
||||
>
|
||||
<Link href="/admin" className="dropdown-item">
|
||||
Admin panel
|
||||
</Link>
|
||||
<Link href="/admin/commandocentrum" className="dropdown-item">
|
||||
Commandocentrum
|
||||
</Link>
|
||||
<Link href="/admin/logs" className="dropdown-item">
|
||||
Logs
|
||||
</Link>
|
||||
</div>
|
||||
</details>
|
||||
) : null}
|
||||
|
||||
<details className="relative">
|
||||
<summary className="list-none cursor-pointer flex items-center [&::-webkit-details-marker]:hidden">
|
||||
{/* biome-ignore lint/performance/noImgElement: external imager */}
|
||||
<img className="w-[54px] h-[62px] bg-no-repeat bg-center" src={avatar} alt="" />
|
||||
<span className="-ml-2 font-semibold" style={{ color: "var(--color-navbar-text, var(--color-text))" }}>{session.user.name}</span>
|
||||
</summary>
|
||||
<div
|
||||
className="dropdown-menu absolute right-0 top-full mt-0.5 min-w-[200px] rounded-xl z-50 py-2"
|
||||
>
|
||||
<Link href="/me" className="dropdown-item">
|
||||
My dashboard
|
||||
</Link>
|
||||
<Link href={`/u/${session.user.name}`} className="dropdown-item">
|
||||
My profile
|
||||
</Link>
|
||||
<Link href="/settings" className="dropdown-item">
|
||||
User settings
|
||||
</Link>
|
||||
<Link href="/settings/2fa" className="dropdown-item">
|
||||
Two-factor
|
||||
</Link>
|
||||
<Link href="/settings/sessions" className="dropdown-item">
|
||||
Login activity
|
||||
</Link>
|
||||
<div className="h-px my-1 mx-3" style={{ backgroundColor: "color-mix(in srgb, var(--color-text-muted) 12%, transparent)" }} />
|
||||
<form
|
||||
action={async () => {
|
||||
"use server";
|
||||
await signOut({ redirectTo: "/" });
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
className="dropdown-item w-full text-center mx-1 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: "color-mix(in srgb, var(--button-danger-color) 12%, transparent)",
|
||||
color: "var(--button-danger-color)",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
width: "calc(100% - 8px)",
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user