'use client' import { ArrowLeft, ChevronDown, Crown, Loader2, Save, Search, Settings, Shield, ShieldCheck, Terminal, UserCheck, } from 'lucide-react' import Link from 'next/link' import { useMemo, useState } from 'react' import { toast } from 'sonner' import { saveRank as saveRankAction, setCmsPermissions } from '@/actions/permissions' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Checkbox } from '@/components/ui/checkbox' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Switch } from '@/components/ui/switch' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { useServerAction } from '@/hooks/use-server-action' import { getAvatarUrl } from '@/lib/imager' import { cn } from '@/lib/utils' /* ─── Types ──────────────────────────────────────────────── */ interface RankData { id: number rank_name: string badge: string level: number prefix: string prefix_color: string hidden_rank: string log_commands: string room_effect: number auto_credits_amount: number auto_pixels_amount: number auto_gotw_amount: number auto_points_amount: number permissions: Record permissionMaxValues: Record } interface RankUser { id: number username: string look: string } interface CmsRoleData { id: number slug: string title: string permissionSlugs: string[] } interface CmsPermission { id: number slug: string title: string } interface Props { rank: RankData users: RankUser[] totalUsers: number cmsRole: CmsRoleData | null allCmsPermissions: CmsPermission[] } /* ─── Helpers ────────────────────────────────────────────── */ function categorizePerms(keys: string[]) { const commands: string[] = [] const account: string[] = [] const other: string[] = [] for (const key of keys.sort()) { if (key.startsWith('cmd_') || key === 'kiss_cmd') commands.push(key) else if (key.startsWith('acc_')) account.push(key) else other.push(key) } return { commands, account, other } } /* ─── Tri-state Toggle ───────────────────────────────────── */ function PermToggle({ value, maxValue, onChange, }: { value: string maxValue: number onChange: (v: string) => void }) { const values = Array.from({ length: Math.max(1, maxValue) + 1 }, (_, i) => String(i)) return (
{values.map((v) => ( ))}
) } /* ─── Main Component ─────────────────────────────────────── */ export function RankEditClient({ rank: initialRank, users, totalUsers, cmsRole: initialCmsRole, allCmsPermissions, }: Props) { const { run, isPending } = useServerAction() const [general, setGeneral] = useState({ rank_name: initialRank.rank_name, badge: initialRank.badge, prefix: initialRank.prefix, prefix_color: initialRank.prefix_color, hidden_rank: initialRank.hidden_rank, log_commands: initialRank.log_commands, room_effect: initialRank.room_effect, auto_credits_amount: initialRank.auto_credits_amount, auto_pixels_amount: initialRank.auto_pixels_amount, auto_gotw_amount: initialRank.auto_gotw_amount, auto_points_amount: initialRank.auto_points_amount, }) const [perms, setPerms] = useState>(initialRank.permissions) const [cmsPermSlugs, setCmsPermSlugs] = useState(initialCmsRole?.permissionSlugs ?? []) const [permSearch, setPermSearch] = useState('') const [cmsSearch, setCmsSearch] = useState('') const [collapsed, setCollapsed] = useState>(new Set()) const cats = useMemo( () => categorizePerms(Object.keys(initialRank.permissions)), [initialRank.permissions], ) const enabledCount = Object.values(perms).filter((v) => v !== '0').length const totalCount = Object.keys(perms).length function setNum(field: string, val: string) { setGeneral((p) => ({ ...p, [field]: parseInt(val, 10) || 0 })) } function saveRank() { run(() => saveRankAction({ id: initialRank.id, fields: { ...general, ...perms } }), { successMessage: 'Rank saved', }) } async function toggleCms(slug: string) { if (!initialCmsRole) return const prev = cmsPermSlugs const next = prev.includes(slug) ? prev.filter((s) => s !== slug) : [...prev, slug] setCmsPermSlugs(next) try { const r = await setCmsPermissions({ roleId: initialCmsRole.id, permissionSlugs: next }) if (!r.ok) { setCmsPermSlugs(prev) toast.error('Failed') } } catch { setCmsPermSlugs(prev) toast.error('Failed') } } function toggle(key: string) { setCollapsed((p) => { const n = new Set(p) if (n.has(key)) n.delete(key) else n.add(key) return n }) } /* ── Collapsible perm section ──────────────────────────── */ function setSectionAll(keys: string[], value: string) { setPerms((p) => { const next = { ...p } for (const k of keys) next[k] = value return next }) } function setAllPerms(value: string) { setPerms((p) => { const next = { ...p } for (const k of Object.keys(next)) next[k] = value return next }) } function PermSection({ title, keys, sectionKey, }: { title: string keys: string[] sectionKey: string }) { const filtered = permSearch ? keys.filter((k) => k.toLowerCase().includes(permSearch.toLowerCase())) : keys if (permSearch && filtered.length === 0) return null const on = filtered.filter((k) => (perms[k] ?? '0') !== '0').length const _allOn = on === filtered.length const open = !collapsed.has(sectionKey) return (
{open && (
)}
{open && (
{filtered.map((key) => (
{key} setPerms((p) => ({ ...p, [key]: v }))} />
))}
)}
) } const filteredCms = cmsSearch ? allCmsPermissions.filter( (p) => p.slug.toLowerCase().includes(cmsSearch.toLowerCase()) || p.title?.toLowerCase().includes(cmsSearch.toLowerCase()), ) : allCmsPermissions return (
{/* Header */}
{initialRank.id >= 7 ? ( ) : initialRank.id >= 5 ? ( ) : ( )}

{general.rank_name || `Rank ${initialRank.id}`}

Rank #{initialRank.id} · Level {initialRank.level} · {totalUsers} users

{/* Tabs */} General Emulator ({enabledCount}) CMS ({cmsPermSlugs.length}) Users ({totalUsers}) {/* ═══ General ═══ */} Rank Settings
setGeneral({ ...general, rank_name: e.target.value })} />
setGeneral({ ...general, badge: e.target.value })} />
setGeneral({ ...general, prefix: e.target.value })} />
setGeneral({ ...general, prefix_color: e.target.value })} /> {general.prefix_color && (
)}

Hide from public

setGeneral({ ...general, hidden_rank: v ? '1' : '0' })} />

Record to audit log

setGeneral({ ...general, log_commands: v ? '1' : '0' })} />
setNum('room_effect', e.target.value)} />
setNum('auto_credits_amount', e.target.value)} />
setNum('auto_pixels_amount', e.target.value)} />
setNum('auto_gotw_amount', e.target.value)} />
setNum('auto_points_amount', e.target.value)} />
{/* Users preview */} Users with this rank {totalUsers > users.length && ( (showing {users.length} of {totalUsers}) )} {users.length === 0 ? (

No users with this rank.

) : (
{users.map((u) => ( {u.username} ))}
)}
{/* ═══ Emulator Permissions ═══ */}

{enabledCount} of {totalCount} permissions enabled

Off On Owner
setPermSearch(e.target.value)} className="pl-9" />
{cats.other.length > 0 && ( )}
{/* ═══ CMS Permissions ═══ */} {initialRank.id < 4 ? (

CMS permissions are available from rank 4 and above.

) : ( <>

{cmsPermSlugs.length} of {allCmsPermissions.length} assigned

setCmsSearch(e.target.value)} className="pl-9" />
{filteredCms.map((perm) => { const on = cmsPermSlugs.includes(perm.slug) return (
toggleCms(perm.slug)} />
{perm.slug} {perm.title && ( {perm.title} )}
{on && ( Active )}
) })}
{filteredCms.length === 0 && (

No permissions match your search.

)} )}
{/* ═══ Users ═══ */} Users with this rank {totalUsers > users.length && ( (showing {users.length} of {totalUsers}) )} {users.length === 0 ? (

No users with this rank.

) : (
{users.map((u) => ( {u.username} ))}
)}
) }