- 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
205 lines
5.8 KiB
TypeScript
205 lines
5.8 KiB
TypeScript
'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>
|
|
)
|
|
}
|