11'use client' ;
22
3+ import { zodResolver } from '@hookform/resolvers/zod' ;
4+ import { registerSchema } from '@veridion/shared' ;
35import { Eye , EyeOff , Lock , Mail , Shield , User } from 'lucide-react' ;
46import Link from 'next/link' ;
57import { useRouter } from 'next/navigation' ;
68import { useState } from 'react' ;
9+ import { useForm } from 'react-hook-form' ;
710import { toast } from 'sonner' ;
11+ import { type z } from 'zod' ;
812
9- import { apiFetch , storeAccessToken } from '@/lib/api-client' ;
13+ import { useAuth } from '@/lib/auth' ;
14+
15+ type RegisterForm = z . infer < typeof registerSchema > ;
1016
1117export default function RegisterPage ( ) {
18+ const { register : registerUser , isAuthenticated } = useAuth ( ) ;
1219 const router = useRouter ( ) ;
1320 const [ showPassword , setShowPassword ] = useState ( false ) ;
14- const [ loading , setLoading ] = useState ( false ) ;
15-
16- async function handleSubmit ( e : React . FormEvent < HTMLFormElement > ) {
17- e . preventDefault ( ) ;
18- setLoading ( true ) ;
21+ const [ serverError , setServerError ] = useState < string | null > ( null ) ;
1922
20- const formData = new FormData ( e . currentTarget ) ;
21- const displayName = formData . get ( 'displayName' ) as string ;
22- const email = formData . get ( 'email' ) as string ;
23- const password = formData . get ( 'password' ) as string ;
23+ const {
24+ register,
25+ handleSubmit,
26+ formState : { errors, isSubmitting } ,
27+ } = useForm < RegisterForm > ( {
28+ resolver : zodResolver ( registerSchema ) ,
29+ } ) ;
2430
25- if ( ! / [ A - Z ] / . test ( password ) || ! / [ a - z ] / . test ( password ) || ! / [ 0 - 9 ] / . test ( password ) ) {
26- toast . error ( 'Password must contain uppercase, lowercase, and a number' ) ;
27- setLoading ( false ) ;
28- return ;
29- }
31+ // Redirect if already authenticated
32+ if ( isAuthenticated ) {
33+ router . replace ( '/dashboard' ) ;
34+ return null ;
35+ }
3036
37+ async function onSubmit ( data : RegisterForm ) {
38+ setServerError ( null ) ;
3139 try {
32- const response = await apiFetch < { accessToken : string } > ( '/auth/register' , {
33- method : 'POST' ,
34- body : JSON . stringify ( { displayName, email, password } ) ,
35- } ) ;
36- storeAccessToken ( response . accessToken ) ;
40+ await registerUser ( data . email , data . password , data . displayName ) ;
3741 toast . success ( 'Account created successfully!' ) ;
3842 router . push ( '/dashboard' ) ;
39- } catch ( error ) {
40- toast . error ( error instanceof Error ? error . message : 'Unable to create account' ) ;
41- } finally {
42- setLoading ( false ) ;
43+ } catch ( err ) {
44+ const message = err instanceof Error ? err . message : 'Registration failed' ;
45+ setServerError ( message ) ;
46+ toast . error ( message ) ;
4347 }
4448 }
4549
@@ -58,12 +62,20 @@ export default function RegisterPage() {
5862 </ div >
5963
6064 < div className = "bg-card rounded-xl border p-6 shadow-sm" >
65+ { /* Server error */ }
66+ { serverError && (
67+ < div className = "bg-destructive/10 text-destructive mb-4 rounded-lg px-4 py-2.5 text-sm" >
68+ { serverError }
69+ </ div >
70+ ) }
71+
6172 < form
6273 onSubmit = { ( e ) => {
63- void handleSubmit ( e ) ;
74+ void handleSubmit ( onSubmit ) ( e ) ;
6475 } }
6576 className = "space-y-5"
6677 >
78+ { /* Display name */ }
6779 < div >
6880 < label htmlFor = "displayName" className = "text-sm font-medium" >
6981 Display name
@@ -72,17 +84,20 @@ export default function RegisterPage() {
7284 < User className = "text-muted-foreground absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2" />
7385 < input
7486 id = "displayName"
75- name = "displayName"
7687 type = "text"
77- required
78- minLength = { 2 }
79- maxLength = { 50 }
8088 placeholder = "John Doe"
81- className = "bg-background placeholder:text-muted-foreground focus:ring-ring w-full rounded-lg border py-2 pl-10 pr-3 text-sm focus:outline-none focus:ring-2"
89+ className = { `bg-background placeholder:text-muted-foreground focus:ring-ring w-full rounded-lg border py-2 pl-10 pr-3 text-sm focus:outline-none focus:ring-2 ${
90+ errors . displayName ? 'border-destructive' : ''
91+ } `}
92+ { ...register ( 'displayName' ) }
8293 />
8394 </ div >
95+ { errors . displayName && (
96+ < p className = "text-destructive mt-1 text-xs" > { errors . displayName . message } </ p >
97+ ) }
8498 </ div >
8599
100+ { /* Email */ }
86101 < div >
87102 < label htmlFor = "email" className = "text-sm font-medium" >
88103 Email address
@@ -91,16 +106,21 @@ export default function RegisterPage() {
91106 < Mail className = "text-muted-foreground absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2" />
92107 < input
93108 id = "email"
94- name = "email"
95109 type = "email"
96110 autoComplete = "email"
97- required
98111 placeholder = "you@example.com"
99- className = "bg-background placeholder:text-muted-foreground focus:ring-ring w-full rounded-lg border py-2 pl-10 pr-3 text-sm focus:outline-none focus:ring-2"
112+ className = { `bg-background placeholder:text-muted-foreground focus:ring-ring w-full rounded-lg border py-2 pl-10 pr-3 text-sm focus:outline-none focus:ring-2 ${
113+ errors . email ? 'border-destructive' : ''
114+ } `}
115+ { ...register ( 'email' ) }
100116 />
101117 </ div >
118+ { errors . email && (
119+ < p className = "text-destructive mt-1 text-xs" > { errors . email . message } </ p >
120+ ) }
102121 </ div >
103122
123+ { /* Password */ }
104124 < div >
105125 < label htmlFor = "password" className = "text-sm font-medium" >
106126 Password
@@ -109,34 +129,39 @@ export default function RegisterPage() {
109129 < Lock className = "text-muted-foreground absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2" />
110130 < input
111131 id = "password"
112- name = "password"
113132 type = { showPassword ? 'text' : 'password' }
114133 autoComplete = "new-password"
115- required
116- minLength = { 8 }
117134 placeholder = "Min. 8 characters, uppercase, lowercase, number"
118- className = "bg-background placeholder:text-muted-foreground focus:ring-ring w-full rounded-lg border py-2 pl-10 pr-10 text-sm focus:outline-none focus:ring-2"
135+ className = { `bg-background placeholder:text-muted-foreground focus:ring-ring w-full rounded-lg border py-2 pl-10 pr-10 text-sm focus:outline-none focus:ring-2 ${
136+ errors . password ? 'border-destructive' : ''
137+ } `}
138+ { ...register ( 'password' ) }
119139 />
120140 < button
121141 type = "button"
122142 onClick = { ( ) => setShowPassword ( ! showPassword ) }
123143 className = "text-muted-foreground hover:text-foreground absolute right-3 top-1/2 -translate-y-1/2"
144+ aria-label = { showPassword ? 'Hide password' : 'Show password' }
124145 >
125146 { showPassword ? < EyeOff className = "h-4 w-4" /> : < Eye className = "h-4 w-4" /> }
126147 </ button >
127148 </ div >
149+ { errors . password && (
150+ < p className = "text-destructive mt-1 text-xs" > { errors . password . message } </ p >
151+ ) }
128152 < p className = "text-muted-foreground mt-1.5 text-xs" >
129153 Must contain at least 8 characters, one uppercase letter, one lowercase letter, and
130154 one number.
131155 </ p >
132156 </ div >
133157
158+ { /* Submit */ }
134159 < button
135160 type = "submit"
136- disabled = { loading }
161+ disabled = { isSubmitting }
137162 className = "bg-primary text-primary-foreground hover:bg-primary/90 w-full rounded-lg px-4 py-2.5 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50"
138163 >
139- { loading ? 'Creating account...' : 'Create account' }
164+ { isSubmitting ? 'Creating account...' : 'Create account' }
140165 </ button >
141166 </ form >
142167
0 commit comments