Skip to content

Commit 39b8604

Browse files
committed
refactor(auth-ui): modularize auth pages and unify layout, fields, and banners
1 parent 1a8f5d4 commit 39b8604

18 files changed

Lines changed: 744 additions & 652 deletions
Lines changed: 3 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,123 +1,7 @@
11
"use client";
2-
import { Link } from "@/i18n/routing";
3-
import { useState } from "react";
4-
import { useSearchParams } from "next/navigation";
5-
import { Button } from "@/components/ui/button";
62

7-
export default function ForgotPasswordPage() {
8-
const searchParams = useSearchParams();
9-
const returnTo = searchParams.get("returnTo");
10-
11-
const [loading, setLoading] = useState(false);
12-
const [email, setEmail] = useState("");
13-
const [submitted, setSubmitted] = useState(false);
14-
const [error, setError] = useState<string | null>(null);
15-
16-
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
17-
e.preventDefault();
18-
setLoading(true);
19-
setError(null);
20-
21-
try {
22-
const res = await fetch("/api/auth/password-reset", {
23-
method: "POST",
24-
headers: { "Content-Type": "application/json" },
25-
body: JSON.stringify({ email }),
26-
});
27-
28-
if (!res.ok) {
29-
setError("Something went wrong. Please try again.");
30-
return;
31-
}
32-
33-
setSubmitted(true);
34-
} catch (err) {
35-
console.error("Password reset request failed:", err);
36-
setError("Network error. Please check your connection and try again.");
37-
} finally {
38-
setLoading(false);
39-
}
40-
}
41-
42-
return (
43-
<div className="mx-auto max-w-sm py-12">
44-
<h1 className="mb-6 text-2xl font-semibold">
45-
Forgot password
46-
</h1>
3+
import { ForgotPasswordForm } from "@/components/auth/ForgotPasswordForm";
474

48-
{submitted ? (
49-
<div className="rounded-md border border-green-400 bg-green-50 p-4 text-sm text-green-800">
50-
<p>
51-
If an account for{" "}
52-
<strong>{email}</strong> exists, we’ve sent a
53-
password reset link.
54-
</p>
55-
56-
<p className="mt-2">
57-
Please check your inbox and follow the
58-
instructions to reset your password.
59-
</p>
60-
61-
<Link
62-
href={
63-
returnTo
64-
? `/login?returnTo=${encodeURIComponent(returnTo)}`
65-
: "/login"
66-
}
67-
className="mt-4 inline-block underline"
68-
>
69-
Back to login
70-
</Link>
71-
</div>
72-
) : (
73-
<form onSubmit={onSubmit} className="space-y-4">
74-
<p className="text-sm text-gray-600">
75-
Enter your email address and we’ll send
76-
you a link to reset your password.
77-
</p>
78-
79-
<input
80-
type="email"
81-
required
82-
placeholder="Email"
83-
value={email}
84-
onChange={e => setEmail(e.target.value)}
85-
className="w-full rounded border px-3 py-2"
86-
/>
87-
88-
{error && (
89-
<p className="text-sm text-red-600">
90-
{error}
91-
</p>
92-
)}
93-
94-
<Button
95-
type="submit"
96-
disabled={loading}
97-
className="w-full"
98-
>
99-
{loading
100-
? "Sending reset link..."
101-
: "Send reset link"}
102-
</Button>
103-
</form>
104-
)}
105-
106-
{!submitted && (
107-
<p className="mt-4 text-sm text-gray-600">
108-
Remembered your password?{" "}
109-
<Link
110-
href={
111-
returnTo
112-
? `/login?returnTo=${encodeURIComponent(returnTo)}`
113-
: "/login"
114-
}
115-
className="underline"
116-
>
117-
Log in
118-
</Link>
119-
</p>
120-
)}
121-
</div>
122-
);
5+
export default function ForgotPasswordPage() {
6+
return <ForgotPasswordForm />;
1237
}
Lines changed: 10 additions & 207 deletions
Original file line numberDiff line numberDiff line change
@@ -1,219 +1,22 @@
11
"use client";
22

33
import { useLocale } from "next-intl";
4-
import { Link } from "@/i18n/routing";
5-
import { useState } from "react";
64
import { useSearchParams } from "next/navigation";
7-
import { Button } from "@/components/ui/button";
8-
import { OAuthButtons } from "@/components/auth/OAuthButtons";
9-
10-
function isSafeRedirectUrl(url: string): boolean {
11-
if (!url.startsWith("/")) return false;
12-
if (url.startsWith("//")) return false;
13-
if (url.includes("://")) return false;
14-
return true;
15-
}
5+
import { LoginForm } from "@/components/auth/LoginForm";
6+
import { getSafeRedirect } from "@/lib/auth/safe-redirect";
167

178
export default function LoginPage() {
18-
const searchParams = useSearchParams();
199
const locale = useLocale();
10+
const searchParams = useSearchParams();
2011

21-
const returnToParam = searchParams.get("returnTo");
22-
const returnTo = returnToParam ?? "";
23-
24-
const [loading, setLoading] = useState(false);
25-
const [errorMessage, setErrorMessage] =
26-
useState<string | null>(null);
27-
const [errorCode, setErrorCode] =
28-
useState<string | null>(null);
29-
const [email, setEmail] = useState("");
30-
const [verificationSent, setVerificationSent] =
31-
useState(false);
32-
const [showPassword, setShowPassword] =
33-
useState(false);
34-
35-
async function onSubmit(
36-
e: React.FormEvent<HTMLFormElement>
37-
) {
38-
e.preventDefault();
39-
setLoading(true);
40-
setErrorMessage(null);
41-
setErrorCode(null);
42-
setVerificationSent(false);
43-
44-
const formData = new FormData(e.currentTarget);
45-
const emailValue = String(formData.get("email") || "");
46-
setEmail(emailValue);
47-
48-
try {
49-
const res = await fetch("/api/auth/login", {
50-
method: "POST",
51-
headers: { "Content-Type": "application/json" },
52-
body: JSON.stringify({
53-
email: emailValue,
54-
password: formData.get("password"),
55-
}),
56-
});
57-
58-
const data = await res.json().catch(() => null);
59-
60-
if (!res.ok) {
61-
setErrorCode(data?.code ?? null);
62-
63-
if (data?.code === "EMAIL_NOT_VERIFIED") {
64-
setErrorMessage(
65-
"Your email address is not verified. Please check your inbox."
66-
);
67-
} else {
68-
setErrorMessage("Invalid email or password");
69-
}
70-
return;
71-
}
72-
73-
const redirectTarget =
74-
returnTo && isSafeRedirectUrl(returnTo)
75-
? returnTo
76-
: `/${locale}/dashboard`;
77-
78-
window.location.href = redirectTarget;
79-
} catch (err) {
80-
console.error("Login request failed:", err);
81-
setErrorMessage(
82-
"Network error. Please check your connection and try again."
83-
);
84-
setErrorCode(null);
85-
} finally {
86-
setLoading(false);
87-
}
88-
}
89-
90-
async function resendVerification() {
91-
if (!email) return;
92-
93-
await fetch("/api/auth/resend-verification", {
94-
method: "POST",
95-
headers: { "Content-Type": "application/json" },
96-
body: JSON.stringify({ email }),
97-
});
98-
99-
setVerificationSent(true);
100-
setErrorCode(null);
101-
setErrorMessage(null);
102-
}
12+
const returnTo = getSafeRedirect(
13+
searchParams.get("returnTo")
14+
);
10315

10416
return (
105-
<div className="mx-auto max-w-sm py-12">
106-
<h1 className="mb-6 text-2xl font-semibold">
107-
Log in
108-
</h1>
109-
110-
<OAuthButtons />
111-
112-
<div className="my-4 flex items-center gap-3">
113-
<div className="h-px flex-1 bg-gray-200" />
114-
<span className="text-xs text-gray-500">
115-
or
116-
</span>
117-
<div className="h-px flex-1 bg-gray-200" />
118-
</div>
119-
120-
<form onSubmit={onSubmit} className="space-y-4">
121-
<input
122-
name="email"
123-
type="email"
124-
placeholder="Email"
125-
required
126-
className="w-full rounded border px-3 py-2"
127-
onChange={e => setEmail(e.target.value)}
128-
/>
129-
130-
<div className="relative">
131-
<input
132-
name="password"
133-
type={showPassword ? "text" : "password"}
134-
placeholder="Password"
135-
required
136-
className="w-full rounded border px-3 py-2 pr-10"
137-
/>
138-
139-
<button
140-
type="button"
141-
aria-label={
142-
showPassword
143-
? "Hide password"
144-
: "Show password"
145-
}
146-
onClick={() =>
147-
setShowPassword(v => !v)
148-
}
149-
className="absolute inset-y-0 right-2 flex items-center text-sm text-gray-500"
150-
>
151-
{showPassword ? "Hide" : "Show"}
152-
</button>
153-
</div>
154-
155-
<div className="text-right">
156-
<Link
157-
href={
158-
returnTo
159-
? `/forgot-password?returnTo=${encodeURIComponent(
160-
returnTo
161-
)}`
162-
: "/forgot-password"
163-
}
164-
className="text-sm underline text-gray-600"
165-
>
166-
Forgot password?
167-
</Link>
168-
</div>
169-
170-
{errorMessage && !verificationSent && (
171-
<div className="rounded-md border border-yellow-400 bg-yellow-50 p-3 text-sm text-yellow-800">
172-
<p>{errorMessage}</p>
173-
174-
{errorCode === "EMAIL_NOT_VERIFIED" && (
175-
<button
176-
type="button"
177-
onClick={resendVerification}
178-
className="mt-2 underline"
179-
>
180-
Resend verification email
181-
</button>
182-
)}
183-
</div>
184-
)}
185-
186-
{verificationSent && (
187-
<div className="rounded-md border border-green-400 bg-green-50 p-3 text-sm text-green-800">
188-
Verification successfully sent to{" "}
189-
<strong>{email}</strong>
190-
</div>
191-
)}
192-
193-
<Button
194-
type="submit"
195-
disabled={loading}
196-
className="w-full"
197-
>
198-
{loading ? "Logging in..." : "Log in"}
199-
</Button>
200-
</form>
201-
202-
<p className="mt-4 text-sm text-gray-600">
203-
Don’t have an account?{" "}
204-
<Link
205-
href={
206-
returnTo
207-
? `/signup?returnTo=${encodeURIComponent(
208-
returnTo
209-
)}`
210-
: "/signup"
211-
}
212-
className="underline"
213-
>
214-
Sign up
215-
</Link>
216-
</p>
217-
</div>
17+
<LoginForm
18+
locale={locale}
19+
returnTo={returnTo}
20+
/>
21821
);
21922
}

0 commit comments

Comments
 (0)