diff --git a/src/components/auth/BannerAlert.tsx b/src/components/auth/BannerAlert.tsx new file mode 100644 index 0000000..86aaad9 --- /dev/null +++ b/src/components/auth/BannerAlert.tsx @@ -0,0 +1,43 @@ +import { AlertCircle, CheckCircle, Info, XCircle } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface BannerAlertProps { + type: "info" | "success" | "warning" | "error"; + title?: string; + message: string; + className?: string; +} + +const icons = { + info: Info, + success: CheckCircle, + warning: AlertCircle, + error: XCircle, +}; + +const styles = { + info: "bg-accent/10 border-accent/20 text-accent", + success: "bg-success/10 border-success/20 text-success", + warning: "bg-warning/10 border-warning/20 text-warning", + error: "bg-destructive/10 border-destructive/20 text-destructive", +}; + +export function BannerAlert({ type, title, message, className }: BannerAlertProps) { + const Icon = icons[type]; + + return ( +
+ +
+ {title &&

{title}

} +

{message}

+
+
+ ); +} diff --git a/src/components/auth/PasswordStrengthMeter.tsx b/src/components/auth/PasswordStrengthMeter.tsx new file mode 100644 index 0000000..1f5a0f0 --- /dev/null +++ b/src/components/auth/PasswordStrengthMeter.tsx @@ -0,0 +1,94 @@ +import { useMemo } from "react"; +import { Check, X } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface PasswordStrengthMeterProps { + password: string; +} + +interface PasswordRequirement { + label: string; + test: (password: string) => boolean; +} + +const requirements: PasswordRequirement[] = [ + { label: "At least 8 characters", test: (p) => p.length >= 8 }, + { label: "Contains uppercase letter", test: (p) => /[A-Z]/.test(p) }, + { label: "Contains lowercase letter", test: (p) => /[a-z]/.test(p) }, + { label: "Contains number", test: (p) => /\d/.test(p) }, + { label: "Contains special character", test: (p) => /[!@#$%^&*(),.?":{}|<>]/.test(p) }, +]; + +export function PasswordStrengthMeter({ password }: PasswordStrengthMeterProps) { + const results = useMemo(() => { + return requirements.map((req) => ({ + ...req, + met: req.test(password), + })); + }, [password]); + + const strength = useMemo(() => { + const metCount = results.filter((r) => r.met).length; + if (metCount === 0) return { level: 0, label: "", color: "bg-muted" }; + if (metCount <= 2) return { level: 1, label: "Weak", color: "bg-destructive" }; + if (metCount <= 3) return { level: 2, label: "Fair", color: "bg-warning" }; + if (metCount <= 4) return { level: 3, label: "Good", color: "bg-accent" }; + return { level: 4, label: "Strong", color: "bg-success" }; + }, [results]); + + if (!password) return null; + + return ( +
+ {/* Strength bar */} +
+
+ {[1, 2, 3, 4].map((level) => ( +
+ ))} +
+ {strength.label && ( +

+ {strength.label} +

+ )} +
+ + {/* Requirements checklist */} + +
+ ); +} + +export function isPasswordValid(password: string): boolean { + return requirements.every((req) => req.test(password)); +} diff --git a/src/pages/auth/ForgotPasswordPage.tsx b/src/pages/auth/ForgotPasswordPage.tsx index 55a94bd..e83ae8a 100644 --- a/src/pages/auth/ForgotPasswordPage.tsx +++ b/src/pages/auth/ForgotPasswordPage.tsx @@ -1,9 +1,10 @@ import { useState } from "react"; import { Link } from "react-router-dom"; -import { Mail, ArrowLeft, ArrowRight } from "lucide-react"; +import { Mail, ArrowLeft, ArrowRight, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { BannerAlert } from "@/components/auth/BannerAlert"; export default function ForgotPasswordPage() { const [email, setEmail] = useState(""); @@ -13,12 +14,15 @@ export default function ForgotPasswordPage() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); + + // Mock API call - POST /api/auth/forgot-password setTimeout(() => { setIsLoading(false); setIsSubmitted(true); }, 1000); }; + // Success state - always show neutral message (don't leak account existence) if (isSubmitted) { return (
@@ -30,19 +34,40 @@ export default function ForgotPasswordPage() { Check your email

- If an account exists for {email}, you'll receive a password reset link shortly. + If an account exists for {email}, + you'll receive a password reset link shortly.

- - + + + - +
); } + // Request form return (
@@ -50,7 +75,7 @@ export default function ForgotPasswordPage() { Forgot password?

- Enter your email and we'll send you a reset link + No worries, we'll send you reset instructions

@@ -67,13 +92,21 @@ export default function ForgotPasswordPage() { onChange={(e) => setEmail(e.target.value)} className="pl-10" required + autoComplete="email" />
- + + + ); + } + + // Success state - email sent + if (state === "success") { + return ( +
+
+ +
+ +

+ Check your email +

+

+ We've sent a verification link to {email}. + Click the link to verify your account and get started. +

+ +
+ + + +
+ +

+ Didn't receive the email?{" "} + +

+
+ ); + } + + // Registration form return (
@@ -37,9 +135,17 @@ export default function RegisterPage() {

+ {error && ( + + )} +
- +
setName(e.target.value)} className="pl-10" required + autoComplete="name" />
@@ -66,6 +173,7 @@ export default function RegisterPage() { onChange={(e) => setEmail(e.target.value)} className="pl-10" required + autoComplete="email" />
@@ -82,12 +190,10 @@ export default function RegisterPage() { onChange={(e) => setPassword(e.target.value)} className="pl-10" required - minLength={8} + autoComplete="new-password" /> -

- Must be at least 8 characters -

+
@@ -102,11 +208,19 @@ export default function RegisterPage() { onChange={(e) => setConfirmPassword(e.target.value)} className="pl-10" required + autoComplete="new-password" />
+ {confirmPassword && !passwordsMatch && ( +

Passwords do not match

+ )} - + + + ); + } + + // Error state + if (state === "error") { + return ( +
+
+ +
+ +

+ Verification failed +

+

+ {errorMessage} +

+ +
+ + + + + +
+
+ ); + } + + // Resend verification form return ( -
-
- +
+
+
+ +
+ +

+ Resend verification +

+

+ Enter your email to receive a new verification link +

-

- Check your email -

-

- We've sent a verification link to your email address. Click the link to verify your account. -

+ {resendSuccess && ( + + )} -
- -
+ -

- Wrong email?{" "} - - Go back +

+ + Back to sign in