mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
refactor: use rhf for login and onboarding
This commit is contained in:
@@ -1,19 +1,32 @@
|
|||||||
|
import { arktypeResolver } from "@hookform/resolvers/arktype";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { useId, useState } from "react";
|
import { type } from "arktype";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { loginMutation } from "~/api-client/@tanstack/react-query.gen";
|
import { loginMutation } from "~/api-client/@tanstack/react-query.gen";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
import { Label } from "~/components/ui/label";
|
|
||||||
|
const loginSchema = type({
|
||||||
|
username: "2<=string<=50",
|
||||||
|
password: "string>=1",
|
||||||
|
});
|
||||||
|
|
||||||
|
type LoginFormValues = typeof loginSchema.inferIn;
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [username, setUsername] = useState("");
|
|
||||||
const [password, setPassword] = useState("");
|
const form = useForm<LoginFormValues>({
|
||||||
const usernameId = useId();
|
resolver: arktypeResolver(loginSchema),
|
||||||
const passwordId = useId();
|
defaultValues: {
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const login = useMutation({
|
const login = useMutation({
|
||||||
...loginMutation(),
|
...loginMutation(),
|
||||||
@@ -26,17 +39,11 @@ export default function LoginPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const onSubmit = (values: LoginFormValues) => {
|
||||||
e.preventDefault();
|
|
||||||
if (!username.trim() || !password.trim()) {
|
|
||||||
toast.error("Username and password are required");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
login.mutate({
|
login.mutate({
|
||||||
body: {
|
body: {
|
||||||
username: username.trim(),
|
username: values.username.trim(),
|
||||||
password: password.trim(),
|
password: values.password.trim(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -49,36 +56,45 @@ export default function LoginPage() {
|
|||||||
<CardDescription>Sign in to your account</CardDescription>
|
<CardDescription>Sign in to your account</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<Form {...form}>
|
||||||
<div className="space-y-2">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<Label htmlFor={usernameId}>Username</Label>
|
<FormField
|
||||||
<Input
|
control={form.control}
|
||||||
id={usernameId}
|
name="username"
|
||||||
type="text"
|
render={({ field }) => (
|
||||||
placeholder="Enter your username"
|
<FormItem>
|
||||||
value={username}
|
<FormLabel>Username</FormLabel>
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
<FormControl>
|
||||||
disabled={login.isPending}
|
<Input
|
||||||
autoFocus
|
{...field}
|
||||||
required
|
type="text"
|
||||||
|
placeholder="Enter your username"
|
||||||
|
disabled={login.isPending}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
<FormField
|
||||||
<div className="space-y-2">
|
control={form.control}
|
||||||
<Label htmlFor={passwordId}>Password</Label>
|
name="password"
|
||||||
<Input
|
render={({ field }) => (
|
||||||
id={passwordId}
|
<FormItem>
|
||||||
type="password"
|
<FormLabel>Password</FormLabel>
|
||||||
placeholder="Enter your password"
|
<FormControl>
|
||||||
value={password}
|
<Input {...field} type="password" placeholder="Enter your password" disabled={login.isPending} />
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
</FormControl>
|
||||||
disabled={login.isPending}
|
<FormMessage />
|
||||||
required
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
<Button type="submit" className="w-full" loading={login.isPending}>
|
||||||
<Button type="submit" className="w-full" loading={login.isPending}>
|
Sign In
|
||||||
Sign In
|
</Button>
|
||||||
</Button>
|
</form>
|
||||||
</form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,23 +1,36 @@
|
|||||||
|
import { arktypeResolver } from "@hookform/resolvers/arktype";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { useId, useState } from "react";
|
import { type } from "arktype";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { registerMutation } from "~/api-client/@tanstack/react-query.gen";
|
import { registerMutation } from "~/api-client/@tanstack/react-query.gen";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
import { Label } from "~/components/ui/label";
|
|
||||||
|
const onboardingSchema = type({
|
||||||
|
username: "2<=string<=50",
|
||||||
|
password: "string>=8",
|
||||||
|
confirmPassword: "string>=1",
|
||||||
|
});
|
||||||
|
|
||||||
|
type OnboardingFormValues = typeof onboardingSchema.inferIn;
|
||||||
|
|
||||||
export default function OnboardingPage() {
|
export default function OnboardingPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [username, setUsername] = useState("");
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const [confirmPassword, setConfirmPassword] = useState("");
|
|
||||||
const usernameId = useId();
|
|
||||||
const passwordId = useId();
|
|
||||||
const confirmPasswordId = useId();
|
|
||||||
|
|
||||||
const register = useMutation({
|
const form = useForm<OnboardingFormValues>({
|
||||||
|
resolver: arktypeResolver(onboardingSchema),
|
||||||
|
defaultValues: {
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
confirmPassword: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registerUser = useMutation({
|
||||||
...registerMutation(),
|
...registerMutation(),
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
toast.success("Admin user created successfully!");
|
toast.success("Admin user created successfully!");
|
||||||
@@ -29,22 +42,19 @@ export default function OnboardingPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const onSubmit = (values: OnboardingFormValues) => {
|
||||||
e.preventDefault();
|
if (values.password !== values.confirmPassword) {
|
||||||
if (!username.trim() || !password.trim()) {
|
form.setError("confirmPassword", {
|
||||||
toast.error("Username and password are required");
|
type: "manual",
|
||||||
|
message: "Passwords do not match",
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password !== confirmPassword) {
|
registerUser.mutate({
|
||||||
toast.error("Passwords do not match");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
register.mutate({
|
|
||||||
body: {
|
body: {
|
||||||
username: username.trim(),
|
username: values.username.trim(),
|
||||||
password: password.trim(),
|
password: values.password.trim(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -57,48 +67,64 @@ export default function OnboardingPage() {
|
|||||||
<CardDescription>Create the admin user to get started</CardDescription>
|
<CardDescription>Create the admin user to get started</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<Form {...form}>
|
||||||
<div className="space-y-2">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<Label htmlFor={usernameId}>Username</Label>
|
<FormField
|
||||||
<Input
|
control={form.control}
|
||||||
id={usernameId}
|
name="username"
|
||||||
type="text"
|
render={({ field }) => (
|
||||||
placeholder="admin"
|
<FormItem>
|
||||||
value={username}
|
<FormLabel>Username</FormLabel>
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
<FormControl>
|
||||||
disabled={register.isPending}
|
<Input {...field} type="text" placeholder="admin" disabled={registerUser.isPending} autoFocus />
|
||||||
autoFocus
|
</FormControl>
|
||||||
required
|
<FormDescription>Choose a username for the admin account (2-50 characters).</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
<FormField
|
||||||
<div className="space-y-2">
|
control={form.control}
|
||||||
<Label htmlFor={passwordId}>Password</Label>
|
name="password"
|
||||||
<Input
|
render={({ field }) => (
|
||||||
id={passwordId}
|
<FormItem>
|
||||||
type="password"
|
<FormLabel>Password</FormLabel>
|
||||||
placeholder="Enter a secure password"
|
<FormControl>
|
||||||
value={password}
|
<Input
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
{...field}
|
||||||
disabled={register.isPending}
|
type="password"
|
||||||
required
|
placeholder="Enter a secure password"
|
||||||
|
disabled={registerUser.isPending}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Password must be at least 8 characters long.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
<FormField
|
||||||
<div className="space-y-2">
|
control={form.control}
|
||||||
<Label htmlFor={confirmPasswordId}>Confirm Password</Label>
|
name="confirmPassword"
|
||||||
<Input
|
render={({ field }) => (
|
||||||
id={confirmPasswordId}
|
<FormItem>
|
||||||
type="password"
|
<FormLabel>Confirm Password</FormLabel>
|
||||||
placeholder="Re-enter your password"
|
<FormControl>
|
||||||
value={confirmPassword}
|
<Input
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
{...field}
|
||||||
disabled={register.isPending}
|
type="password"
|
||||||
required
|
placeholder="Re-enter your password"
|
||||||
|
disabled={registerUser.isPending}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
<Button type="submit" className="w-full" loading={registerUser.isPending}>
|
||||||
<Button type="submit" className="w-full" disabled={register.isPending}>
|
Create Admin User
|
||||||
{register.isPending ? "Creating..." : "Create Admin User"}
|
</Button>
|
||||||
</Button>
|
</form>
|
||||||
</form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user