Skip to content

Commit 1bdd9d4

Browse files
Merge pull request #85 from PavanCodes05/feat/resetPasswordUI
feat: implement password reset UI flow
2 parents 1586a98 + 9e43aec commit 1bdd9d4

File tree

4 files changed

+306
-8
lines changed

4 files changed

+306
-8
lines changed

app/auth/page.js

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,19 +114,14 @@ export default function Auth() {
114114
/>
115115

116116
<div className="flex flex-wrap gap-2 justify-between mt-2 w-full px-1">
117-
<button
118-
type="button"
117+
<Link
118+
href="/auth/recover"
119119
className={`underline underline-offset-4 decoration-dashed text-foreground text-xs ${isLoading ? "pointer-events-none opacity-50" : ""}`}
120120
aria-disabled={isLoading}
121121
tabIndex={isLoading ? -1 : undefined}
122-
onClick={() => {
123-
alert(
124-
"This feature is a work in progress - Sorry for the inconvenience.",
125-
);
126-
}}
127122
>
128123
{"Recover Password"}
129-
</button>
124+
</Link>
130125
<Link
131126
className={`underline underline-offset-4 decoration-dashed text-foreground text-xs ${isLoading ? "pointer-events-none opacity-50" : ""}`}
132127
href="/auth/register"

app/auth/recover/page.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"use client";
2+
import Image from "next/image";
3+
import Link from "next/link";
4+
import { useRouter } from "next/navigation";
5+
import { useState } from "react";
6+
import { env } from "next-runtime-env";
7+
8+
export default function Recover() {
9+
const [formData, setFormData] = useState({
10+
email: "",
11+
});
12+
const [isLoading, setIsLoading] = useState(false);
13+
const router = useRouter();
14+
15+
const handleChange = (e) => {
16+
setFormData({ ...formData, [e.target.name]: e.target.value });
17+
};
18+
19+
const handleSubmit = async (e) => {
20+
e.preventDefault();
21+
setIsLoading(true);
22+
23+
const inputData = {
24+
email: formData.email,
25+
};
26+
27+
try {
28+
const res = await fetch(
29+
(env("NEXT_PUBLIC_AUTH_BASE_URL") ?? "http://localhost:5000") +
30+
"/api/password/reset",
31+
{
32+
method: "POST",
33+
headers: {
34+
"Content-Type": "application/json",
35+
},
36+
credentials: "include",
37+
body: JSON.stringify(inputData),
38+
},
39+
);
40+
41+
if (res.ok) {
42+
router.push("recover/verify?m=" + encodeURI(formData.email));
43+
} else {
44+
let errorData = {
45+
message: `Password reset failed with status: ${res.status}`,
46+
};
47+
try {
48+
errorData = await res.json();
49+
} catch (parseError) {
50+
console.error(
51+
"Could not parse error response:",
52+
parseError,
53+
);
54+
}
55+
alert(
56+
errorData.message ??
57+
"An unknown error occured during password reset.",
58+
);
59+
setIsLoading(false);
60+
}
61+
} catch (error) {
62+
console.error("Password reset request failed:", error);
63+
alert(
64+
"Failed to connect to the server. Please check your network or try again later.",
65+
);
66+
setIsLoading(false);
67+
}
68+
};
69+
70+
return (
71+
<div className="flex flex-col items-center min-h-screen font-[family-name:var(--font-geist-mono)] p-8 bg-gray-100">
72+
<div className="flex items-center justify-center overflow-hidden h-32">
73+
<Image
74+
src="/LOGO.png"
75+
alt="EVOLVE OnClick logo"
76+
height={320}
77+
width={680}
78+
/>
79+
</div>
80+
<div className="flex flex-col items-center justify-center flex-grow w-fit min-w-[32%]">
81+
<form
82+
onSubmit={handleSubmit}
83+
className="flex flex-col gap-1 p-4 w-full bg-white shadow-sm border border-dashed border-gray-200 rounded-3xl"
84+
>
85+
<div className="mb-4">
86+
<h2 className="text-xl font-semibold text-center">
87+
Recover Your Password
88+
</h2>
89+
<p className="text-xs text-gray-500 text-center">
90+
Enter your email to get started.
91+
</p>
92+
</div>
93+
<input
94+
type="email"
95+
name="email"
96+
placeholder="Email"
97+
value={formData.email}
98+
onChange={handleChange}
99+
className="w-full p-2 mt-4 border rounded-xl"
100+
required
101+
disabled={isLoading}
102+
/>
103+
<button
104+
type="submit"
105+
className={`rounded-full transition-colors flex items-center justify-center bg-yellow-400 text-black hover:bg-yellow-50 text-sm sm:text-base p-2 w-full border border-black gap-2 mt-4 ${
106+
isLoading ? "opacity-50 cursor-not-allowed" : ""
107+
}`}
108+
disabled={isLoading}
109+
>
110+
{isLoading ? "Sending OTP..." : "Send OTP"}{" "}
111+
</button>
112+
</form>
113+
<div className="flex flex-wrap gap-4 justify-center mt-8">
114+
<p className="text-gray-500 text-center">
115+
Create a new account
116+
</p>
117+
<Link
118+
className={`flex items-center gap-2 underline underline-offset-4 decoration-dashed text-foreground ${
119+
isLoading ? "pointer-events-none opacity-50" : ""
120+
}`}
121+
href="/auth/register"
122+
aria-disabled={isLoading}
123+
tabIndex={isLoading ? -1 : undefined}
124+
>
125+
Sign Up now →
126+
</Link>
127+
</div>
128+
</div>
129+
</div>
130+
);
131+
}

app/auth/recover/verify/layout.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Suspense } from "react";
2+
3+
export default function Layout({ children }) {
4+
return <Suspense fallback={<p>Loading...</p>}>{children}</Suspense>;
5+
}

app/auth/recover/verify/page.js

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
"use client";
2+
3+
import Image from "next/image";
4+
import Link from "next/link";
5+
import { useRouter, useSearchParams } from "next/navigation";
6+
import { useState } from "react";
7+
import { env } from "next-runtime-env";
8+
9+
export default function VerifyOTP() {
10+
const [formData, setFormData] = useState({
11+
otp: "",
12+
newPassword: "",
13+
confirmPassword: "",
14+
});
15+
const [isLoading, setIsLoading] = useState(false);
16+
const searchParams = useSearchParams();
17+
const email = searchParams.get("m");
18+
const router = useRouter();
19+
20+
const handleChange = (e) => {
21+
setFormData({ ...formData, [e.target.name]: e.target.value });
22+
};
23+
24+
const handleSubmit = async (e) => {
25+
e.preventDefault();
26+
27+
// Validate password match
28+
if (formData.newPassword !== formData.confirmPassword) {
29+
alert("Passwords do not match. Please try again.");
30+
return;
31+
}
32+
33+
setIsLoading(true);
34+
35+
const inputData = {
36+
email: email,
37+
otp: formData.otp,
38+
new_password: formData.newPassword,
39+
};
40+
41+
try {
42+
const response = await fetch(
43+
(env("NEXT_PUBLIC_AUTH_BASE_URL") ?? "http://localhost:5000") +
44+
"/api/password/reset/verify",
45+
{
46+
method: "POST",
47+
headers: {
48+
"Content-Type": "application/json",
49+
},
50+
credentials: "include",
51+
body: JSON.stringify(inputData),
52+
},
53+
);
54+
55+
if (response.ok) {
56+
alert("Password reset successful!");
57+
setIsLoading(false);
58+
router.push("/auth");
59+
} else {
60+
let errorData = {
61+
message: `Verification failed with status: ${response.status}`,
62+
};
63+
try {
64+
errorData = await response.json();
65+
} catch (parseError) {
66+
console.error(
67+
"Could not parse error response:",
68+
parseError,
69+
);
70+
}
71+
alert(
72+
errorData.message ?? "Invalid OTP or verification failed.",
73+
);
74+
setIsLoading(false);
75+
}
76+
} catch (error) {
77+
console.error("OTP Verification request failed:", error);
78+
alert(
79+
"Failed to connect to the server. Please check your network or try again later.",
80+
);
81+
setIsLoading(false);
82+
}
83+
};
84+
85+
return (
86+
<div className="flex flex-col items-center min-h-screen font-[family-name:var(--font-geist-mono)] p-8 bg-gray-100">
87+
<div className="flex items-center justify-center overflow-hidden h-32">
88+
<Image
89+
src="/LOGO.png"
90+
alt="EVOLVE OnClick logo"
91+
height={320}
92+
width={680}
93+
/>
94+
</div>
95+
<div className="flex flex-col flex-grow min-w-[32%] w-full md:max-w-[80%] lg:max-w-[60%] xl:max-w-[40%] mx-auto justify-center items-center align-middle">
96+
<form
97+
onSubmit={handleSubmit}
98+
className="flex flex-col gap-1 p-4 w-full bg-white shadow-sm border border-dashed border-gray-200 rounded-3xl"
99+
>
100+
<div className="mb-4">
101+
<h2 className="text-xl font-semibold text-center">
102+
Verify OTP
103+
</h2>
104+
<p className="text-xs text-gray-500 text-center break-words px-2">
105+
{`Please enter the OTP sent to ${email ?? "your email"}.`}
106+
</p>
107+
<p className="text-xs text-gray-500 text-center break-words px-2 mt-2 mx-auto justify-center items-center align-middle">
108+
OTP will be sent only if the email is registered. If
109+
you did not receive an OTP, please check the email
110+
ID you typed again and{" "}
111+
<Link
112+
href="/auth/recover"
113+
className="text-blue-600 hover:text-blue-800 underline cursor-pointer"
114+
>
115+
retry
116+
</Link>
117+
.
118+
</p>
119+
</div>
120+
<input
121+
type="text"
122+
name="otp"
123+
placeholder="Enter OTP"
124+
value={formData.otp}
125+
onChange={handleChange}
126+
className="w-full p-2 mt-4 border rounded-xl"
127+
required
128+
pattern="\d*"
129+
maxLength={6}
130+
inputMode="numeric"
131+
autoComplete="one-time-code"
132+
disabled={isLoading}
133+
/>
134+
<input
135+
type="password"
136+
name="newPassword"
137+
placeholder="Enter new password"
138+
value={formData.newPassword}
139+
onChange={handleChange}
140+
className="w-full p-2 mt-4 border rounded-xl"
141+
required
142+
disabled={isLoading}
143+
/>
144+
<input
145+
type="password"
146+
name="confirmPassword"
147+
placeholder="Confirm new password"
148+
value={formData.confirmPassword}
149+
onChange={handleChange}
150+
className="w-full p-2 mt-4 border rounded-xl"
151+
required
152+
disabled={isLoading}
153+
/>
154+
<button
155+
type="submit"
156+
className={`rounded-full transition-colors flex items-center justify-center bg-yellow-400 text-black hover:bg-yellow-50 text-sm sm:text-base p-2 w-full border border-black gap-2 mt-4 ${
157+
isLoading ? "opacity-50 cursor-not-allowed" : ""
158+
}`}
159+
disabled={isLoading}
160+
>
161+
{isLoading ? "Verifying..." : "Verify OTP"}
162+
</button>
163+
</form>
164+
</div>
165+
</div>
166+
);
167+
}

0 commit comments

Comments
 (0)