- Add 11 missing server action files: badges, bulk-users, catalog, catalog-bc, catalog-items, import-badges, import-furni, multi-account-detect, permissions, rooms, soundtracks - Add missing admin navigation links: tickets, sounds, translations, import, radio sub-pages - Add translation keys for all new navigation items
203 lines
7.2 KiB
TypeScript
203 lines
7.2 KiB
TypeScript
import {
|
|
ArrowLeftRight,
|
|
Coins,
|
|
Diamond,
|
|
DoorOpen,
|
|
Eye,
|
|
Gem,
|
|
MessageSquare,
|
|
Pencil,
|
|
Shield,
|
|
Sparkles,
|
|
Wifi,
|
|
WifiOff,
|
|
} from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { AdminBreadcrumb } from '@/components/admin/admin-breadcrumb'
|
|
import { UserWatchToggle } from '@/components/admin/user-watch-toggle'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Button } from '@/components/ui/button'
|
|
import { getAvatarUrl } from '@/lib/imager'
|
|
|
|
interface UserHeroProps {
|
|
user: {
|
|
id: number
|
|
username: string
|
|
motto: string
|
|
look: string
|
|
rank: number
|
|
credits: number
|
|
pixels: number
|
|
online: string
|
|
permission: { rankName: string }
|
|
twoFactorConfirmed: number
|
|
}
|
|
diamonds: number
|
|
duckets: number
|
|
roomCount: number
|
|
chatCount: number
|
|
tradeCount: number
|
|
isOnline: boolean
|
|
activeBan: boolean
|
|
mode: 'show' | 'edit'
|
|
canEdit: boolean
|
|
/** When non-null, render the watch toggle with this initial state. */
|
|
watching?: boolean | null
|
|
}
|
|
|
|
export function UserHero({
|
|
user,
|
|
diamonds,
|
|
duckets,
|
|
roomCount,
|
|
chatCount,
|
|
tradeCount,
|
|
isOnline,
|
|
activeBan,
|
|
mode,
|
|
canEdit,
|
|
watching,
|
|
}: UserHeroProps) {
|
|
return (
|
|
<>
|
|
<AdminBreadcrumb
|
|
items={[
|
|
{ label: 'Users', href: '/admin/users' },
|
|
{ label: user.username },
|
|
{ label: mode === 'edit' ? 'Edit' : 'View' },
|
|
]}
|
|
/>
|
|
|
|
<div className="relative rounded-xl border bg-gradient-to-r from-card to-muted/30 p-6">
|
|
<div className="flex flex-col sm:flex-row items-start gap-5">
|
|
{/* Avatar */}
|
|
<div className="relative shrink-0">
|
|
<img
|
|
src={getAvatarUrl(user.look, { headDirection: 2, direction: 2 })}
|
|
alt={user.username}
|
|
className="h-28 rounded-lg border bg-card shadow-sm"
|
|
style={{ imageRendering: 'pixelated' }}
|
|
/>
|
|
<span
|
|
className={`absolute -top-1 -right-1 flex h-4 w-4 ${isOnline ? '' : 'opacity-50'}`}
|
|
>
|
|
{isOnline && (
|
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
|
)}
|
|
<span
|
|
className={`relative inline-flex rounded-full h-4 w-4 border-2 border-card ${isOnline ? 'bg-green-500' : 'bg-zinc-400'}`}
|
|
/>
|
|
</span>
|
|
</div>
|
|
|
|
{/* Info */}
|
|
<div className="flex-1 min-w-0 space-y-2">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<h1 className="text-3xl font-bold tracking-tight">{user.username}</h1>
|
|
<Badge variant="secondary" className="text-xs">
|
|
ID: {user.id}
|
|
</Badge>
|
|
{/* Mode switch buttons */}
|
|
{mode === 'show' && canEdit && (
|
|
<Button variant="outline" size="sm" asChild className="ml-2">
|
|
<Link href={`/admin/users/edit/${user.id}`}>
|
|
<Pencil className="mr-1.5 h-3.5 w-3.5" /> Edit
|
|
</Link>
|
|
</Button>
|
|
)}
|
|
{mode === 'edit' && (
|
|
<Button variant="outline" size="sm" asChild className="ml-2">
|
|
<Link href={`/admin/users/show/${user.id}`}>
|
|
<Eye className="mr-1.5 h-3.5 w-3.5" /> View
|
|
</Link>
|
|
</Button>
|
|
)}
|
|
{watching != null && (
|
|
<UserWatchToggle
|
|
targetUserId={user.id}
|
|
username={user.username}
|
|
initialWatching={watching}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{user.motto && (
|
|
<p className="text-muted-foreground italic">“{user.motto}”</p>
|
|
)}
|
|
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<Badge>{user.permission.rankName || `Rank ${user.rank}`}</Badge>
|
|
<Badge
|
|
variant={isOnline ? 'default' : 'outline'}
|
|
className={isOnline ? 'bg-green-600' : ''}
|
|
>
|
|
{isOnline ? (
|
|
<>
|
|
<Wifi className="mr-1 h-3 w-3" /> Online
|
|
</>
|
|
) : (
|
|
<>
|
|
<WifiOff className="mr-1 h-3 w-3" /> Offline
|
|
</>
|
|
)}
|
|
</Badge>
|
|
{activeBan && <Badge variant="destructive">Banned</Badge>}
|
|
{user.twoFactorConfirmed ? (
|
|
<Badge variant="outline" className="border-green-500/50 text-green-600">
|
|
<Shield className="mr-1 h-3 w-3" /> 2FA
|
|
</Badge>
|
|
) : null}
|
|
</div>
|
|
|
|
{/* Currency row */}
|
|
<div className="flex flex-wrap gap-3 pt-1">
|
|
<div className="flex items-center gap-1.5 text-sm">
|
|
<Coins className="h-4 w-4 text-yellow-500" />
|
|
<span className="font-semibold">{user.credits.toLocaleString()}</span>
|
|
<span className="text-muted-foreground text-xs">credits</span>
|
|
</div>
|
|
<div className="flex items-center gap-1.5 text-sm">
|
|
<Diamond className="h-4 w-4 text-blue-400" />
|
|
<span className="font-semibold">{diamonds.toLocaleString()}</span>
|
|
<span className="text-muted-foreground text-xs">diamonds</span>
|
|
</div>
|
|
<div className="flex items-center gap-1.5 text-sm">
|
|
<Sparkles className="h-4 w-4 text-purple-400" />
|
|
<span className="font-semibold">{duckets.toLocaleString()}</span>
|
|
<span className="text-muted-foreground text-xs">duckets</span>
|
|
</div>
|
|
<div className="flex items-center gap-1.5 text-sm">
|
|
<Gem className="h-4 w-4 text-pink-400" />
|
|
<span className="font-semibold">{user.pixels.toLocaleString()}</span>
|
|
<span className="text-muted-foreground text-xs">pixels</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick stats cards */}
|
|
<div className="grid grid-cols-3 gap-2 shrink-0">
|
|
<Link
|
|
href={`/admin/rooms?search=${user.username}`}
|
|
className="rounded-lg border bg-card px-3 py-2 text-center hover:shadow-md transition-shadow"
|
|
>
|
|
<DoorOpen className="h-4 w-4 mx-auto text-muted-foreground" />
|
|
<p className="text-lg font-bold">{roomCount}</p>
|
|
<p className="text-[10px] text-muted-foreground">Rooms</p>
|
|
</Link>
|
|
<div className="rounded-lg border bg-card px-3 py-2 text-center">
|
|
<MessageSquare className="h-4 w-4 mx-auto text-muted-foreground" />
|
|
<p className="text-lg font-bold">{chatCount.toLocaleString()}</p>
|
|
<p className="text-[10px] text-muted-foreground">Messages</p>
|
|
</div>
|
|
<div className="rounded-lg border bg-card px-3 py-2 text-center">
|
|
<ArrowLeftRight className="h-4 w-4 mx-auto text-muted-foreground" />
|
|
<p className="text-lg font-bold">{tradeCount}</p>
|
|
<p className="text-[10px] text-muted-foreground">Trades</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|