|
3 | 3 | import { useState } from "react"; |
4 | 4 | import { useRouter } from "next/navigation"; |
5 | 5 | 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"; |
7 | 8 |
|
8 | 9 | export default function ForgotPasswordPage() { |
9 | 10 | const router = useRouter(); |
@@ -211,181 +212,258 @@ export default function ForgotPasswordPage() { |
211 | 212 | }; |
212 | 213 |
|
213 | 214 | 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"} |
225 | 226 | </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> |
226 | 236 |
|
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" : ""}`}> |
249 | 272 | <input |
| 273 | + id="email-otp" |
250 | 274 | type="email" |
251 | 275 | value={identifier} |
252 | 276 | onChange={(e) => setIdentifier(e.target.value)} |
253 | 277 | 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" |
255 | 279 | placeholder="Enter your registered email" |
256 | 280 | disabled={loading} |
257 | 281 | /> |
258 | 282 | </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" : ""}`}> |
275 | 306 | <input |
| 307 | + id="identifier-recovery" |
276 | 308 | type="text" |
277 | 309 | value={identifier} |
278 | 310 | onChange={(e) => setIdentifier(e.target.value)} |
279 | 311 | 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" |
281 | 313 | placeholder="Enter your username or email" |
282 | 314 | disabled={loading} |
283 | 315 | /> |
284 | 316 | </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" : ""}`}> |
287 | 323 | <input |
| 324 | + id="recovery-code" |
288 | 325 | type="text" |
289 | 326 | value={recoveryCode} |
290 | 327 | onChange={(e) => setRecoveryCode(e.target.value)} |
291 | 328 | 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" |
293 | 330 | placeholder="e.g. a1b2c3d4" |
294 | 331 | disabled={loading} |
295 | 332 | /> |
296 | 333 | </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 | + )} |
299 | 340 |
|
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" : ""}`}> |
303 | 347 | <input |
| 348 | + id="otp-code" |
304 | 349 | type="text" |
305 | 350 | value={otp} |
306 | 351 | onChange={(e) => setOtp(e.target.value.trim())} |
307 | 352 | 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" |
310 | 355 | disabled={loading} |
311 | 356 | maxLength={6} |
312 | 357 | /> |
313 | 358 | </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> |
343 | 371 | </div> |
| 372 | + )} |
344 | 373 |
|
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> |
366 | 398 | </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> |
367 | 403 |
|
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> |
381 | 429 | </div> |
382 | | - )} |
383 | 430 |
|
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} |
386 | 450 | </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> |
387 | 460 | </div> |
388 | 461 | </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> |
390 | 468 | ); |
391 | 469 | } |
0 commit comments