11'use client' ;
22
33import { useTranslations } from 'next-intl' ;
4- import { useState } from 'react' ;
4+ import { useMemo , useState } from 'react' ;
55
66import { AuthErrorBanner } from '@/components/auth/AuthErrorBanner' ;
77import { AuthShell } from '@/components/auth/AuthShell' ;
88import { AuthSuccessBanner } from '@/components/auth/AuthSuccessBanner' ;
99import { PasswordField } from '@/components/auth/fields/PasswordField' ;
1010import { Button } from '@/components/ui/button' ;
11+ import {
12+ PASSWORD_MAX_LEN ,
13+ PASSWORD_MIN_LEN ,
14+ PASSWORD_POLICY_REGEX ,
15+ } from '@/lib/auth/signup-constraints' ;
1116
1217type ResetPasswordFormProps = {
1318 token : string ;
@@ -19,8 +24,31 @@ export function ResetPasswordForm({ token }: ResetPasswordFormProps) {
1924 const [ error , setError ] = useState < string | null > ( null ) ;
2025 const [ success , setSuccess ] = useState ( false ) ;
2126
27+ const [ passwordValue , setPasswordValue ] = useState ( '' ) ;
28+ const [ passwordTouched , setPasswordTouched ] = useState ( false ) ;
29+
30+ const passwordPolicyOk = useMemo ( ( ) => {
31+ if ( ! passwordValue ) return false ;
32+ if ( passwordValue . length < PASSWORD_MIN_LEN ) return false ;
33+ if ( passwordValue . length > PASSWORD_MAX_LEN ) return false ;
34+ return PASSWORD_POLICY_REGEX . test ( passwordValue ) ;
35+ } , [ passwordValue ] ) ;
36+
37+ const passwordRequirementsText =
38+ '8–128 characters, at least one capital letter, and at least one special character.' ;
39+
40+ const passwordErrorText =
41+ passwordTouched && ! passwordPolicyOk
42+ ? `Password must meet requirements: ${ passwordRequirementsText } `
43+ : null ;
44+
45+ const submitDisabled = loading || ! passwordPolicyOk ;
46+
2247 async function onSubmit ( e : React . FormEvent < HTMLFormElement > ) {
2348 e . preventDefault ( ) ;
49+
50+ if ( submitDisabled ) return ;
51+
2452 setLoading ( true ) ;
2553 setError ( null ) ;
2654
@@ -38,8 +66,12 @@ export function ResetPasswordForm({ token }: ResetPasswordFormProps) {
3866 } ) ,
3967 } ) ;
4068
69+ const data = await res . json ( ) . catch ( ( ) => null ) ;
70+
4171 if ( ! res . ok ) {
42- setError ( t ( 'errors.resetFailed' ) ) ;
72+ const msg =
73+ typeof data ?. error === 'string' ? data . error : t ( 'errors.resetFailed' ) ;
74+ setError ( msg ) ;
4375 return ;
4476 }
4577
@@ -57,15 +89,30 @@ export function ResetPasswordForm({ token }: ResetPasswordFormProps) {
5789 < AuthSuccessBanner message = { t ( 'success' ) } />
5890 ) : (
5991 < form onSubmit = { onSubmit } className = "space-y-4" >
60- < PasswordField minLength = { 8 } />
92+ < div className = "space-y-1" >
93+ < PasswordField
94+ id = "password"
95+ name = "password"
96+ placeholder = "New password"
97+ autoComplete = "new-password"
98+ minLength = { PASSWORD_MIN_LEN }
99+ maxLength = { PASSWORD_MAX_LEN }
100+ pattern = { PASSWORD_POLICY_REGEX . source }
101+ onChange = { setPasswordValue }
102+ onBlur = { ( ) => setPasswordTouched ( true ) }
103+ />
104+ { passwordErrorText && (
105+ < p className = "text-sm text-red-600" > { passwordErrorText } </ p >
106+ ) }
107+ </ div >
61108
62109 { error && < AuthErrorBanner message = { error } /> }
63110
64- < Button type = "submit" disabled = { loading } className = "w-full" >
111+ < Button type = "submit" disabled = { submitDisabled } className = "w-full" >
65112 { loading ? t ( 'submitting' ) : t ( 'submit' ) }
66113 </ Button >
67114 </ form >
68115 ) }
69116 </ AuthShell >
70117 ) ;
71- }
118+ }
0 commit comments