Reapply "Add missing admin action files and navigation links"

This reverts commit 4d515bc400.
This commit is contained in:
Simo committed 2026-07-11 20:52:56 +02:00
1 parent 96ed768f14
commit 5b4228261a
338 files changed
+28143 -5948

No files matched your search

@@ -0,0 +1,204 @@
'use client'
import { Loader2, UserPlus } from 'lucide-react'
import { useState } from 'react'
import { toast } from 'sonner'
import { createUser } from '@/actions/users'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { useServerAction } from '@/hooks/use-server-action'
interface CreateUserDialogProps {
sessionRank: number
}
const RANK_OPTIONS: { value: number; label: string }[] = [
{ value: 1, label: 'User (1)' },
{ value: 2, label: 'Bronze (2)' },
{ value: 3, label: 'Silver (3)' },
{ value: 4, label: 'Gold (4)' },
{ value: 5, label: 'Moderator (5)' },
{ value: 6, label: 'Admin (6)' },
{ value: 7, label: 'Super Admin (7)' },
]
export function CreateUserDialog({ sessionRank }: CreateUserDialogProps) {
const { run, isPending } = useServerAction()
const [open, setOpen] = useState(false)
const [username, setUsername] = useState('')
const [mail, setMail] = useState('')
const [password, setPassword] = useState('')
const [rank, setRank] = useState('1')
const [motto, setMotto] = useState('')
const allowedRanks = RANK_OPTIONS.filter((r) => (sessionRank >= 7 ? true : r.value < sessionRank))
function reset() {
setUsername('')
setMail('')
setPassword('')
setRank('1')
setMotto('')
}
function generatePassword() {
const charset = 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ23456789'
let out = ''
const buf = new Uint32Array(12)
crypto.getRandomValues(buf)
for (let i = 0; i < buf.length; i++) out += charset[buf[i] % charset.length]
setPassword(out)
toast.message('Password generated', { description: out })
}
function handleCreate() {
const u = username.trim()
const m = mail.trim()
if (!u || !m || !password) {
toast.error('Username, email and password are required')
return
}
if (u.length < 3 || u.length > 20 || !/^[a-zA-Z0-9_-]+$/.test(u)) {
toast.error('Invalid username (3-20 chars, letters/digits/_/-)')
return
}
if (password.length < 6) {
toast.error('Password must be at least 6 characters')
return
}
run(
() =>
createUser({
username: u,
mail: m,
password,
rank: Number(rank),
motto: motto.trim(),
}),
{
successMessage: `User "${u}" created.`,
onSuccess: () => {
setOpen(false)
reset()
},
},
)
}
return (
<Dialog
open={open}
onOpenChange={(next) => {
setOpen(next)
if (!next) reset()
}}
>
<DialogTrigger asChild>
<Button>
<UserPlus className="mr-2 h-4 w-4" />
New User
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create User</DialogTitle>
<DialogDescription>
Account is created immediately with default look and 5,000 credits/duckets/diamonds.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="cu-username">Username</Label>
<Input
id="cu-username"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="HabboName"
autoComplete="off"
maxLength={20}
/>
</div>
<div className="space-y-2">
<Label htmlFor="cu-mail">Email</Label>
<Input
id="cu-mail"
type="email"
value={mail}
onChange={(e) => setMail(e.target.value)}
placeholder="[email protected]"
autoComplete="off"
/>
</div>
<div className="space-y-2">
<Label htmlFor="cu-password">Password</Label>
<div className="flex gap-2">
<Input
id="cu-password"
type="text"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Minimum 6 characters"
autoComplete="new-password"
className="font-mono"
/>
<Button type="button" variant="outline" onClick={generatePassword}>
Generate
</Button>
</div>
</div>
<div className="space-y-2">
<Label>Rank</Label>
<Select value={rank} onValueChange={setRank}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{allowedRanks.map((r) => (
<SelectItem key={r.value} value={String(r.value)}>
{r.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="cu-motto">Motto (optional)</Label>
<Input
id="cu-motto"
value={motto}
onChange={(e) => setMotto(e.target.value)}
maxLength={127}
placeholder="I'm new here!"
/>
</div>
<Button onClick={handleCreate} disabled={isPending} className="w-full">
{isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<UserPlus className="mr-2 h-4 w-4" />
)}
Create User
</Button>
</div>
</DialogContent>
</Dialog>
)
}