From 1ad8f69355bb44ebfe5a330a67a93f557ddb3822 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Thu, 2 Oct 2025 21:52:55 +0200 Subject: [PATCH] refactor: use rhf for login and onboarding --- apps/client/app/routes/login.tsx | 102 ++++++++++-------- apps/client/app/routes/onboarding.tsx | 146 +++++++++++++++----------- 2 files changed, 145 insertions(+), 103 deletions(-) diff --git a/apps/client/app/routes/login.tsx b/apps/client/app/routes/login.tsx index 3c3d0dc..a414fee 100644 --- a/apps/client/app/routes/login.tsx +++ b/apps/client/app/routes/login.tsx @@ -1,19 +1,32 @@ +import { arktypeResolver } from "@hookform/resolvers/arktype"; 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 { toast } from "sonner"; import { loginMutation } from "~/api-client/@tanstack/react-query.gen"; import { Button } from "~/components/ui/button"; 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 { Label } from "~/components/ui/label"; + +const loginSchema = type({ + username: "2<=string<=50", + password: "string>=1", +}); + +type LoginFormValues = typeof loginSchema.inferIn; export default function LoginPage() { const navigate = useNavigate(); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const usernameId = useId(); - const passwordId = useId(); + + const form = useForm({ + resolver: arktypeResolver(loginSchema), + defaultValues: { + username: "", + password: "", + }, + }); const login = useMutation({ ...loginMutation(), @@ -26,17 +39,11 @@ export default function LoginPage() { }, }); - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (!username.trim() || !password.trim()) { - toast.error("Username and password are required"); - return; - } - + const onSubmit = (values: LoginFormValues) => { login.mutate({ body: { - username: username.trim(), - password: password.trim(), + username: values.username.trim(), + password: values.password.trim(), }, }); }; @@ -49,36 +56,45 @@ export default function LoginPage() { Sign in to your account -
-
- - setUsername(e.target.value)} - disabled={login.isPending} - autoFocus - required + + + ( + + Username + + + + + + )} /> -
-
- - setPassword(e.target.value)} - disabled={login.isPending} - required + ( + + Password + + + + + + )} /> -
- -
+ + +
diff --git a/apps/client/app/routes/onboarding.tsx b/apps/client/app/routes/onboarding.tsx index a9b1efc..a8ff4d4 100644 --- a/apps/client/app/routes/onboarding.tsx +++ b/apps/client/app/routes/onboarding.tsx @@ -1,23 +1,36 @@ +import { arktypeResolver } from "@hookform/resolvers/arktype"; 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 { toast } from "sonner"; import { registerMutation } from "~/api-client/@tanstack/react-query.gen"; import { Button } from "~/components/ui/button"; 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 { 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() { 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({ + resolver: arktypeResolver(onboardingSchema), + defaultValues: { + username: "", + password: "", + confirmPassword: "", + }, + }); + + const registerUser = useMutation({ ...registerMutation(), onSuccess: async () => { toast.success("Admin user created successfully!"); @@ -29,22 +42,19 @@ export default function OnboardingPage() { }, }); - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (!username.trim() || !password.trim()) { - toast.error("Username and password are required"); + const onSubmit = (values: OnboardingFormValues) => { + if (values.password !== values.confirmPassword) { + form.setError("confirmPassword", { + type: "manual", + message: "Passwords do not match", + }); return; } - if (password !== confirmPassword) { - toast.error("Passwords do not match"); - return; - } - - register.mutate({ + registerUser.mutate({ body: { - username: username.trim(), - password: password.trim(), + username: values.username.trim(), + password: values.password.trim(), }, }); }; @@ -57,48 +67,64 @@ export default function OnboardingPage() { Create the admin user to get started -
-
- - setUsername(e.target.value)} - disabled={register.isPending} - autoFocus - required + + + ( + + Username + + + + Choose a username for the admin account (2-50 characters). + + + )} /> -
-
- - setPassword(e.target.value)} - disabled={register.isPending} - required + ( + + Password + + + + Password must be at least 8 characters long. + + + )} /> -
-
- - setConfirmPassword(e.target.value)} - disabled={register.isPending} - required + ( + + Confirm Password + + + + + + )} /> -
- -
+ + +