style: format code biome

This commit is contained in:
openhands committed 2026-07-13 21:57:41 +02:00
1 parent 8efd032cc6
commit df38dccbf1
735 files changed
+128321 -120870

No files matched your search

+167 -127
View File
@@ -1,10 +1,10 @@
import { User } from "lucide-react";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { updateUser } from "@/actions/admin-user-edit";
import { requireStaff } from "@/lib/admin/guard";
import { User } from "lucide-react";
import { prisma } from "@/lib/prisma";
import { getTranslations } from "next-intl/server";
export const dynamic = "force-dynamic";
@@ -12,146 +12,186 @@ export const dynamic = "force-dynamic";
const DUCKETS_TYPE = 0;
const DIAMONDS_TYPE = 5;
export default async function AdminUserEdit({ params }: { params: Promise<{ id: string }> }) {
// Gate the page with the SAME helper the action re-checks.
await requireStaff();
export default async function AdminUserEdit({
params,
}: {
params: Promise<{ id: string }>;
}) {
// Gate the page with the SAME helper the action re-checks.
await requireStaff();
const { id } = await params;
const t = await getTranslations("pages.admin.users.editForm");
const userId = Number(id);
if (!Number.isInteger(userId) || userId <= 0) notFound();
const { id } = await params;
const t = await getTranslations("pages.admin.users.editForm");
const userId = Number(id);
if (!Number.isInteger(userId) || userId <= 0) notFound();
let user: {
id: number;
username: string;
mail: string | null;
motto: string;
look: string;
rank: number;
credits: number;
pixels: number;
points: number;
} | null = null;
let duckets = 0;
let diamonds = 0;
let user: {
id: number;
username: string;
mail: string | null;
motto: string;
look: string;
rank: number;
credits: number;
pixels: number;
points: number;
} | null = null;
let duckets = 0;
let diamonds = 0;
try {
user = await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
username: true,
mail: true,
motto: true,
look: true,
rank: true,
credits: true,
pixels: true,
points: true,
},
});
if (user) {
const currencies = await prisma.usersCurrency.findMany({
where: { userId, type: { in: [DUCKETS_TYPE, DIAMONDS_TYPE] } },
select: { type: true, amount: true },
});
for (const c of currencies) {
if (c.type === DUCKETS_TYPE) duckets = c.amount;
else if (c.type === DIAMONDS_TYPE) diamonds = c.amount;
}
}
} catch {
return (
<main>
<p className="text-sm theme-text-muted dark:theme-text-muted">
<Link href="/admin/users">← {t("title")}</Link>
</p>
<h1>{t("title")}</h1>
<p className="text-xs theme-text-muted dark:theme-text-muted">{t("loadError")}</p>
</main>
);
}
try {
user = await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
username: true,
mail: true,
motto: true,
look: true,
rank: true,
credits: true,
pixels: true,
points: true,
},
});
if (user) {
const currencies = await prisma.usersCurrency.findMany({
where: { userId, type: { in: [DUCKETS_TYPE, DIAMONDS_TYPE] } },
select: { type: true, amount: true },
});
for (const c of currencies) {
if (c.type === DUCKETS_TYPE) duckets = c.amount;
else if (c.type === DIAMONDS_TYPE) diamonds = c.amount;
}
}
} catch {
return (
<main>
<p className="text-sm theme-text-muted dark:theme-text-muted">
<Link href="/admin/users">← {t("title")}</Link>
</p>
<h1>{t("title")}</h1>
<p className="text-xs theme-text-muted dark:theme-text-muted">
{t("loadError")}
</p>
</main>
);
}
if (!user) notFound();
if (!user) notFound();
return (
<main>
<p className="text-sm theme-text-muted dark:theme-text-muted mb-2">
<Link href={`/admin/users/${user.id}`}>← {user.username}</Link>
</p>
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-[var(--admin-accent)]/20 to-[var(--admin-accent)]/5 grid place-items-center">
<User size={20} className="text-[var(--admin-accent)]" />
</div>
<div>
<h1 className="m-0 text-xl font-extrabold text-[var(--admin-text)]">
{t("title", { username: user.username })}
</h1>
<p className="m-0 text-xs text-[var(--admin-text-muted)] mt-0.5">{t("idLabel", { id: user.id })}</p>
</div>
</div>
return (
<main>
<p className="text-sm theme-text-muted dark:theme-text-muted mb-2">
<Link href={`/admin/users/${user.id}`}>← {user.username}</Link>
</p>
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-[var(--admin-accent)]/20 to-[var(--admin-accent)]/5 grid place-items-center">
<User size={20} className="text-[var(--admin-accent)]" />
</div>
<div>
<h1 className="m-0 text-xl font-extrabold text-[var(--admin-text)]">
{t("title", { username: user.username })}
</h1>
<p className="m-0 text-xs text-[var(--admin-text-muted)] mt-0.5">
{t("idLabel", { id: user.id })}
</p>
</div>
</div>
<form action={updateUser} className="admin-card">
<input type="hidden" name="id" value={user.id} />
<form action={updateUser} className="admin-card">
<input type="hidden" name="id" value={user.id} />
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<label>
{t("email")}
<input name="mail" type="email" defaultValue={user.mail ?? ""} maxLength={500} />
</label>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<label>
{t("email")}
<input
name="mail"
type="email"
defaultValue={user.mail ?? ""}
maxLength={500}
/>
</label>
<label>
{t("rank")}
<input name="rank" type="number" min={1} defaultValue={user.rank} />
</label>
<label>
{t("rank")}
<input name="rank" type="number" min={1} defaultValue={user.rank} />
</label>
<label>
{t("motto")}
<input name="motto" defaultValue={user.motto} maxLength={127} />
</label>
<label>
{t("motto")}
<input name="motto" defaultValue={user.motto} maxLength={127} />
</label>
<label>
{t("look")}
<input name="look" defaultValue={user.look} maxLength={256} />
</label>
<label>
{t("look")}
<input name="look" defaultValue={user.look} maxLength={256} />
</label>
<label>
{t("credits")}
<input name="credits" type="number" min={0} defaultValue={user.credits} />
</label>
<label>
{t("credits")}
<input
name="credits"
type="number"
min={0}
defaultValue={user.credits}
/>
</label>
<label>
{t("pixels")}
<input name="pixels" type="number" min={0} defaultValue={user.pixels} />
</label>
<label>
{t("pixels")}
<input
name="pixels"
type="number"
min={0}
defaultValue={user.pixels}
/>
</label>
<label>
{t("points")}
<input name="points" type="number" min={0} defaultValue={user.points} />
</label>
<label>
{t("points")}
<input
name="points"
type="number"
min={0}
defaultValue={user.points}
/>
</label>
<label>
{t("duckets")}
<input name="duckets" type="number" min={0} defaultValue={duckets} />
</label>
<label>
{t("duckets")}
<input
name="duckets"
type="number"
min={0}
defaultValue={duckets}
/>
</label>
<label>
{t("diamonds")}
<input name="diamonds" type="number" min={0} defaultValue={diamonds} />
</label>
</div>
<label>
{t("diamonds")}
<input
name="diamonds"
type="number"
min={0}
defaultValue={diamonds}
/>
</label>
</div>
<p className="text-xs theme-text-muted dark:theme-text-muted">{t("hint")}</p>
<p className="text-xs theme-text-muted dark:theme-text-muted">
{t("hint")}
</p>
<div className="flex gap-2 mt-2">
<button type="submit" className="btn btn-primary">
{t("save")}
</button>
<Link href={`/admin/users/${user.id}`} className="btn">
{t("cancel")}
</Link>
</div>
</form>
</main>
);
<div className="flex gap-2 mt-2">
<button type="submit" className="btn btn-primary">
{t("save")}
</button>
<Link href={`/admin/users/${user.id}`} className="btn">
{t("cancel")}
</Link>
</div>
</form>
</main>
);
}
+43 -43
View File
@@ -1,50 +1,50 @@
import { Skeleton } from "@/components/ui/skeleton";
export default function UserDetailLoading() {
return (
<div className="space-y-6">
{/* Breadcrumb */}
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-4 w-4" />
<Skeleton className="h-4 w-24" />
</div>
return (
<div className="space-y-6">
{/* Breadcrumb */}
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-4 w-4" />
<Skeleton className="h-4 w-24" />
</div>
{/* User header */}
<div className="flex items-center gap-4">
<Skeleton className="h-16 w-16 rounded" />
<div>
<Skeleton className="h-9 w-40" />
<div className="flex items-center gap-2 mt-1">
<Skeleton className="h-5 w-16 rounded-full" />
<Skeleton className="h-5 w-16 rounded-full" />
</div>
</div>
</div>
{/* User header */}
<div className="flex items-center gap-4">
<Skeleton className="h-16 w-16 rounded" />
<div>
<Skeleton className="h-9 w-40" />
<div className="flex items-center gap-2 mt-1">
<Skeleton className="h-5 w-16 rounded-full" />
<Skeleton className="h-5 w-16 rounded-full" />
</div>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2">
{/* Form */}
<div className="rounded-lg border bg-card p-6 space-y-4">
<Skeleton className="h-6 w-24" />
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="space-y-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-10 w-full" />
</div>
))}
<Skeleton className="h-10 w-24 mt-4" />
</div>
<div className="grid gap-6 lg:grid-cols-2">
{/* Form */}
<div className="rounded-lg border bg-card p-6 space-y-4">
<Skeleton className="h-6 w-24" />
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="space-y-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-10 w-full" />
</div>
))}
<Skeleton className="h-10 w-24 mt-4" />
</div>
{/* Side cards */}
<div className="space-y-6">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="rounded-lg border bg-card p-6 space-y-3">
<Skeleton className="h-6 w-32" />
<Skeleton className="h-20 w-full" />
</div>
))}
</div>
</div>
</div>
);
{/* Side cards */}
<div className="space-y-6">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="rounded-lg border bg-card p-6 space-y-3">
<Skeleton className="h-6 w-32" />
<Skeleton className="h-20 w-full" />
</div>
))}
</div>
</div>
</div>
);
}
+4 -4
View File
@@ -2,10 +2,10 @@ import { redirect } from "next/navigation";
/** Legacy route — redirects to /admin/users/show/[id] */
export default async function UserLegacyPage({
params,
params,
}: {
params: Promise<{ locale: string; id: string }>;
params: Promise<{ locale: string; id: string }>;
}) {
const { id } = await params;
redirect(`/admin/users/show/${id}`);
const { id } = await params;
redirect(`/admin/users/show/${id}`);
}
+110 -91
View File
@@ -4,107 +4,126 @@ import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface ChatMessage {
id: number;
roomId: number;
timestamp: number;
message: string;
id: number;
roomId: number;
timestamp: number;
message: string;
}
interface CommandEntry {
id: number;
timestamp: number;
command: string;
params: string;
succes: string;
id: number;
timestamp: number;
command: string;
params: string;
succes: string;
}
interface UserActivityLogProps {
chatLogs: ChatMessage[];
commandLogs: CommandEntry[];
chatLogs: ChatMessage[];
commandLogs: CommandEntry[];
}
function formatTime(ts: number) {
if (!ts) return "-";
const d = new Date(ts * 1000);
return d.toLocaleString(undefined, {
day: "2-digit",
month: "short",
hour: "2-digit",
minute: "2-digit",
});
if (!ts) return "-";
const d = new Date(ts * 1000);
return d.toLocaleString(undefined, {
day: "2-digit",
month: "short",
hour: "2-digit",
minute: "2-digit",
});
}
export function UserActivityLog({ chatLogs, commandLogs }: UserActivityLogProps) {
return (
<div className="grid gap-6 lg:grid-cols-2">
{/* Chat Messages */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MessageSquare className="h-4 w-4" />
Recent Messages ({chatLogs.length})
</CardTitle>
</CardHeader>
<CardContent>
{chatLogs.length === 0 ? (
<p className="text-sm text-muted-foreground">No recent messages.</p>
) : (
<div className="space-y-2 max-h-[400px] overflow-y-auto">
{chatLogs.map((log) => (
<div key={log.id} className="rounded-md border px-3 py-2 text-sm">
<div className="flex items-center justify-between gap-2 mb-1">
<Link
href={`/admin/rooms/${log.roomId}`}
className="text-xs text-primary hover:underline"
>
Room #{log.roomId}
</Link>
<span className="text-xs text-muted-foreground">{formatTime(log.timestamp)}</span>
</div>
<p className="break-words">{log.message}</p>
</div>
))}
</div>
)}
</CardContent>
</Card>
export function UserActivityLog({
chatLogs,
commandLogs,
}: UserActivityLogProps) {
return (
<div className="grid gap-6 lg:grid-cols-2">
{/* Chat Messages */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MessageSquare className="h-4 w-4" />
Recent Messages ({chatLogs.length})
</CardTitle>
</CardHeader>
<CardContent>
{chatLogs.length === 0 ? (
<p className="text-sm text-muted-foreground">No recent messages.</p>
) : (
<div className="space-y-2 max-h-[400px] overflow-y-auto">
{chatLogs.map((log) => (
<div
key={log.id}
className="rounded-md border px-3 py-2 text-sm"
>
<div className="flex items-center justify-between gap-2 mb-1">
<Link
href={`/admin/rooms/${log.roomId}`}
className="text-xs text-primary hover:underline"
>
Room #{log.roomId}
</Link>
<span className="text-xs text-muted-foreground">
{formatTime(log.timestamp)}
</span>
</div>
<p className="break-words">{log.message}</p>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Commands */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Terminal className="h-4 w-4" />
Recent Commands ({commandLogs.length})
</CardTitle>
</CardHeader>
<CardContent>
{commandLogs.length === 0 ? (
<p className="text-sm text-muted-foreground">No recent commands.</p>
) : (
<div className="space-y-2 max-h-[400px] overflow-y-auto">
{commandLogs.map((log) => (
<div key={log.id} className="rounded-md border px-3 py-2 text-sm">
<div className="flex items-center justify-between gap-2">
<code className="font-mono text-xs font-bold">:{log.command}</code>
<div className="flex items-center gap-2">
<Badge
variant={log.succes === "yes" ? "default" : "destructive"}
className="text-[10px]"
>
{log.succes === "yes" ? "OK" : "FAIL"}
</Badge>
<span className="text-xs text-muted-foreground">{formatTime(log.timestamp)}</span>
</div>
</div>
{log.params && (
<p className="text-xs text-muted-foreground mt-1 break-words">{log.params}</p>
)}
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
);
{/* Commands */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Terminal className="h-4 w-4" />
Recent Commands ({commandLogs.length})
</CardTitle>
</CardHeader>
<CardContent>
{commandLogs.length === 0 ? (
<p className="text-sm text-muted-foreground">No recent commands.</p>
) : (
<div className="space-y-2 max-h-[400px] overflow-y-auto">
{commandLogs.map((log) => (
<div
key={log.id}
className="rounded-md border px-3 py-2 text-sm"
>
<div className="flex items-center justify-between gap-2">
<code className="font-mono text-xs font-bold">
:{log.command}
</code>
<div className="flex items-center gap-2">
<Badge
variant={
log.succes === "yes" ? "default" : "destructive"
}
className="text-[10px]"
>
{log.succes === "yes" ? "OK" : "FAIL"}
</Badge>
<span className="text-xs text-muted-foreground">
{formatTime(log.timestamp)}
</span>
</div>
</div>
{log.params && (
<p className="text-xs text-muted-foreground mt-1 break-words">
{log.params}
</p>
)}
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
);
}
@@ -9,84 +9,86 @@ import { Input } from "@/components/ui/input";
import { useServerAction } from "@/hooks/use-server-action";
interface BadgeInfo {
id: number;
badgeCode: string;
id: number;
badgeCode: string;
}
interface UserBadgesSectionProps {
userId: number;
badges: BadgeInfo[];
userId: number;
badges: BadgeInfo[];
}
export function UserBadgesSection({ userId, badges }: UserBadgesSectionProps) {
const { isPending, run } = useServerAction();
const [newBadge, setNewBadge] = useState("");
const { isPending, run } = useServerAction();
const [newBadge, setNewBadge] = useState("");
function handleAddBadge() {
if (!newBadge.trim()) return;
run(() => giveBadge({ userId, badgeCode: newBadge.trim() }), {
successMessage: `Badge "${newBadge.trim()}" added.`,
onSuccess: () => setNewBadge(""),
});
}
function handleAddBadge() {
if (!newBadge.trim()) return;
run(() => giveBadge({ userId, badgeCode: newBadge.trim() }), {
successMessage: `Badge "${newBadge.trim()}" added.`,
onSuccess: () => setNewBadge(""),
});
}
function handleRemoveBadge(badgeCode: string) {
run(() => removeBadge({ userId, badgeCode }), {
successMessage: `Badge "${badgeCode}" removed.`,
});
}
function handleRemoveBadge(badgeCode: string) {
run(() => removeBadge({ userId, badgeCode }), {
successMessage: `Badge "${badgeCode}" removed.`,
});
}
return (
<Card>
<CardHeader>
<CardTitle>Badges ({badges.length})</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex gap-2">
<Input
value={newBadge}
onChange={(e) => setNewBadge(e.target.value)}
placeholder="Badge code..."
onKeyDown={(e) => e.key === "Enter" && handleAddBadge()}
/>
<Button size="icon" onClick={handleAddBadge} disabled={isPending}>
<Plus className="h-4 w-4" />
</Button>
</div>
return (
<Card>
<CardHeader>
<CardTitle>Badges ({badges.length})</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex gap-2">
<Input
value={newBadge}
onChange={(e) => setNewBadge(e.target.value)}
placeholder="Badge code..."
onKeyDown={(e) => e.key === "Enter" && handleAddBadge()}
/>
<Button size="icon" onClick={handleAddBadge} disabled={isPending}>
<Plus className="h-4 w-4" />
</Button>
</div>
<div className="flex flex-wrap gap-2">
{badges.map((badge) => (
<div
key={badge.id}
className="group flex items-center gap-1.5 rounded-md border bg-card px-2 py-1 text-sm hover:border-destructive/50 transition-colors"
>
<img
src={
process.env.NEXT_PUBLIC_BADGE_URL
? `${process.env.NEXT_PUBLIC_BADGE_URL}/${badge.badgeCode}.gif`
: `/swf/c_images/album1584/${badge.badgeCode}.gif`
}
alt={badge.badgeCode}
className="h-5 w-5"
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
}}
/>
<span>{badge.badgeCode}</span>
<button
type="button"
onClick={() => handleRemoveBadge(badge.badgeCode)}
disabled={isPending}
className="ml-0.5 rounded-full p-0.5 opacity-0 group-hover:opacity-100 hover:bg-destructive/10 text-destructive transition-opacity disabled:opacity-50"
title={`Remove ${badge.badgeCode}`}
>
<X className="h-3 w-3" />
</button>
</div>
))}
{badges.length === 0 && <p className="text-sm text-muted-foreground">No badges</p>}
</div>
</CardContent>
</Card>
);
<div className="flex flex-wrap gap-2">
{badges.map((badge) => (
<div
key={badge.id}
className="group flex items-center gap-1.5 rounded-md border bg-card px-2 py-1 text-sm hover:border-destructive/50 transition-colors"
>
<img
src={
process.env.NEXT_PUBLIC_BADGE_URL
? `${process.env.NEXT_PUBLIC_BADGE_URL}/${badge.badgeCode}.gif`
: `/swf/c_images/album1584/${badge.badgeCode}.gif`
}
alt={badge.badgeCode}
className="h-5 w-5"
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
}}
/>
<span>{badge.badgeCode}</span>
<button
type="button"
onClick={() => handleRemoveBadge(badge.badgeCode)}
disabled={isPending}
className="ml-0.5 rounded-full p-0.5 opacity-0 group-hover:opacity-100 hover:bg-destructive/10 text-destructive transition-opacity disabled:opacity-50"
title={`Remove ${badge.badgeCode}`}
>
<X className="h-3 w-3" />
</button>
</div>
))}
{badges.length === 0 && (
<p className="text-sm text-muted-foreground">No badges</p>
)}
</div>
</CardContent>
</Card>
);
}
+160 -134
View File
@@ -8,153 +8,179 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { useServerAction } from "@/hooks/use-server-action";
interface BanInfo {
id: number;
reason: string;
type: string;
staffName: string;
timestamp: number;
banExpire: number;
id: number;
reason: string;
type: string;
staffName: string;
timestamp: number;
banExpire: number;
}
interface UserBanSectionProps {
userId: number;
username: string;
bans: BanInfo[];
userId: number;
username: string;
bans: BanInfo[];
}
export function UserBanSection({ userId, username, bans }: UserBanSectionProps) {
const { isPending, run } = useServerAction();
const [showBanForm, setShowBanForm] = useState(false);
const [reason, setReason] = useState("");
const [duration, setDuration] = useState("24");
const [banType, setBanType] = useState("account");
export function UserBanSection({
userId,
username,
bans,
}: UserBanSectionProps) {
const { isPending, run } = useServerAction();
const [showBanForm, setShowBanForm] = useState(false);
const [reason, setReason] = useState("");
const [duration, setDuration] = useState("24");
const [banType, setBanType] = useState("account");
const activeBan = bans.find((b) => {
const now = Math.floor(Date.now() / 1000);
return b.banExpire === 0 || b.banExpire > now;
});
const activeBan = bans.find((b) => {
const now = Math.floor(Date.now() / 1000);
return b.banExpire === 0 || b.banExpire > now;
});
function handleBan() {
run(
() =>
banUser({
userId,
reason,
duration: parseInt(duration, 10),
type: banType as "account" | "ip" | "machine",
}),
{
successMessage: `${username} has been banned.`,
onSuccess: () => {
setShowBanForm(false);
setReason("");
},
},
);
}
function handleBan() {
run(
() =>
banUser({
userId,
reason,
duration: parseInt(duration, 10),
type: banType as "account" | "ip" | "machine",
}),
{
successMessage: `${username} has been banned.`,
onSuccess: () => {
setShowBanForm(false);
setReason("");
},
},
);
}
function handleUnban() {
run(() => unbanUser({ userId }), {
successMessage: `${username} has been unbanned.`,
});
}
function handleUnban() {
run(() => unbanUser({ userId }), {
successMessage: `${username} has been unbanned.`,
});
}
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Bans</CardTitle>
<div className="flex gap-2">
{activeBan ? (
<Button variant="outline" size="sm" onClick={handleUnban} disabled={isPending}>
<ShieldOff className="mr-2 h-4 w-4" />
Unban
</Button>
) : (
<Button variant="destructive" size="sm" onClick={() => setShowBanForm(!showBanForm)}>
<Ban className="mr-2 h-4 w-4" />
Ban
</Button>
)}
</div>
</CardHeader>
<CardContent className="space-y-4">
{activeBan && (
<div className="rounded-md border border-destructive/50 bg-destructive/10 p-3">
<p className="text-sm font-medium text-destructive">Active Ban</p>
<p className="text-sm">{activeBan.reason}</p>
<p className="text-xs text-muted-foreground">
By {activeBan.staffName} &middot;{" "}
{activeBan.banExpire === 0
? "Permanent"
: `Expires ${new Date(activeBan.banExpire * 1000).toLocaleString()}`}
</p>
</div>
)}
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Bans</CardTitle>
<div className="flex gap-2">
{activeBan ? (
<Button
variant="outline"
size="sm"
onClick={handleUnban}
disabled={isPending}
>
<ShieldOff className="mr-2 h-4 w-4" />
Unban
</Button>
) : (
<Button
variant="destructive"
size="sm"
onClick={() => setShowBanForm(!showBanForm)}
>
<Ban className="mr-2 h-4 w-4" />
Ban
</Button>
)}
</div>
</CardHeader>
<CardContent className="space-y-4">
{activeBan && (
<div className="rounded-md border border-destructive/50 bg-destructive/10 p-3">
<p className="text-sm font-medium text-destructive">Active Ban</p>
<p className="text-sm">{activeBan.reason}</p>
<p className="text-xs text-muted-foreground">
By {activeBan.staffName} &middot;{" "}
{activeBan.banExpire === 0
? "Permanent"
: `Expires ${new Date(activeBan.banExpire * 1000).toLocaleString()}`}
</p>
</div>
)}
{showBanForm && !activeBan && (
<div className="space-y-3 rounded-md border p-3">
<div className="space-y-2">
<Label>Reason</Label>
<Textarea
value={reason}
onChange={(e) => setReason(e.target.value)}
placeholder="Ban reason..."
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label>Duration (hours)</Label>
<Input
type="number"
value={duration}
onChange={(e) => setDuration(e.target.value)}
placeholder="0 = permanent"
/>
</div>
<div className="space-y-2">
<Label>Type</Label>
<Select value={banType} onValueChange={setBanType}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="account">Account</SelectItem>
<SelectItem value="ip">IP</SelectItem>
<SelectItem value="machine">Machine</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<Button variant="destructive" onClick={handleBan} disabled={isPending || !reason}>
{isPending ? "Banning..." : "Confirm Ban"}
</Button>
</div>
)}
{showBanForm && !activeBan && (
<div className="space-y-3 rounded-md border p-3">
<div className="space-y-2">
<Label>Reason</Label>
<Textarea
value={reason}
onChange={(e) => setReason(e.target.value)}
placeholder="Ban reason..."
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label>Duration (hours)</Label>
<Input
type="number"
value={duration}
onChange={(e) => setDuration(e.target.value)}
placeholder="0 = permanent"
/>
</div>
<div className="space-y-2">
<Label>Type</Label>
<Select value={banType} onValueChange={setBanType}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="account">Account</SelectItem>
<SelectItem value="ip">IP</SelectItem>
<SelectItem value="machine">Machine</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<Button
variant="destructive"
onClick={handleBan}
disabled={isPending || !reason}
>
{isPending ? "Banning..." : "Confirm Ban"}
</Button>
</div>
)}
{bans.length > 0 && (
<div className="space-y-2">
<p className="text-sm font-medium">History</p>
{bans.map((ban) => (
<div key={ban.id} className="flex items-center justify-between text-sm">
<div>
<span>{ban.reason}</span>
<span className="ml-2">
<Badge variant="outline">{ban.type}</Badge>
</span>
</div>
<span className="text-xs text-muted-foreground">
{new Date(ban.timestamp * 1000).toLocaleDateString()}
</span>
</div>
))}
</div>
)}
</CardContent>
</Card>
);
{bans.length > 0 && (
<div className="space-y-2">
<p className="text-sm font-medium">History</p>
{bans.map((ban) => (
<div
key={ban.id}
className="flex items-center justify-between text-sm"
>
<div>
<span>{ban.reason}</span>
<span className="ml-2">
<Badge variant="outline">{ban.type}</Badge>
</span>
</div>
<span className="text-xs text-muted-foreground">
{new Date(ban.timestamp * 1000).toLocaleDateString()}
</span>
</div>
))}
</div>
)}
</CardContent>
</Card>
);
}
+144 -114
View File
@@ -7,135 +7,165 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useServerAction } from "@/hooks/use-server-action";
import { type UpdateUserInput, updateUserSchema } from "@/lib/validators/user";
interface UserEditFormProps {
user: {
id: number;
username: string;
mail: string;
rank: number;
motto: string;
credits: number;
pixels: number;
diamonds: number;
duckets: number;
};
/** Ranks loaded from the `permissions` table (id + human-readable name). */
ranks?: Array<{ id: number; name: string }>;
user: {
id: number;
username: string;
mail: string;
rank: number;
motto: string;
credits: number;
pixels: number;
diamonds: number;
duckets: number;
};
/** Ranks loaded from the `permissions` table (id + human-readable name). */
ranks?: Array<{ id: number; name: string }>;
}
// Fallback used if the page does not supply the DB-loaded rank list.
const FALLBACK_RANKS: Array<{ id: number; name: string }> = [
{ id: 1, name: "User" },
{ id: 2, name: "Bronze" },
{ id: 3, name: "Silver" },
{ id: 4, name: "Gold" },
{ id: 5, name: "Moderator" },
{ id: 6, name: "Admin" },
{ id: 7, name: "Super Admin" },
{ id: 1, name: "User" },
{ id: 2, name: "Bronze" },
{ id: 3, name: "Silver" },
{ id: 4, name: "Gold" },
{ id: 5, name: "Moderator" },
{ id: 6, name: "Admin" },
{ id: 7, name: "Super Admin" },
];
export function UserEditForm({ user, ranks }: UserEditFormProps) {
const rankOptions = ranks && ranks.length > 0 ? ranks : FALLBACK_RANKS;
const { isPending, run } = useServerAction();
const rankOptions = ranks && ranks.length > 0 ? ranks : FALLBACK_RANKS;
const { isPending, run } = useServerAction();
const {
register,
handleSubmit,
setValue,
formState: { errors },
} = useForm<UpdateUserInput>({
resolver: zodResolver(updateUserSchema),
defaultValues: {
username: user.username,
mail: user.mail,
rank: user.rank,
motto: user.motto,
credits: user.credits,
pixels: user.pixels,
diamonds: user.diamonds,
duckets: user.duckets,
},
});
const {
register,
handleSubmit,
setValue,
formState: { errors },
} = useForm<UpdateUserInput>({
resolver: zodResolver(updateUserSchema),
defaultValues: {
username: user.username,
mail: user.mail,
rank: user.rank,
motto: user.motto,
credits: user.credits,
pixels: user.pixels,
diamonds: user.diamonds,
duckets: user.duckets,
},
});
function onSubmit(data: UpdateUserInput) {
run(() => updateUser({ id: user.id, ...data }), {
successMessage: "User updated successfully.",
});
}
function onSubmit(data: UpdateUserInput) {
run(() => updateUser({ id: user.id, ...data }), {
successMessage: "User updated successfully.",
});
}
return (
<Card>
<CardHeader>
<CardTitle>Edit User</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="username">Username</Label>
<Input id="username" {...register("username")} />
{errors.username && <p className="text-sm text-destructive">{errors.username.message}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="mail">Email</Label>
<Input id="mail" type="email" {...register("mail")} />
{errors.mail && <p className="text-sm text-destructive">{errors.mail.message}</p>}
</div>
</div>
return (
<Card>
<CardHeader>
<CardTitle>Edit User</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="username">Username</Label>
<Input id="username" {...register("username")} />
{errors.username && (
<p className="text-sm text-destructive">
{errors.username.message}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="mail">Email</Label>
<Input id="mail" type="email" {...register("mail")} />
{errors.mail && (
<p className="text-sm text-destructive">
{errors.mail.message}
</p>
)}
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="rank">Rank</Label>
<Select
defaultValue={String(user.rank)}
onValueChange={(v) => setValue("rank", parseInt(v, 10))}
>
<SelectTrigger>
<SelectValue placeholder="Select rank" />
</SelectTrigger>
<SelectContent>
{rankOptions.map((r) => (
<SelectItem key={r.id} value={String(r.id)}>
{r.id} — {r.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="motto">Motto</Label>
<Input id="motto" {...register("motto")} />
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="rank">Rank</Label>
<Select
defaultValue={String(user.rank)}
onValueChange={(v) => setValue("rank", parseInt(v, 10))}
>
<SelectTrigger>
<SelectValue placeholder="Select rank" />
</SelectTrigger>
<SelectContent>
{rankOptions.map((r) => (
<SelectItem key={r.id} value={String(r.id)}>
{r.id} — {r.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="motto">Motto</Label>
<Input id="motto" {...register("motto")} />
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div className="space-y-2">
<Label htmlFor="credits">Credits</Label>
<Input id="credits" type="number" {...register("credits", { valueAsNumber: true })} />
</div>
<div className="space-y-2">
<Label htmlFor="pixels">Pixels</Label>
<Input id="pixels" type="number" {...register("pixels", { valueAsNumber: true })} />
</div>
<div className="space-y-2">
<Label htmlFor="diamonds">Diamonds</Label>
<Input id="diamonds" type="number" {...register("diamonds", { valueAsNumber: true })} />
</div>
<div className="space-y-2">
<Label htmlFor="duckets">Duckets</Label>
<Input id="duckets" type="number" {...register("duckets", { valueAsNumber: true })} />
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div className="space-y-2">
<Label htmlFor="credits">Credits</Label>
<Input
id="credits"
type="number"
{...register("credits", { valueAsNumber: true })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="pixels">Pixels</Label>
<Input
id="pixels"
type="number"
{...register("pixels", { valueAsNumber: true })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="diamonds">Diamonds</Label>
<Input
id="diamonds"
type="number"
{...register("diamonds", { valueAsNumber: true })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="duckets">Duckets</Label>
<Input
id="duckets"
type="number"
{...register("duckets", { valueAsNumber: true })}
/>
</div>
</div>
<Button type="submit" disabled={isPending}>
{isPending ? "Saving..." : "Save Changes"}
</Button>
</form>
</CardContent>
</Card>
);
<Button type="submit" disabled={isPending}>
{isPending ? "Saving..." : "Save Changes"}
</Button>
</form>
</CardContent>
</Card>
);
}
+235 -176
View File
@@ -1,206 +1,265 @@
"use client";
import { Check, Coins, Copy, KeyRound, MessageCircle, Unplug, Volume2, VolumeX } from "lucide-react";
import {
Check,
Coins,
Copy,
KeyRound,
MessageCircle,
Unplug,
Volume2,
VolumeX,
} from "lucide-react";
import { useState } from "react";
import { alertUser, disconnectUser, muteUser, resetPassword, sendCredits, unmuteUser } from "@/actions/users";
import {
alertUser,
disconnectUser,
muteUser,
resetPassword,
sendCredits,
unmuteUser,
} from "@/actions/users";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useServerAction } from "@/hooks/use-server-action";
interface UserQuickActionsProps {
userId: number;
username: string;
isOnline: boolean;
userId: number;
username: string;
isOnline: boolean;
}
export function UserQuickActions({ userId, username, isOnline }: UserQuickActionsProps) {
const { run, isPending } = useServerAction();
export function UserQuickActions({
userId,
username,
isOnline,
}: UserQuickActionsProps) {
const { run, isPending } = useServerAction();
// Reset password state
const [newPassword, setNewPassword] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
// Reset password state
const [newPassword, setNewPassword] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
// Alert state
const [alertMessage, setAlertMessage] = useState("");
// Alert state
const [alertMessage, setAlertMessage] = useState("");
// Mute state
const [muteDuration, setMuteDuration] = useState("10");
// Mute state
const [muteDuration, setMuteDuration] = useState("10");
// Credits state
const [creditAmount, setCreditAmount] = useState("1000");
// Credits state
const [creditAmount, setCreditAmount] = useState("1000");
function handleResetPassword() {
if (!confirm(`Reset password for ${username}? A new random password will be generated.`)) return;
run(() => resetPassword({ userId }), {
successMessage: "Password reset successfully.",
onSuccess: (data) => {
setNewPassword(data.newPassword as string);
},
});
}
function handleResetPassword() {
if (
!confirm(
`Reset password for ${username}? A new random password will be generated.`,
)
)
return;
run(() => resetPassword({ userId }), {
successMessage: "Password reset successfully.",
onSuccess: (data) => {
setNewPassword(data.newPassword as string);
},
});
}
function handleDisconnect() {
run(() => disconnectUser({ userId }), {
successMessage: `${username} disconnected.`,
});
}
function handleDisconnect() {
run(() => disconnectUser({ userId }), {
successMessage: `${username} disconnected.`,
});
}
function handleAlert() {
if (!alertMessage.trim()) return;
run(() => alertUser({ userId, message: alertMessage.trim() }), {
successMessage: "Alert sent.",
onSuccess: () => setAlertMessage(""),
});
}
function handleAlert() {
if (!alertMessage.trim()) return;
run(() => alertUser({ userId, message: alertMessage.trim() }), {
successMessage: "Alert sent.",
onSuccess: () => setAlertMessage(""),
});
}
function handleMute() {
const duration = Number(muteDuration);
if (Number.isNaN(duration) || duration < 0) return;
run(() => muteUser({ userId, duration }), {
successMessage: `${username} muted.`,
});
}
function handleMute() {
const duration = Number(muteDuration);
if (Number.isNaN(duration) || duration < 0) return;
run(() => muteUser({ userId, duration }), {
successMessage: `${username} muted.`,
});
}
function handleUnmute() {
run(() => unmuteUser({ userId }), {
successMessage: `${username} unmuted.`,
});
}
function handleUnmute() {
run(() => unmuteUser({ userId }), {
successMessage: `${username} unmuted.`,
});
}
function handleSendCredits() {
const amount = parseInt(creditAmount, 10);
if (!amount || amount <= 0) return;
run(() => sendCredits({ userId, amount }), {
successMessage: `${amount.toLocaleString()} credits sent to ${username}.`,
});
}
function handleSendCredits() {
const amount = parseInt(creditAmount, 10);
if (!amount || amount <= 0) return;
run(() => sendCredits({ userId, amount }), {
successMessage: `${amount.toLocaleString()} credits sent to ${username}.`,
});
}
function copyPassword() {
if (newPassword) {
navigator.clipboard.writeText(newPassword);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
}
function copyPassword() {
if (newPassword) {
navigator.clipboard.writeText(newPassword);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
}
return (
<>
<Card>
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Row 1: Password + Disconnect */}
<div className="grid grid-cols-2 gap-3">
<Button
variant="outline"
onClick={handleResetPassword}
disabled={isPending}
className="justify-start"
>
<KeyRound className="mr-2 h-4 w-4" />
Reset Password
</Button>
<Button
variant="outline"
onClick={handleDisconnect}
disabled={isPending || !isOnline}
className="justify-start"
>
<Unplug className="mr-2 h-4 w-4" />
Disconnect
{isOnline && (
<Badge variant="default" className="ml-auto text-[10px] bg-[var(--admin-success-subtle)]">
Online
</Badge>
)}
</Button>
</div>
return (
<>
<Card>
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Row 1: Password + Disconnect */}
<div className="grid grid-cols-2 gap-3">
<Button
variant="outline"
onClick={handleResetPassword}
disabled={isPending}
className="justify-start"
>
<KeyRound className="mr-2 h-4 w-4" />
Reset Password
</Button>
<Button
variant="outline"
onClick={handleDisconnect}
disabled={isPending || !isOnline}
className="justify-start"
>
<Unplug className="mr-2 h-4 w-4" />
Disconnect
{isOnline && (
<Badge
variant="default"
className="ml-auto text-[10px] bg-[var(--admin-success-subtle)]"
>
Online
</Badge>
)}
</Button>
</div>
{/* Send Alert */}
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Send In-Game Alert</Label>
<div className="flex gap-2">
<Input
placeholder="Message to send..."
value={alertMessage}
onChange={(e) => setAlertMessage(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleAlert()}
className="flex-1"
/>
<Button onClick={handleAlert} disabled={isPending || !alertMessage.trim()} size="icon">
<MessageCircle className="h-4 w-4" />
</Button>
</div>
</div>
{/* Send Alert */}
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">
Send In-Game Alert
</Label>
<div className="flex gap-2">
<Input
placeholder="Message to send..."
value={alertMessage}
onChange={(e) => setAlertMessage(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleAlert()}
className="flex-1"
/>
<Button
onClick={handleAlert}
disabled={isPending || !alertMessage.trim()}
size="icon"
>
<MessageCircle className="h-4 w-4" />
</Button>
</div>
</div>
{/* Mute */}
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Mute / Unmute</Label>
<div className="flex gap-2">
<Input
type="number"
placeholder="Minutes (0 = permanent)"
value={muteDuration}
onChange={(e) => setMuteDuration(e.target.value)}
className="w-32"
/>
<Button variant="outline" onClick={handleMute} disabled={isPending} size="sm">
<VolumeX className="mr-1.5 h-3.5 w-3.5" /> Mute
</Button>
<Button variant="outline" onClick={handleUnmute} disabled={isPending} size="sm">
<Volume2 className="mr-1.5 h-3.5 w-3.5" /> Unmute
</Button>
</div>
</div>
{/* Mute */}
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">
Mute / Unmute
</Label>
<div className="flex gap-2">
<Input
type="number"
placeholder="Minutes (0 = permanent)"
value={muteDuration}
onChange={(e) => setMuteDuration(e.target.value)}
className="w-32"
/>
<Button
variant="outline"
onClick={handleMute}
disabled={isPending}
size="sm"
>
<VolumeX className="mr-1.5 h-3.5 w-3.5" /> Mute
</Button>
<Button
variant="outline"
onClick={handleUnmute}
disabled={isPending}
size="sm"
>
<Volume2 className="mr-1.5 h-3.5 w-3.5" /> Unmute
</Button>
</div>
</div>
{/* Send Credits */}
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Send Credits (RCON)</Label>
<div className="flex gap-2">
<Input
type="number"
value={creditAmount}
onChange={(e) => setCreditAmount(e.target.value)}
className="w-32"
min={1}
/>
<Button variant="outline" onClick={handleSendCredits} disabled={isPending} size="sm">
<Coins className="mr-1.5 h-3.5 w-3.5" /> Send Credits
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Send Credits */}
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">
Send Credits (RCON)
</Label>
<div className="flex gap-2">
<Input
type="number"
value={creditAmount}
onChange={(e) => setCreditAmount(e.target.value)}
className="w-32"
min={1}
/>
<Button
variant="outline"
onClick={handleSendCredits}
disabled={isPending}
size="sm"
>
<Coins className="mr-1.5 h-3.5 w-3.5" /> Send Credits
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Password Reset Dialog */}
<Dialog open={!!newPassword} onOpenChange={() => setNewPassword(null)}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle>Password Reset</DialogTitle>
<DialogDescription>
New password generated for <strong>{username}</strong>. Copy it now — it won&apos;t be shown
again.
</DialogDescription>
</DialogHeader>
<div className="flex items-center gap-2 rounded-lg border bg-muted p-3">
<code className="flex-1 text-lg font-mono font-bold tracking-wider select-all">
{newPassword}
</code>
<Button variant="ghost" size="icon" onClick={copyPassword}>
{copied ? (
<Check className="h-4 w-4 text-[var(--admin-success)]" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
</DialogContent>
</Dialog>
</>
);
{/* Password Reset Dialog */}
<Dialog open={!!newPassword} onOpenChange={() => setNewPassword(null)}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle>Password Reset</DialogTitle>
<DialogDescription>
New password generated for <strong>{username}</strong>. Copy it
now — it won&apos;t be shown again.
</DialogDescription>
</DialogHeader>
<div className="flex items-center gap-2 rounded-lg border bg-muted p-3">
<code className="flex-1 text-lg font-mono font-bold tracking-wider select-all">
{newPassword}
</code>
<Button variant="ghost" size="icon" onClick={copyPassword}>
{copied ? (
<Check className="h-4 w-4 text-[var(--admin-success)]" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
</DialogContent>
</Dialog>
</>
);
}
+60 -50
View File
@@ -4,62 +4,72 @@ import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface UserRoom {
id: number;
name: string;
state: string;
users: number;
usersMax: number;
id: number;
name: string;
state: string;
users: number;
usersMax: number;
}
interface UserRoomsProps {
rooms: UserRoom[];
rooms: UserRoom[];
}
const stateColors: Record<string, "default" | "secondary" | "outline" | "destructive"> = {
open: "default",
locked: "secondary",
password: "destructive",
invisible: "outline",
const stateColors: Record<
string,
"default" | "secondary" | "outline" | "destructive"
> = {
open: "default",
locked: "secondary",
password: "destructive",
invisible: "outline",
};
export function UserRooms({ rooms }: UserRoomsProps) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<DoorOpen className="h-4 w-4" />
Rooms ({rooms.length})
</CardTitle>
</CardHeader>
<CardContent>
{rooms.length === 0 ? (
<p className="text-sm text-muted-foreground">This user doesn&apos;t own any rooms.</p>
) : (
<div className="grid gap-2 sm:grid-cols-2">
{rooms.map((room) => (
<Link
key={room.id}
href={`/admin/rooms/${room.id}`}
className="flex items-center gap-3 rounded-lg border px-3 py-2.5 hover:bg-muted/50 transition-colors"
>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{room.name}</p>
<div className="flex items-center gap-2 mt-0.5">
<Badge variant={stateColors[room.state] || "outline"} className="text-[10px]">
{room.state}
</Badge>
<span className="text-xs text-muted-foreground flex items-center gap-1">
<Users className="h-3 w-3" />
{room.users}/{room.usersMax}
</span>
</div>
</div>
<span className="text-xs font-mono text-muted-foreground">#{room.id}</span>
</Link>
))}
</div>
)}
</CardContent>
</Card>
);
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<DoorOpen className="h-4 w-4" />
Rooms ({rooms.length})
</CardTitle>
</CardHeader>
<CardContent>
{rooms.length === 0 ? (
<p className="text-sm text-muted-foreground">
This user doesn&apos;t own any rooms.
</p>
) : (
<div className="grid gap-2 sm:grid-cols-2">
{rooms.map((room) => (
<Link
key={room.id}
href={`/admin/rooms/${room.id}`}
className="flex items-center gap-3 rounded-lg border px-3 py-2.5 hover:bg-muted/50 transition-colors"
>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{room.name}</p>
<div className="flex items-center gap-2 mt-0.5">
<Badge
variant={stateColors[room.state] || "outline"}
className="text-[10px]"
>
{room.state}
</Badge>
<span className="text-xs text-muted-foreground flex items-center gap-1">
<Users className="h-3 w-3" />
{room.users}/{room.usersMax}
</span>
</div>
</div>
<span className="text-xs font-mono text-muted-foreground">
#{room.id}
</span>
</Link>
))}
</div>
)}
</CardContent>
</Card>
);
}