Skip to content

Commit f8e3335

Browse files
Merge pull request #255 from Atiqumer/fix/ui-forgot-password
style: update forgot password UI to match home page theme
2 parents ab2b81e + 5622d83 commit f8e3335

2 files changed

Lines changed: 224 additions & 141 deletions

File tree

savebook/app/(auth)/forgot-password/page.js

Lines changed: 207 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import { useState } from "react";
44
import { useRouter } from "next/navigation";
55
import Link from "next/link";
6-
import { Eye, EyeOff } from "lucide-react";
6+
import { Eye, EyeOff, KeyRound, Mail, ShieldAlert, Sparkles } from "lucide-react";
7+
import AuthShell from "@/components/auth/AuthShell";
78

89
export default function ForgotPasswordPage() {
910
const router = useRouter();
@@ -211,181 +212,258 @@ export default function ForgotPasswordPage() {
211212
};
212213

213214
return (
214-
<div className="min-h-screen bg-gradient-to-br from-gray-900 to-blue-900 flex items-center justify-center p-4">
215-
<div className="max-w-md w-full">
216-
<div className="text-center mb-8">
217-
<h2 className="text-3xl font-extrabold text-white">
218-
{step === 1 ? "Reset Password" : "Set New Password"}
219-
</h2>
220-
<p className="mt-2 text-sm text-gray-300">
221-
{step === 1
222-
? "Choose a method to reset your password"
223-
: "Enter the OTP sent to your email and your new password"}
224-
</p>
215+
<AuthShell
216+
eyebrow="Password Reset"
217+
title="Regain access to your notes and continue where you left off."
218+
description="Reset your password using email verification or a recovery code. Your SaveBook workspace will be ready once you're back in."
219+
asideTitle="Recovery options"
220+
asideCopy="Choose email verification for a quick reset, or use your recovery code to preserve your encrypted notes and maintain full access to your content."
221+
>
222+
<div className="mb-8">
223+
<div className="inline-flex items-center gap-2 rounded-full bg-[color:var(--background)]/80 px-3 py-1 text-xs font-semibold uppercase tracking-[0.24em] text-[color:var(--accent-2)]">
224+
<ShieldAlert className="h-3.5 w-3.5" />
225+
{step === 1 ? "Reset password" : "Set new password"}
225226
</div>
227+
<h2 className="mt-5 text-3xl font-semibold text-[color:var(--foreground)]">
228+
{step === 1 ? "Choose recovery method" : "Create new password"}
229+
</h2>
230+
<p className="mt-3 text-sm leading-7 text-[color:var(--muted)]">
231+
{step === 1
232+
? "Select how you'd like to reset your password and regain access to your account."
233+
: "Enter the verification code and set a new password for your SaveBook account."}
234+
</p>
235+
</div>
226236

227-
<div className="bg-gray-800 p-8 rounded-2xl shadow-xl border border-gray-700">
228-
{step === 1 && (
229-
<div className="mb-6 flex space-x-2 p-1 bg-gray-900 rounded-lg">
230-
<button
231-
onClick={() => { setMethod("otp"); setErrorType(""); setMessage(""); }}
232-
className={`flex-1 py-2 text-sm font-medium rounded-md transition-colors ${method === "otp" ? "bg-blue-600 text-white shadow-sm" : "text-gray-400 hover:text-gray-200"}`}
233-
>
234-
Get OTP via Email
235-
</button>
236-
<button
237-
onClick={() => { setMethod("recoveryCode"); setErrorType(""); setMessage(""); }}
238-
className={`flex-1 py-2 text-sm font-medium rounded-md transition-colors ${method === "recoveryCode" ? "bg-blue-600 text-white shadow-sm" : "text-gray-400 hover:text-gray-200"}`}
239-
>
240-
Use Recovery Code
241-
</button>
242-
</div>
243-
)}
244-
245-
{step === 1 && method === "otp" && (
246-
<form onSubmit={handleRequestOTP} className="space-y-6">
247-
<div>
248-
<label className="block text-sm font-medium text-gray-300 mb-2">Email Address</label>
237+
<div className="rounded-[2rem] border border-[var(--border)] bg-[color:var(--background)]/55 p-6 md:p-7">
238+
{step === 1 && (
239+
<div className="mb-6 flex gap-2 rounded-[1.2rem] border border-[var(--border)] bg-[color:var(--background)]/70 p-1">
240+
<button
241+
onClick={() => { setMethod("otp"); setErrorType(""); setMessage(""); }}
242+
className={`flex flex-1 items-center justify-center gap-2 rounded-[0.9rem] px-4 py-2.5 text-sm font-medium transition-all ${
243+
method === "otp"
244+
? "bg-gradient-to-r from-[color:var(--accent)] to-[color:var(--accent-3)] text-[color:var(--button-text)] shadow-md"
245+
: "text-[color:var(--muted)] hover:text-[color:var(--foreground)]"
246+
}`}
247+
>
248+
<Mail className="h-4 w-4" />
249+
Email OTP
250+
</button>
251+
<button
252+
onClick={() => { setMethod("recoveryCode"); setErrorType(""); setMessage(""); }}
253+
className={`flex flex-1 items-center justify-center gap-2 rounded-[0.9rem] px-4 py-2.5 text-sm font-medium transition-all ${
254+
method === "recoveryCode"
255+
? "bg-gradient-to-r from-[color:var(--accent)] to-[color:var(--accent-3)] text-[color:var(--button-text)] shadow-md"
256+
: "text-[color:var(--muted)] hover:text-[color:var(--foreground)]"
257+
}`}
258+
>
259+
<KeyRound className="h-4 w-4" />
260+
Recovery Code
261+
</button>
262+
</div>
263+
)}
264+
265+
{step === 1 && method === "otp" && (
266+
<form onSubmit={handleRequestOTP} className="space-y-6">
267+
<div>
268+
<label htmlFor="email-otp" className="mb-2 block text-sm font-medium text-[color:var(--foreground)]">
269+
Email Address
270+
</label>
271+
<div className={`field-shell px-4 py-3 ${errorType === "identifier" ? "!border-red-500" : ""}`}>
249272
<input
273+
id="email-otp"
250274
type="email"
251275
value={identifier}
252276
onChange={(e) => setIdentifier(e.target.value)}
253277
required
254-
className={`w-full px-4 py-3 bg-gray-700 border rounded-lg text-white focus:ring-2 focus:ring-blue-500 outline-none transition-colors ${errorType === "identifier" ? "border-red-500 focus:border-red-500" : "border-gray-600"}`}
278+
className="field-input"
255279
placeholder="Enter your registered email"
256280
disabled={loading}
257281
/>
258282
</div>
259-
<button
260-
type="submit"
261-
disabled={loading}
262-
className="w-full bg-gradient-to-r from-blue-600 to-purple-700 text-white py-3 rounded-lg font-medium hover:from-blue-700 hover:to-purple-800 transition disabled:opacity-50 flex justify-center items-center"
263-
>
264-
{loading ? "Sending OTP..." : "Send OTP"}
265-
</button>
266-
</form>
267-
)}
268-
269-
{(step === 2 || (step === 1 && method === "recoveryCode")) && (
270-
<form onSubmit={handleResetPassword} className="space-y-5">
271-
{method === "recoveryCode" && (
272-
<>
273-
<div>
274-
<label className="block text-sm font-medium text-gray-300 mb-2">Username or Email</label>
283+
<p className="mt-2 text-xs text-[color:var(--muted)]">
284+
We'll send a one-time password to this email address.
285+
</p>
286+
</div>
287+
<button
288+
type="submit"
289+
disabled={loading}
290+
className="site-button w-full disabled:opacity-60"
291+
>
292+
{loading ? "Sending OTP..." : "Send OTP"}
293+
</button>
294+
</form>
295+
)}
296+
297+
{(step === 2 || (step === 1 && method === "recoveryCode")) && (
298+
<form onSubmit={handleResetPassword} className="space-y-6">
299+
{method === "recoveryCode" && (
300+
<>
301+
<div>
302+
<label htmlFor="identifier-recovery" className="mb-2 block text-sm font-medium text-[color:var(--foreground)]">
303+
Username or Email
304+
</label>
305+
<div className={`field-shell px-4 py-3 ${errorType === "identifier" ? "!border-red-500" : ""}`}>
275306
<input
307+
id="identifier-recovery"
276308
type="text"
277309
value={identifier}
278310
onChange={(e) => setIdentifier(e.target.value)}
279311
required
280-
className={`w-full px-4 py-3 bg-gray-700 border rounded-lg text-white focus:ring-2 focus:ring-blue-500 outline-none transition-colors ${errorType === "identifier" ? "border-red-500 focus:border-red-500" : "border-gray-600"}`}
312+
className="field-input"
281313
placeholder="Enter your username or email"
282314
disabled={loading}
283315
/>
284316
</div>
285-
<div>
286-
<label className="block text-sm font-medium text-gray-300 mb-2">Recovery Code</label>
317+
</div>
318+
<div>
319+
<label htmlFor="recovery-code" className="mb-2 block text-sm font-medium text-[color:var(--foreground)]">
320+
Recovery Code
321+
</label>
322+
<div className={`field-shell px-4 py-3 ${errorType === "recoveryCode" ? "!border-red-500" : ""}`}>
287323
<input
324+
id="recovery-code"
288325
type="text"
289326
value={recoveryCode}
290327
onChange={(e) => setRecoveryCode(e.target.value)}
291328
required
292-
className={`w-full px-4 py-3 bg-gray-700 border rounded-lg text-white font-mono tracking-wider focus:ring-2 focus:ring-blue-500 outline-none transition-colors ${errorType === "recoveryCode" ? "border-red-500 focus:border-red-500" : "border-gray-600"}`}
329+
className="field-input font-mono tracking-wider"
293330
placeholder="e.g. a1b2c3d4"
294331
disabled={loading}
295332
/>
296333
</div>
297-
</>
298-
)}
334+
<p className="mt-2 text-xs text-[color:var(--muted)]">
335+
Enter one of the recovery codes you saved when creating your account.
336+
</p>
337+
</div>
338+
</>
339+
)}
299340

300-
{method === "otp" && step === 2 && (
301-
<div>
302-
<label className="block text-sm font-medium text-gray-300 mb-2">One-Time Password (OTP)</label>
341+
{method === "otp" && step === 2 && (
342+
<div>
343+
<label htmlFor="otp-code" className="mb-2 block text-sm font-medium text-[color:var(--foreground)]">
344+
One-Time Password (OTP)
345+
</label>
346+
<div className={`field-shell px-4 py-3 ${errorType === "otp" ? "!border-red-500" : ""}`}>
303347
<input
348+
id="otp-code"
304349
type="text"
305350
value={otp}
306351
onChange={(e) => setOtp(e.target.value.trim())}
307352
required
308-
className={`w-full px-4 py-3 bg-gray-700 border rounded-lg text-white font-mono tracking-wider focus:ring-2 focus:ring-blue-500 outline-none transition-colors ${errorType === "otp" ? "border-red-500 focus:border-red-500" : "border-gray-600"}`}
309-
placeholder="Enter 6-digit OTP"
353+
className="field-input font-mono tracking-wider text-center text-lg"
354+
placeholder="000000"
310355
disabled={loading}
311356
maxLength={6}
312357
/>
313358
</div>
314-
)}
315-
316-
{method === "otp" && step === 2 && (
317-
<div className="p-3 rounded-lg text-sm bg-yellow-900/30 text-yellow-400 border border-yellow-800">
318-
⚠️ Resetting via OTP will permanently delete all your encrypted notes. Use a recovery code to keep them.
319-
</div>
320-
)}
321-
322-
<div>
323-
<label className="block text-sm font-medium text-gray-300 mb-2">New Password</label>
324-
<div className="relative">
325-
<input
326-
type={showPassword ? "text" : "password"}
327-
value={password}
328-
onChange={(e) => setPassword(e.target.value)}
329-
required
330-
className={`w-full px-4 py-3 bg-gray-700 border rounded-lg text-white focus:ring-2 focus:ring-blue-500 outline-none pr-10 transition-colors ${errorType === "password" ? "border-red-500 focus:border-red-500" : "border-gray-600"}`}
331-
placeholder="Enter new password"
332-
disabled={loading}
333-
minLength={6}
334-
/>
335-
<button
336-
type="button"
337-
onClick={() => setShowPassword(!showPassword)}
338-
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-200 focus:outline-none"
339-
>
340-
{showPassword ? <EyeOff size={20} /> : <Eye size={20} />}
341-
</button>
342-
</div>
359+
<p className="mt-2 text-xs text-[color:var(--muted)]">
360+
Check your email for the 6-digit verification code.
361+
</p>
362+
</div>
363+
)}
364+
365+
{method === "otp" && step === 2 && (
366+
<div className="flex items-start gap-3 rounded-[1.2rem] border border-yellow-500/30 bg-yellow-500/10 px-4 py-3 text-sm text-yellow-600 dark:text-yellow-400">
367+
<ShieldAlert className="mt-0.5 h-5 w-5 flex-shrink-0" />
368+
<p>
369+
<strong>Warning:</strong> Resetting via OTP will permanently delete all your encrypted notes. Use a recovery code to preserve them.
370+
</p>
343371
</div>
372+
)}
344373

345-
<div>
346-
<label className="block text-sm font-medium text-gray-300 mb-2">Confirm Password</label>
347-
<div className="relative">
348-
<input
349-
type={showConfirmPassword ? "text" : "password"}
350-
value={confirmPassword}
351-
onChange={(e) => setConfirmPassword(e.target.value)}
352-
required
353-
className={`w-full px-4 py-3 bg-gray-700 border rounded-lg text-white focus:ring-2 focus:ring-blue-500 outline-none pr-10 transition-colors ${errorType === "confirmPassword" ? "border-red-500 focus:border-red-500" : "border-gray-600"}`}
354-
placeholder="Confirm new password"
355-
disabled={loading}
356-
minLength={6}
357-
/>
358-
<button
359-
type="button"
360-
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
361-
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-200 focus:outline-none"
362-
>
363-
{showConfirmPassword ? <EyeOff size={20} /> : <Eye size={20} />}
364-
</button>
365-
</div>
374+
<div>
375+
<label htmlFor="new-password" className="mb-2 block text-sm font-medium text-[color:var(--foreground)]">
376+
New Password
377+
</label>
378+
<div className={`field-shell relative px-4 py-3 ${errorType === "password" ? "!border-red-500" : ""}`}>
379+
<input
380+
id="new-password"
381+
type={showPassword ? "text" : "password"}
382+
value={password}
383+
onChange={(e) => setPassword(e.target.value)}
384+
required
385+
className="field-input pr-10"
386+
placeholder="Enter new password"
387+
disabled={loading}
388+
minLength={6}
389+
/>
390+
<button
391+
type="button"
392+
onClick={() => setShowPassword(!showPassword)}
393+
className="absolute right-3 top-1/2 -translate-y-1/2 rounded-full p-1 text-[color:var(--muted)] hover:text-[color:var(--foreground)]"
394+
aria-label={showPassword ? "Hide password" : "Show password"}
395+
>
396+
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
397+
</button>
366398
</div>
399+
<p className="mt-2 text-xs text-[color:var(--muted)]">
400+
Password must be at least 6 characters long.
401+
</p>
402+
</div>
367403

368-
<button
369-
type="submit"
370-
disabled={loading}
371-
className="w-full bg-gradient-to-r from-blue-600 to-purple-700 text-white py-3 rounded-lg font-medium hover:from-blue-700 hover:to-purple-800 transition disabled:opacity-50 flex justify-center items-center mt-4"
372-
>
373-
{loading ? "Processing..." : "Reset Password"}
374-
</button>
375-
</form>
376-
)}
377-
378-
{message && (
379-
<div className={`mt-4 p-3 rounded-lg text-sm text-center ${errorType === "success" ? "bg-green-900/30 text-green-400 border border-green-800" : "bg-red-900/30 text-red-400 border border-red-800"}`}>
380-
{message}
404+
<div>
405+
<label htmlFor="confirm-password" className="mb-2 block text-sm font-medium text-[color:var(--foreground)]">
406+
Confirm Password
407+
</label>
408+
<div className={`field-shell relative px-4 py-3 ${errorType === "confirmPassword" ? "!border-red-500" : ""}`}>
409+
<input
410+
id="confirm-password"
411+
type={showConfirmPassword ? "text" : "password"}
412+
value={confirmPassword}
413+
onChange={(e) => setConfirmPassword(e.target.value)}
414+
required
415+
className="field-input pr-10"
416+
placeholder="Confirm new password"
417+
disabled={loading}
418+
minLength={6}
419+
/>
420+
<button
421+
type="button"
422+
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
423+
className="absolute right-3 top-1/2 -translate-y-1/2 rounded-full p-1 text-[color:var(--muted)] hover:text-[color:var(--foreground)]"
424+
aria-label={showConfirmPassword ? "Hide password" : "Show password"}
425+
>
426+
{showConfirmPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
427+
</button>
428+
</div>
381429
</div>
382-
)}
383430

384-
<div className="text-center mt-6">
385-
<Link href="/login" className="text-sm text-blue-400 hover:text-blue-300 transition-colors">Back to Login</Link>
431+
<button
432+
type="submit"
433+
disabled={loading}
434+
className="site-button w-full disabled:opacity-60"
435+
>
436+
{loading ? "Processing..." : "Reset Password"}
437+
</button>
438+
</form>
439+
)}
440+
441+
{message && (
442+
<div
443+
className={`mt-6 rounded-[1.2rem] border px-4 py-3 text-center text-sm ${
444+
errorType === "success"
445+
? "border-green-500/30 bg-green-500/10 text-green-600 dark:text-green-400"
446+
: "border-red-500/30 bg-red-500/10 text-red-600 dark:text-red-400"
447+
}`}
448+
>
449+
{message}
386450
</div>
451+
)}
452+
453+
<div className="mt-6 text-center">
454+
<Link
455+
href="/login"
456+
className="text-sm text-[color:var(--accent)] hover:opacity-80 transition-opacity"
457+
>
458+
Back to Login
459+
</Link>
387460
</div>
388461
</div>
389-
</div>
462+
463+
<div className="mt-6 flex items-center gap-3 rounded-[1.5rem] border border-[var(--border)] bg-[color:var(--background)]/45 px-4 py-3 text-sm text-[color:var(--muted)]">
464+
<Sparkles className="h-4 w-4 text-[color:var(--accent)]" />
465+
Your notes are waiting. Reset your password to continue.
466+
</div>
467+
</AuthShell>
390468
);
391469
}

0 commit comments

Comments
 (0)