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:
parent
792e969c00
commit
af767879e3
|
|
@ -1,11 +1,14 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { ArrowLeft, ScanLine, Trophy, Zap, Calendar } from "lucide-react";
|
import { ArrowLeft, ScanLine, Trophy, Zap, Calendar } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
|
|
@ -45,6 +48,7 @@ const SEVERITY_STYLES: Record<string, string> = {
|
||||||
|
|
||||||
export default function UserDetailClient({ user }: UserDetailProps) {
|
export default function UserDetailClient({ user }: UserDetailProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [reasonDraft, setReasonDraft] = useState(user.bannedReason ?? "");
|
||||||
|
|
||||||
async function handleUpdate(data: Record<string, unknown>) {
|
async function handleUpdate(data: Record<string, unknown>) {
|
||||||
try {
|
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 = [
|
const STAT_ITEMS = [
|
||||||
{ label: "Scans", value: user._count.scans, icon: ScanLine, color: "text-vine" },
|
{ label: "Scans", value: user._count.scans, icon: ScanLine, color: "text-vine" },
|
||||||
{ label: "XP", value: user.xp, icon: Zap, color: "text-gold" },
|
{ label: "XP", value: user.xp, icon: Zap, color: "text-gold" },
|
||||||
|
|
@ -164,6 +175,26 @@ export default function UserDetailClient({ user }: UserDetailProps) {
|
||||||
onCheckedChange={(banned) => handleUpdate({ banned })}
|
onCheckedChange={(banned) => handleUpdate({ banned })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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'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>
|
</div>
|
||||||
|
|
||||||
{/* Scan history */}
|
{/* Scan history */}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue