Files
Felis/panel/src/components/CreateUserDialog.tsx
T

190 lines
5.6 KiB
TypeScript

import { useState } from "react";
import { Plus, Loader2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api, humanizeError } from "@/lib/api";
interface Props {
onCreated: (id: string) => void;
}
export function CreateUserDialog({ onCreated }: Props) {
const { t } = useTranslation("admin");
const [open, setOpen] = useState(false);
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [role, setRole] = useState<"user" | "admin">("user");
const [password, setPassword] = useState("");
const [mustChange, setMustChange] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [err, setErr] = useState<string | null>(null);
function reset() {
setUsername("");
setEmail("");
setRole("user");
setPassword("");
setMustChange(true);
setErr(null);
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (submitting) return;
setErr(null);
if (!username.trim()) {
setErr(t("users_create_validation_username"));
return;
}
if (password.length < 8) {
setErr(t("users_create_validation_password"));
return;
}
setSubmitting(true);
try {
const u = await api.createUser({
username: username.trim(),
email: email.trim() || undefined,
role,
password,
must_change_password: mustChange,
});
setOpen(false);
reset();
onCreated(u.id);
} catch (e) {
setErr(humanizeError(e));
} finally {
setSubmitting(false);
}
}
return (
<Dialog open={open} onOpenChange={(v) => { setOpen(v); if (!v) reset(); }}>
<DialogTrigger asChild>
<Button size="sm" className="gap-1.5">
<Plus className="h-4 w-4" />
{t("users_create_btn")}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{t("users_create_title")}</DialogTitle>
<DialogDescription>
{t("users_create_desc")}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Username */}
<div className="space-y-1.5">
<Label className="text-xs font-semibold text-muted-foreground">
{t("users_field_username")} *
</Label>
<Input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder={t("users_create_username_placeholder")}
className="h-9 text-sm"
autoFocus
/>
</div>
{/* Email */}
<div className="space-y-1.5">
<Label className="text-xs font-semibold text-muted-foreground">
{t("users_field_email")}
</Label>
<Input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="[email protected]"
className="h-9 text-sm"
/>
</div>
{/* Role */}
<div className="space-y-1.5">
<Label className="text-xs font-semibold text-muted-foreground">
{t("users_field_role")}
</Label>
<Select value={role} onValueChange={(v: "user" | "admin") => setRole(v)}>
<SelectTrigger className="h-9 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="user">{t("users_role_user")}</SelectItem>
<SelectItem value="admin">{t("users_role_admin")}</SelectItem>
</SelectContent>
</Select>
</div>
{/* Password */}
<div className="space-y-1.5">
<Label className="text-xs font-semibold text-muted-foreground">
{t("users_field_password")} *
</Label>
<Input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="min. 8 characters"
className="h-9 text-sm"
/>
</div>
{/* Must change password toggle */}
<label className="flex items-center gap-2 cursor-pointer select-none">
<input
type="checkbox"
checked={mustChange}
onChange={(e) => setMustChange(e.target.checked)}
className="h-4 w-4 rounded border-border"
/>
<span className="text-sm text-foreground">
{t("users_create_must_change")}
</span>
</label>
{err && (
<p className="rounded-md border border-destructive/20 bg-destructive/10 p-3 text-sm text-destructive">
{err}
</p>
)}
<DialogFooter>
<Button type="submit" disabled={submitting} className="gap-1.5">
{submitting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Plus className="h-4 w-4" />
)}
{t("users_create_btn")}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}