"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() { const fields = Object.fromEntries( Object.entries({ ...general, ...perms }).map(([key, value]) => [ key, String(value), ]), ); run(() => saveRankAction({ id: initialRank.id, fields }), { 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; void _allOn; 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} ))}
)}
); }