1+ import { useState , FormEvent , MouseEvent } from "react" ;
2+ import getAuthToken from "../../scripts/auth/getAuthToken" ;
3+ import { useNavigate } from "react-router-dom" ;
4+ import { Eye , EyeOff } from "lucide-react" ;
5+
6+ interface Props {
7+ setState : ( state : boolean ) => void ;
8+ }
9+
10+ interface Errors {
11+ oldPassword : string ;
12+ newPassword : string ;
13+ confirmPassword : string ;
14+ }
15+
16+ const API_BASE = import . meta. env . VITE_API_BASE_URL ;
17+
18+ function ChangePassword ( { setState } : Props ) {
19+ const navigate = useNavigate ( ) ;
20+
21+ const [ oldPassword , setOldPassword ] = useState ( "" ) ;
22+ const [ newPassword , setNewPassword ] = useState ( "" ) ;
23+ const [ confirmPassword , setConfirmPassword ] = useState ( "" ) ;
24+ const [ errors , setErrors ] = useState < Errors > ( {
25+ oldPassword : "" ,
26+ newPassword : "" ,
27+ confirmPassword : "" ,
28+ } ) ;
29+ const [ isLoading , setIsLoading ] = useState ( false ) ;
30+ const [ successState , setSuccessState ] = useState ( false ) ;
31+ const [ error , setError ] = useState < string | null > ( null ) ;
32+
33+ // New state for toggling visibility
34+ const [ showOld , setShowOld ] = useState ( false ) ;
35+ const [ showNew , setShowNew ] = useState ( false ) ;
36+ const [ showConfirm , setShowConfirm ] = useState ( false ) ;
37+
38+ const handleClose = ( ) => setState ( false ) ;
39+
40+ const validateForm = ( ) : boolean => {
41+ const newErrors : Errors = {
42+ oldPassword : "" ,
43+ newPassword : "" ,
44+ confirmPassword : "" ,
45+ } ;
46+
47+ if ( ! oldPassword . trim ( ) ) newErrors . oldPassword = "Old password is required" ;
48+ if ( ! newPassword . trim ( ) ) newErrors . newPassword = "New password is required" ;
49+ else if ( newPassword . length < 6 ) newErrors . newPassword = "New password must be at least 6 characters" ;
50+ if ( newPassword !== confirmPassword ) newErrors . confirmPassword = "Passwords do not match" ;
51+
52+ setErrors ( newErrors ) ;
53+ return Object . values ( newErrors ) . every ( ( v ) => v === "" ) ;
54+ } ;
55+
56+ const handleSubmit = async ( e : FormEvent ) => {
57+ e . preventDefault ( ) ;
58+ if ( ! validateForm ( ) ) return ;
59+
60+ setIsLoading ( true ) ;
61+ setError ( null ) ;
62+
63+ try {
64+ const token = await getAuthToken ( navigate ) ;
65+
66+ const res = await fetch ( `${ API_BASE } /api/user/change_password` , {
67+ method : "POST" ,
68+ headers : {
69+ "Content-Type" : "application/json" ,
70+ Authorization : `Bearer ${ token } ` ,
71+ } ,
72+ body : JSON . stringify ( { oldPassword, newPassword } ) ,
73+ } ) ;
74+
75+ const data = await res . json ( ) ;
76+
77+ if ( res . ok ) {
78+ setSuccessState ( true ) ;
79+ setTimeout ( ( ) => {
80+ navigate ( "/profile" ) ;
81+ } , 1500 ) ;
82+ } else if ( data . errors ) {
83+ const newErrors : Errors = {
84+ oldPassword : "" ,
85+ newPassword : "" ,
86+ confirmPassword : "" ,
87+ } ;
88+ data . errors . forEach ( ( err : { field : string ; message : string } ) => {
89+ if ( err . field in newErrors ) newErrors [ err . field as keyof Errors ] = err . message ;
90+ } ) ;
91+ setErrors ( newErrors ) ;
92+ } else {
93+ setError ( data . message || "Something went wrong" ) ;
94+ }
95+ } catch ( err ) {
96+ setError ( "Network error or unexpected issue." ) ;
97+ } finally {
98+ setIsLoading ( false ) ;
99+ }
100+ } ;
101+
102+ const handleOutsideClick = ( e : MouseEvent < HTMLDivElement > ) => {
103+ if ( e . target === e . currentTarget && ! isLoading ) handleClose ( ) ;
104+ } ;
105+
106+ const handleFormClick = ( e : MouseEvent < HTMLDivElement > ) => {
107+ e . stopPropagation ( ) ;
108+ } ;
109+
110+ const renderPasswordField = (
111+ label : string ,
112+ name : string ,
113+ value : string ,
114+ onChange : ( e : React . ChangeEvent < HTMLInputElement > ) => void ,
115+ errorMsg : string ,
116+ show : boolean ,
117+ toggleShow : ( ) => void
118+ ) => (
119+ < div className = "relative" >
120+ < label className = "block text-gray-700 font-medium mb-2" > { label } </ label >
121+
122+ < div className = "relative flex items-center" >
123+ < input
124+ type = { show ? "text" : "password" }
125+ name = { name }
126+ value = { value }
127+ onChange = { onChange }
128+ className = { `w-full px-4 py-2 pr-10 border ${
129+ errorMsg ? "border-red-500 ring-1 ring-red-500" : "border-gray-300"
130+ } rounded-lg focus:ring-2 focus:ring-accent focus:border-accent transition-colors`}
131+ placeholder = { `Enter ${ label . toLowerCase ( ) } ` }
132+ disabled = { isLoading }
133+ />
134+ < button
135+ type = "button"
136+ className = "absolute right-3 text-gray-400 hover:text-gray-700"
137+ onClick = { toggleShow }
138+ tabIndex = { - 1 }
139+ >
140+ { show ? < EyeOff size = { 18 } /> : < Eye size = { 18 } /> }
141+ </ button >
142+ </ div >
143+
144+ { errorMsg && < p className = "mt-1 text-sm text-red-600" > { errorMsg } </ p > }
145+ </ div >
146+
147+
148+ ) ;
149+
150+ return (
151+ < div
152+ className = "fixed inset-0 w-full min-h-full bg-black/60 z-50 backdrop-blur-sm flex items-center justify-center p-4"
153+ onClick = { handleOutsideClick }
154+ >
155+ < div
156+ className = "bg-bgLight rounded-lg max-h-[90%] w-full max-w-md overflow-auto animate-fadeIn"
157+ onClick = { handleFormClick }
158+ >
159+ < div className = "p-6" >
160+ < div className = "flex justify-between items-center mb-6" >
161+ < h2 className = "text-2xl font-bold text-gray-800" > Change Password</ h2 >
162+ < button
163+ onClick = { handleClose }
164+ disabled = { isLoading }
165+ className = "text-gray-500 hover:text-gray-700 transition-colors"
166+ aria-label = "Close"
167+ >
168+ < svg className = "w-6 h-6" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
169+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = "2" d = "M6 18L18 6M6 6l12 12" />
170+ </ svg >
171+ </ button >
172+ </ div >
173+
174+ { error && (
175+ < div className = "mb-4 p-3 bg-red-50 border border-red-200 text-red-700 rounded-lg flex items-center" >
176+ < svg className = "w-5 h-5 mr-2 flex-shrink-0" fill = "currentColor" viewBox = "0 0 20 20" >
177+ < path
178+ fillRule = "evenodd"
179+ d = "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zm-1 9a1 1 0 100-2 1 1 0 000 2z"
180+ clipRule = "evenodd"
181+ />
182+ </ svg >
183+ { error }
184+ </ div >
185+ ) }
186+
187+ < form onSubmit = { handleSubmit } className = "space-y-5" >
188+ { renderPasswordField (
189+ "Old Password" ,
190+ "oldPassword" ,
191+ oldPassword ,
192+ ( e ) => setOldPassword ( e . target . value ) ,
193+ errors . oldPassword ,
194+ showOld ,
195+ ( ) => setShowOld ( ( prev ) => ! prev )
196+ ) }
197+
198+ { renderPasswordField (
199+ "New Password" ,
200+ "newPassword" ,
201+ newPassword ,
202+ ( e ) => setNewPassword ( e . target . value ) ,
203+ errors . newPassword ,
204+ showNew ,
205+ ( ) => setShowNew ( ( prev ) => ! prev )
206+ ) }
207+
208+ { renderPasswordField (
209+ "Confirm Password" ,
210+ "confirmPassword" ,
211+ confirmPassword ,
212+ ( e ) => setConfirmPassword ( e . target . value ) ,
213+ errors . confirmPassword ,
214+ showConfirm ,
215+ ( ) => setShowConfirm ( ( prev ) => ! prev )
216+ ) }
217+
218+ < div className = "flex justify-end border-t border-gray-200 pt-5" >
219+ < button
220+ type = "button"
221+ onClick = { handleClose }
222+ className = "px-5 py-2 rounded-lg border border-gray-300 font-medium text-gray-700 hover:bg-gray-100 mr-3 transition-colors disabled:opacity-50"
223+ disabled = { isLoading }
224+ >
225+ Cancel
226+ </ button >
227+ < button
228+ type = "submit"
229+ className = { `px-5 py-2 rounded-lg font-medium transition-all flex items-center justify-center min-w-[120px] ${
230+ successState
231+ ? "bg-green-500 text-white"
232+ : "bg-primary text-white hover:bg-primary/90 focus:ring-2 focus:ring-primary/50"
233+ } ${ isLoading && ! successState ? "opacity-70 cursor-not-allowed" : "" } `}
234+ disabled = { isLoading }
235+ >
236+ { isLoading ? (
237+ < span className = "flex items-center" >
238+ < svg className = "animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill = "none" viewBox = "0 0 24 24" >
239+ < circle className = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" strokeWidth = "4" />
240+ < path
241+ className = "opacity-75"
242+ fill = "currentColor"
243+ d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
244+ />
245+ </ svg >
246+ Updating...
247+ </ span >
248+ ) : successState ? (
249+ < span className = "flex items-center" >
250+ < svg className = "w-5 h-5 mr-1" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
251+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = "2" d = "M5 13l4 4L19 7" />
252+ </ svg >
253+ Updated!
254+ </ span >
255+ ) : (
256+ "Update Password"
257+ ) }
258+ </ button >
259+ </ div >
260+ </ form >
261+ </ div >
262+ </ div >
263+ </ div >
264+ ) ;
265+ }
266+
267+ export default ChangePassword ;
268+
0 commit comments