feat(admin): ban appeals, photos polish, economy adjust, staff smoke
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 1m17s
CI / check (push) Successful in 25s
CI / deploy (push) Failing after 11s

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-07-30 20:23:03 +02:00
1 parent fe46bd0544
commit 3ad3f9512c
36 files changed
+1185 -293

No files matched your search

@@ -6,6 +6,7 @@ import { useTranslations } from "next-intl";
import { useEffect, useRef, useState } from "react";
import {
closeHelpCenterTicket,
liftBanFromHelpTicket,
reopenHelpCenterTicket,
replyHelpCenterTicket,
} from "@/actions/admin-help-tickets";
@@ -30,6 +31,7 @@ interface HelpTicketInfo {
open: boolean;
createdAt: string;
updatedAt: string;
categoryName?: string | null;
creator: {
id: number;
username: string;
@@ -42,12 +44,17 @@ export function AdminHelpTicketDetail({
ticket,
messages: initialMessages,
canEdit,
canLiftBan = false,
hasActiveBan = false,
listHref = "/admin/help-tickets",
hideRequesterContact = false,
}: {
ticket: HelpTicketInfo;
messages: ThreadMessage[];
canEdit: boolean;
/** Staff may lift requester bans (USERS_BAN). */
canLiftBan?: boolean;
hasActiveBan?: boolean;
listHref?: string;
/** Hide email/IP-style contact for mod-lite surfaces. */
hideRequesterContact?: boolean;
@@ -94,6 +101,13 @@ export function AdminHelpTicketDetail({
});
}
function handleLiftBan() {
if (!ticket.creator || isPending) return;
run(() => liftBanFromHelpTicket({ ticketId: ticket.id }), {
successMessage: t("success.banLifted"),
});
}
return (
<div className="space-y-6">
<div className="flex items-start gap-4">
@@ -110,6 +124,12 @@ export function AdminHelpTicketDetail({
<Badge variant={ticket.open ? "default" : "secondary"}>
{ticket.open ? t("statusOpen") : t("statusClosed")}
</Badge>
{ticket.categoryName ? (
<Badge variant="outline">{ticket.categoryName}</Badge>
) : null}
{hasActiveBan ? (
<Badge variant="destructive">{t("requesterBanned")}</Badge>
) : null}
</div>
<h1 className="text-2xl font-bold">{ticket.title}</h1>
</div>
@@ -208,13 +228,24 @@ export function AdminHelpTicketDetail({
</div>
<div className="space-y-4">
{canEdit && (
{(canEdit || (canLiftBan && hasActiveBan)) && (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm">{t("actionsTitle")}</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{ticket.open ? (
{canLiftBan && hasActiveBan && ticket.creator ? (
<Button
variant="destructive"
size="sm"
className="w-full"
disabled={isPending}
onClick={handleLiftBan}
>
{t("liftBanSubmit")}
</Button>
) : null}
{canEdit && ticket.open ? (
<Button
variant="outline"
size="sm"
@@ -224,7 +255,8 @@ export function AdminHelpTicketDetail({
>
{t("closeSubmit")}
</Button>
) : (
) : null}
{canEdit && !ticket.open ? (
<Button
variant="outline"
size="sm"
@@ -234,7 +266,7 @@ export function AdminHelpTicketDetail({
>
{t("reopenSubmit")}
</Button>
)}
) : null}
</CardContent>
</Card>
)}
+25 -3
View File
@@ -3,6 +3,7 @@ import { getTranslations } from "next-intl/server";
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
import { positiveBigInt } from "@/lib/api";
import { activeBanWhere } from "@/lib/bans";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { AdminHelpTicketDetail } from "./admin-help-ticket-detail";
@@ -29,6 +30,7 @@ export default async function AdminHelpTicketDetailPage({
select: {
id: true,
userId: true,
categoryId: true,
title: true,
content: true,
open: true,
@@ -57,9 +59,9 @@ export default async function AdminHelpTicketDetailPage({
]),
];
const users =
const [users, category, activeBan] = await Promise.all([
authorIds.length > 0
? await prisma.user.findMany({
? prisma.user.findMany({
where: { id: { in: authorIds } },
select: {
id: true,
@@ -68,7 +70,20 @@ export default async function AdminHelpTicketDetailPage({
mail: true,
},
})
: [];
: Promise.resolve([]),
ticket.categoryId != null
? prisma.websiteHelpCenterCategories.findUnique({
where: { id: ticket.categoryId },
select: { name: true },
})
: Promise.resolve(null),
ticket.userId != null
? prisma.ban.findFirst({
where: { userId: ticket.userId, ...activeBanWhere() },
select: { id: true },
})
: Promise.resolve(null),
]);
const userById = new Map(users.map((u) => [u.id, u]));
const openerId = ticket.userId;
@@ -104,6 +119,10 @@ export default async function AdminHelpTicketDetailPage({
const creator = openerId != null ? userById.get(openerId) : undefined;
const canEdit = canAccess(permissions, PERMS.TICKETS_EDIT, session.user.rank);
const canLiftBan = canAccess(permissions, PERMS.USERS_BAN, session.user.rank);
const isBanAppeal =
/ban\s*appeal/i.test(ticket.title) ||
/ban\s*appeal/i.test(category?.name ?? "");
return (
<div className="space-y-6">
@@ -119,6 +138,7 @@ export default async function AdminHelpTicketDetailPage({
open: ticket.open,
createdAt: (ticket.createdAt ?? new Date()).toISOString(),
updatedAt: (ticket.updatedAt ?? new Date()).toISOString(),
categoryName: category?.name ?? (isBanAppeal ? "Ban appeal" : null),
creator: creator
? {
id: creator.id,
@@ -130,6 +150,8 @@ export default async function AdminHelpTicketDetailPage({
}}
messages={messages}
canEdit={canEdit}
canLiftBan={canLiftBan}
hasActiveBan={!!activeBan}
/>
</div>
);