Skip to content

Commit 1529351

Browse files
committed
Added Change Password option in the User Profile dashboard
1 parent 2022643 commit 1529351

File tree

5 files changed

+309
-5
lines changed

5 files changed

+309
-5
lines changed

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,17 @@
2727
"tailwindcss": "^3.4.17",
2828
"typescript": "^4.9.3",
2929
"vite": "^4.1.0"
30-
}
30+
},
31+
"main": "postcss.config.js",
32+
"repository": {
33+
"type": "git",
34+
"url": "git+https://github.com/addyuchiha/react-app.git"
35+
},
36+
"author": "",
37+
"license": "ISC",
38+
"bugs": {
39+
"url": "https://github.com/addyuchiha/react-app/issues"
40+
},
41+
"homepage": "https://github.com/addyuchiha/react-app#readme",
42+
"description": ""
3143
}

src/components/GalleryView/PicturesList.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ export default function PicturesList({
6565
return <>"loading"</>;
6666
} else {
6767
return (
68-
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-3 sm:gap-4 lg:gap-6 overflow-auto rounded-xl">
68+
<div className="flex-1 overflow-y-auto px-4 pb-4">
69+
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-3 sm:gap-4 lg:gap-6 rounded-xl">
6970
{picturesList.map((picture: Picture) => (
7071
<PictureContainer
7172
key={picture.guid}
@@ -74,6 +75,7 @@ export default function PicturesList({
7475
/>
7576
))}
7677
</div>
78+
</div>
7779
);
7880
}
7981
}
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
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+

src/components/Profile/Details.tsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,59 @@ import useUserState from "../../scripts/auth/useState";
33
import { LogOut, User2 } from "lucide-react";
44
import { useNavigate } from "react-router-dom";
55
import SubscriptionDetails from "./SubscriptionDetails";
6+
import ChangePassword from "./ChangePassword";
7+
import { useState } from "react";
68

79
export default function Details() {
810
const user = useUserState();
911
const navigate = useNavigate();
1012

13+
const [showChangePasswordDialog, setShowChangePasswordDialog] = useState(false);
14+
1115
const signOut = () => {
1216
Cookies.remove("accessToken");
1317
Cookies.remove("refreshToken");
1418
navigate("/sign-in");
1519
};
1620

21+
const forgetPassword = () => {
22+
setShowChangePasswordDialog(true);
23+
};
24+
1725
return (
18-
<div className="bg-primary p-4 rounded-xl flex flex-grow flex-col">
26+
<div className="bg-primary p-4 rounded-xl flex flex-grow flex-col relative">
1927
<div className="flex items-center h-min text-white space-x-4">
2028
<div className="rounded-full bg-accent flex items-center justify-center p-3 w-12 h-12">
2129
<User2 size={24} />
2230
</div>
2331
<div>
24-
<span className=" font-bold text-lg text-nowrap block">
32+
<span className="font-bold text-lg text-nowrap block">
2533
{user.firstName} {user.lastName}
2634
</span>
2735
<span className="font-thin">{user.email}</span>
2836
</div>
2937
</div>
38+
3039
<SubscriptionDetails />
40+
41+
<button
42+
onClick={forgetPassword}
43+
className="text-white hover:text-green-400 px-4 py-1 rounded-lg transition-colors font-medium flex justify-center items-center gap-2"
44+
>
45+
Forget Password???
46+
</button>
47+
3148
<button
3249
onClick={signOut}
33-
className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg transition-colors font-medium flex justify-center items-center gap-2"
50+
className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 mt-2 rounded-lg transition-colors font-medium flex justify-center items-center gap-2"
3451
>
3552
<LogOut size={18} />
3653
SignOut
3754
</button>
55+
56+
{showChangePasswordDialog && (
57+
<ChangePassword setState={setShowChangePasswordDialog} />
58+
)}
3859
</div>
3960
);
4061
}

0 commit comments

Comments
 (0)