1
- "use client"
1
+ "use client" ;
2
2
3
- import React , { useState , useEffect } from ' react' ;
4
- import axios from ' axios' ;
5
- import { useNavigate } from ' react-router-dom' ;
6
- import { toast } from "sonner" ;
3
+ import React , { useState , useEffect } from " react" ;
4
+ import axios from " axios" ;
5
+ import { useNavigate } from " react-router-dom" ;
6
+ import { toast } from "sonner" ;
7
7
8
8
import { cn } from "@/lib/utils" ;
9
9
import { Icons } from "@/components/ui/icons" ;
10
10
import { Button } from "@/components/ui/button" ;
11
11
import { Input } from "@/components/ui/input" ;
12
12
import { Label } from "@/components/ui/label" ;
13
13
14
- interface UserAuthFormProps extends React . HTMLAttributes < HTMLDivElement > { }
14
+ interface UserAuthFormProps extends React . HTMLAttributes < HTMLDivElement > { }
15
+ interface PasswordRules {
16
+ minLength : boolean ;
17
+ containsUpper : boolean ;
18
+ containsLower : boolean ;
19
+ containsNumber : boolean ;
20
+ containsSpecial : boolean ;
21
+ noWhitespace : boolean ;
22
+ }
15
23
16
- const backendUrl = import . meta. env . VITE_BACKEND_URL || ' http://localhost:5000' ;
24
+ const backendUrl = import . meta. env . VITE_BACKEND_URL || " http://localhost:5000" ;
17
25
18
26
export function UserAuthForm ( { className, ...props } : UserAuthFormProps ) {
19
27
const [ isLoading , setIsLoading ] = useState < boolean > ( false ) ;
20
- const [ username , setUsername ] = useState < string > ( '' ) ;
21
- const [ isUsernameAvailable , setIsUsernameAvailable ] = useState < boolean | null > ( null ) ;
22
- const [ email , setEmail ] = useState < string > ( '' ) ;
23
- const [ password , setPassword ] = useState < string > ( '' ) ;
28
+ const [ username , setUsername ] = useState < string > ( "" ) ;
29
+ const [ isUsernameAvailable , setIsUsernameAvailable ] = useState <
30
+ boolean | null
31
+ > ( null ) ;
32
+ const [ email , setEmail ] = useState < string > ( "" ) ;
33
+ const [ password , setPassword ] = useState < string > ( "" ) ;
34
+ const [ usernameError , setUsernameError ] = useState < string > ( "" ) ;
35
+ const [ passwordRules , setPasswordRules ] = useState < PasswordRules | null > (
36
+ null
37
+ ) ;
38
+ const [ isPasswordValid , setIsPasswordValid ] = useState < boolean > ( false ) ;
24
39
const navigate = useNavigate ( ) ; // Hook for navigation
25
40
26
41
useEffect ( ( ) => {
27
42
const checkUsernameAvailability = async ( ) => {
28
43
if ( username ) {
29
44
try {
45
+ if ( username . match ( / ^ [ a - z A - Z 0 - 9 ] + $ / ) === null ) {
46
+ setUsernameError ( "Username must be alphanumeric" ) ;
47
+ setIsUsernameAvailable ( null ) ;
48
+ return ;
49
+ } else if ( username . length <= 7 ) {
50
+ setUsernameError ( "Username must be greater than 7 characters" ) ;
51
+ setIsUsernameAvailable ( null ) ;
52
+ return ;
53
+ } else if ( username . length >= 12 ) {
54
+ setUsernameError ( "Username must be less than 12 characters" ) ;
55
+ setIsUsernameAvailable ( null ) ;
56
+ return ;
57
+ }
58
+ setUsernameError ( "" ) ;
30
59
const response = await axios . get ( `${ backendUrl } /check_username` , {
31
- params : { username }
60
+ params : { username } ,
32
61
} ) ;
33
62
setIsUsernameAvailable ( response . data . available ) ;
34
63
} catch ( error ) {
35
- console . error ( ' Error checking username availability:' , error ) ;
64
+ console . error ( " Error checking username availability:" , error ) ;
36
65
}
37
66
} else {
38
67
setIsUsernameAvailable ( null ) ;
68
+ setUsernameError ( "" ) ;
39
69
}
40
70
} ;
41
71
@@ -46,20 +76,55 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
46
76
return ( ) => clearTimeout ( delayDebounceFn ) ;
47
77
} , [ username ] ) ;
48
78
79
+ const validatePassword = ( password : string ) : PasswordRules => {
80
+ return {
81
+ minLength : password . length >= 6 ,
82
+ containsUpper : / [ A - Z ] / . test ( password ) ,
83
+ containsLower : / [ a - z ] / . test ( password ) ,
84
+ containsNumber : / \d / . test ( password ) ,
85
+ containsSpecial : / [ ! @ # $ % ^ & * ( ) , . ? " : { } | < > ] / . test ( password ) ,
86
+ noWhitespace : ! / \s / . test ( password ) ,
87
+ } ;
88
+ } ;
89
+
90
+ const handlePasswordChange = ( event : React . ChangeEvent < HTMLInputElement > ) => {
91
+ const newPassword = event . target . value ;
92
+ setPassword ( newPassword ) ;
93
+ const passwordValidityConditions = validatePassword ( newPassword ) ;
94
+ setPasswordRules ( passwordValidityConditions ) ;
95
+ const isNewPasswordValid = Object . values ( passwordValidityConditions ) . every (
96
+ ( rulePassed ) => rulePassed === true
97
+ ) ;
98
+ setIsPasswordValid ( isNewPasswordValid ) ;
99
+ } ;
100
+
101
+ const passwordRuleMessages : Record < keyof PasswordRules , string > = {
102
+ minLength : "Should be a minimum of 6 characters." ,
103
+ containsUpper : "Contains at least one uppercase letter." ,
104
+ containsLower : "Contains at least one lowercase letter." ,
105
+ containsNumber : "Contains at least one number." ,
106
+ containsSpecial : "Contains at least one special symbol." ,
107
+ noWhitespace : "Should not contain spaces." ,
108
+ } ;
109
+
49
110
async function onSubmit ( event : React . SyntheticEvent ) {
50
111
event . preventDefault ( ) ;
51
112
setIsLoading ( true ) ;
52
113
114
+ if ( usernameError || ! isPasswordValid ) {
115
+ return ;
116
+ }
117
+
53
118
try {
54
119
await axios . post ( `${ backendUrl } /signup` , { username, email, password } ) ;
55
120
toast . success ( "Signup successful" , {
56
121
description : "You can now log in with your new account." ,
57
122
action : {
58
123
label : "Login" ,
59
- onClick : ( ) => navigate ( ' /login' ) ,
124
+ onClick : ( ) => navigate ( " /login" ) ,
60
125
} ,
61
126
} ) ;
62
- navigate ( ' /login' ) ; // Redirect to login page on successful signup
127
+ navigate ( " /login" ) ; // Redirect to login page on successful signup
63
128
} catch ( err : any ) {
64
129
if ( err . response && err . response . data && err . response . data . message ) {
65
130
toast . error ( err . response . data . message , {
@@ -78,7 +143,7 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
78
143
} ,
79
144
} ) ;
80
145
}
81
- console . error ( ' Error during signup:' , err ) ;
146
+ console . error ( " Error during signup:" , err ) ;
82
147
} finally {
83
148
setIsLoading ( false ) ;
84
149
}
@@ -110,6 +175,7 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
110
175
{ isUsernameAvailable === true && (
111
176
< p className = "text-green-500" > Username is available</ p >
112
177
) }
178
+ { usernameError && < p className = "text-red-500" > { usernameError } </ p > }
113
179
</ div >
114
180
< div className = "grid gap-1" >
115
181
< Label className = "sr-only" htmlFor = "email" >
@@ -139,11 +205,27 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
139
205
autoComplete = "current-password"
140
206
disabled = { isLoading }
141
207
value = { password }
142
- onChange = { ( e ) => setPassword ( e . target . value ) }
208
+ onChange = { handlePasswordChange }
143
209
required
144
210
/>
211
+ { passwordRules &&
212
+ Object . entries ( passwordRules ) . map ( ( [ rule , passed ] ) => (
213
+ < p
214
+ key = { rule }
215
+ className = { passed ? "text-green-500" : "text-red-500" }
216
+ >
217
+ { passwordRuleMessages [ rule as keyof PasswordRules ] }
218
+ </ p >
219
+ ) ) }
145
220
</ div >
146
- < Button disabled = { isLoading || isUsernameAvailable === false } >
221
+ < Button
222
+ disabled = {
223
+ isLoading ||
224
+ isUsernameAvailable === false ||
225
+ usernameError !== "" ||
226
+ isPasswordValid === false
227
+ }
228
+ >
147
229
{ isLoading && (
148
230
< Icons . spinner className = "mr-2 h-4 w-4 animate-spin" />
149
231
) }
0 commit comments