feat(admin/users): bannedReason textarea on user detail page

Adds a Textarea below the ban Switch that lets the admin write the reason
shown to the mobile user in the BannedModal. The reason is persisted on
blur via PATCH /api/users/[id] (existing route), and only rendered when
the user is currently banned to keep the UI tight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yanis 2026-05-01 12:10:23 +02:00
parent 792e969c00
commit af767879e3

View file

@ -1,11 +1,14 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { ArrowLeft, ScanLine, Trophy, Zap, Calendar } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
@ -45,6 +48,7 @@ const SEVERITY_STYLES: Record<string, string> = {
export default function UserDetailClient({ user }: UserDetailProps) {
const router = useRouter();
const [reasonDraft, setReasonDraft] = useState(user.bannedReason ?? "");
async function handleUpdate(data: Record<string, unknown>) {
try {
@ -61,6 +65,13 @@ export default function UserDetailClient({ user }: UserDetailProps) {
}
}
async function handleReasonBlur() {
const trimmed = reasonDraft.trim();
const current = user.bannedReason ?? "";
if (trimmed === current) return;
await handleUpdate({ bannedReason: trimmed.length > 0 ? trimmed : null });
}
const STAT_ITEMS = [
{ label: "Scans", value: user._count.scans, icon: ScanLine, color: "text-vine" },
{ label: "XP", value: user.xp, icon: Zap, color: "text-gold" },
@ -164,6 +175,26 @@ export default function UserDetailClient({ user }: UserDetailProps) {
onCheckedChange={(banned) => handleUpdate({ banned })}
/>
</div>
{user.banned && (
<div className="space-y-2">
<Label htmlFor="bannedReason" className="text-[12px] text-stone-400">
Raison du bannissement
</Label>
<Textarea
id="bannedReason"
value={reasonDraft}
onChange={(e) => setReasonDraft(e.target.value)}
onBlur={handleReasonBlur}
placeholder="Visible par l&apos;utilisateur sur mobile"
maxLength={500}
rows={3}
className="bg-[oklch(0.12_0.005_60)] border-[oklch(0.22_0.005_60)] text-cream"
/>
<p className="text-[11px] text-stone-600">
Affichee dans le mobile au prochain app boot. Maxi 500 caracteres.
</p>
</div>
)}
</div>
{/* Scan history */}