feat/authentication #21

Merged
Hammond merged 12 commits from feat/authentication into master 2025-11-24 22:09:51 +00:00
2 changed files with 51 additions and 47 deletions
Showing only changes of commit eec3976f94 - Show all commits

View File

@ -273,12 +273,13 @@ export default function Login() {
try { try {
const result = await verifyOtp(otpToVerify); const result = await verifyOtp(otpToVerify);
// If verification is successful, redirect to login page // If verification is successful, switch to login step
toast.success("Email verified successfully! Redirecting to login..."); toast.success("Email verified successfully! You can now login.");
// Redirect to login page with email pre-filled // Switch to login step and pre-fill email
setTimeout(() => { setStep("login");
router.push(`/login?email=${encodeURIComponent(emailToVerify)}`); setLoginData(prev => ({ ...prev, email: emailToVerify }));
}, 1000); setOtpData({ email: "", otp: "" });
setRegisteredEmail("");
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : "OTP verification failed. Please try again."; const errorMessage = error instanceof Error ? error.message : "OTP verification failed. Please try again.";
toast.error(errorMessage); toast.error(errorMessage);

View File

@ -134,15 +134,15 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent <DialogContent
showCloseButton={false} showCloseButton={false}
className={`sm:max-w-md ${isDark ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}`} className={`max-w-md max-h-[90vh] overflow-hidden flex flex-col p-0 ${isDark ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}`}
> >
{/* Header with Close Button */} {/* Header with Close Button - Fixed */}
<div className="flex items-start justify-between mb-2"> <div className="flex items-start justify-between p-6 pb-4 flex-shrink-0 border-b border-gray-200 dark:border-gray-700">
<DialogHeader className="flex-1"> <DialogHeader className="flex-1 pr-2">
<DialogTitle className="text-3xl font-bold bg-gradient-to-r from-rose-600 via-pink-600 to-rose-600 bg-clip-text text-transparent"> <DialogTitle className="text-2xl sm:text-3xl font-bold bg-gradient-to-r from-rose-600 via-pink-600 to-rose-600 bg-clip-text text-transparent">
{isSignup ? "Create an account" : "Welcome back"} {isSignup ? "Create an account" : "Welcome back"}
</DialogTitle> </DialogTitle>
<DialogDescription className={isDark ? 'text-gray-400' : 'text-gray-600'}> <DialogDescription className={`text-sm sm:text-base mt-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
{isSignup {isSignup
? "Sign up to complete your booking" ? "Sign up to complete your booking"
: "Please log in to complete your booking"} : "Please log in to complete your booking"}
@ -158,9 +158,11 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
</button> </button>
</div> </div>
{/* Scrollable Content */}
<div className="overflow-y-auto flex-1 px-6">
{/* Signup Form */} {/* Signup Form */}
{isSignup ? ( {isSignup ? (
<form className="space-y-6 mt-4" onSubmit={handleSignup}> <form className="space-y-4 sm:space-y-5 py-4 sm:py-6" onSubmit={handleSignup}>
{error && ( {error && (
<div className={`p-3 rounded-lg border ${isDark ? 'bg-red-900/20 border-red-800' : 'bg-red-50 border-red-200'}`}> <div className={`p-3 rounded-lg border ${isDark ? 'bg-red-900/20 border-red-800' : 'bg-red-50 border-red-200'}`}>
<p className={`text-sm ${isDark ? 'text-red-200' : 'text-red-800'}`}>{error}</p> <p className={`text-sm ${isDark ? 'text-red-200' : 'text-red-800'}`}>{error}</p>
@ -168,7 +170,7 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
)} )}
{/* First Name Field */} {/* First Name Field */}
<div className="space-y-2"> <div className="space-y-1.5 sm:space-y-2">
<label htmlFor="signup-firstName" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}> <label htmlFor="signup-firstName" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
First Name * First Name *
</label> </label>
@ -178,13 +180,13 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
placeholder="John" placeholder="John"
value={signupData.first_name} value={signupData.first_name}
onChange={(e) => setSignupData({ ...signupData, first_name: e.target.value })} onChange={(e) => setSignupData({ ...signupData, first_name: e.target.value })}
className={`h-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`} className={`h-11 sm:h-12 text-sm sm:text-base ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
required required
/> />
</div> </div>
{/* Last Name Field */} {/* Last Name Field */}
<div className="space-y-2"> <div className="space-y-1.5 sm:space-y-2">
<label htmlFor="signup-lastName" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}> <label htmlFor="signup-lastName" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Last Name * Last Name *
</label> </label>
@ -194,13 +196,13 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
placeholder="Doe" placeholder="Doe"
value={signupData.last_name} value={signupData.last_name}
onChange={(e) => setSignupData({ ...signupData, last_name: e.target.value })} onChange={(e) => setSignupData({ ...signupData, last_name: e.target.value })}
className={`h-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`} className={`h-11 sm:h-12 text-sm sm:text-base ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
required required
/> />
</div> </div>
{/* Email Field */} {/* Email Field */}
<div className="space-y-2"> <div className="space-y-1.5 sm:space-y-2">
<label htmlFor="signup-email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}> <label htmlFor="signup-email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Email address * Email address *
</label> </label>
@ -210,13 +212,13 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
placeholder="Email address" placeholder="Email address"
value={signupData.email} value={signupData.email}
onChange={(e) => setSignupData({ ...signupData, email: e.target.value })} onChange={(e) => setSignupData({ ...signupData, email: e.target.value })}
className={`h-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`} className={`h-11 sm:h-12 text-sm sm:text-base ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
required required
/> />
</div> </div>
{/* Phone Field */} {/* Phone Field */}
<div className="space-y-2"> <div className="space-y-1.5 sm:space-y-2">
<label htmlFor="signup-phone" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}> <label htmlFor="signup-phone" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Phone Number (Optional) Phone Number (Optional)
</label> </label>
@ -226,12 +228,12 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
placeholder="+1 (555) 123-4567" placeholder="+1 (555) 123-4567"
value={signupData.phone_number || ""} value={signupData.phone_number || ""}
onChange={(e) => setSignupData({ ...signupData, phone_number: e.target.value })} onChange={(e) => setSignupData({ ...signupData, phone_number: e.target.value })}
className={`h-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`} className={`h-11 sm:h-12 text-sm sm:text-base ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
/> />
</div> </div>
{/* Password Field */} {/* Password Field */}
<div className="space-y-2"> <div className="space-y-1.5 sm:space-y-2">
<label htmlFor="signup-password" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}> <label htmlFor="signup-password" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Password * Password *
</label> </label>
@ -242,7 +244,7 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
placeholder="Password (min 8 characters)" placeholder="Password (min 8 characters)"
value={signupData.password} value={signupData.password}
onChange={(e) => setSignupData({ ...signupData, password: e.target.value })} onChange={(e) => setSignupData({ ...signupData, password: e.target.value })}
className={`h-12 pr-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`} className={`h-11 sm:h-12 pr-12 text-sm sm:text-base ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
required required
/> />
<Button <Button
@ -250,20 +252,20 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
className={`absolute right-4 top-1/2 -translate-y-1/2 h-auto w-auto p-0 ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-500 hover:text-gray-700'}`} className={`absolute right-3 sm:right-4 top-1/2 -translate-y-1/2 h-auto w-auto p-0 ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-500 hover:text-gray-700'}`}
aria-label={showPassword ? "Hide password" : "Show password"} aria-label={showPassword ? "Hide password" : "Show password"}
> >
{showPassword ? ( {showPassword ? (
<EyeOff className="w-5 h-5" /> <EyeOff className="w-4 h-4 sm:w-5 sm:h-5" />
) : ( ) : (
<Eye className="w-5 h-5" /> <Eye className="w-4 h-4 sm:w-5 sm:h-5" />
)} )}
</Button> </Button>
</div> </div>
</div> </div>
{/* Confirm Password Field */} {/* Confirm Password Field */}
<div className="space-y-2"> <div className="space-y-1.5 sm:space-y-2">
<label htmlFor="signup-password2" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}> <label htmlFor="signup-password2" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Confirm Password * Confirm Password *
</label> </label>
@ -274,7 +276,7 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
placeholder="Confirm password" placeholder="Confirm password"
value={signupData.password2} value={signupData.password2}
onChange={(e) => setSignupData({ ...signupData, password2: e.target.value })} onChange={(e) => setSignupData({ ...signupData, password2: e.target.value })}
className={`h-12 pr-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`} className={`h-11 sm:h-12 pr-12 text-sm sm:text-base ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
required required
/> />
<Button <Button
@ -282,13 +284,13 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => setShowPassword2(!showPassword2)} onClick={() => setShowPassword2(!showPassword2)}
className={`absolute right-4 top-1/2 -translate-y-1/2 h-auto w-auto p-0 ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-500 hover:text-gray-700'}`} className={`absolute right-3 sm:right-4 top-1/2 -translate-y-1/2 h-auto w-auto p-0 ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-500 hover:text-gray-700'}`}
aria-label={showPassword2 ? "Hide password" : "Show password"} aria-label={showPassword2 ? "Hide password" : "Show password"}
> >
{showPassword2 ? ( {showPassword2 ? (
<EyeOff className="w-5 h-5" /> <EyeOff className="w-4 h-4 sm:w-5 sm:h-5" />
) : ( ) : (
<Eye className="w-5 h-5" /> <Eye className="w-4 h-4 sm:w-5 sm:h-5" />
)} )}
</Button> </Button>
</div> </div>
@ -298,7 +300,7 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
<Button <Button
type="submit" type="submit"
disabled={registerMutation.isPending} disabled={registerMutation.isPending}
className="w-full h-12 text-base font-semibold bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white shadow-lg hover:shadow-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed" className="w-full h-11 sm:h-12 text-sm sm:text-base font-semibold bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white shadow-lg hover:shadow-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed mt-4 sm:mt-6"
> >
{registerMutation.isPending ? ( {registerMutation.isPending ? (
<> <>
@ -311,7 +313,7 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
</Button> </Button>
{/* Switch to Login */} {/* Switch to Login */}
<p className={`text-sm text-center ${isDark ? 'text-gray-400' : 'text-gray-600'}`}> <p className={`text-xs sm:text-sm text-center pt-2 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
Already have an account?{" "} Already have an account?{" "}
<button <button
type="button" type="button"
@ -324,7 +326,7 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
</form> </form>
) : ( ) : (
/* Login Form */ /* Login Form */
<form className="space-y-6 mt-4" onSubmit={handleLogin}> <form className="space-y-4 sm:space-y-5 py-4 sm:py-6" onSubmit={handleLogin}>
{error && ( {error && (
<div className={`p-3 rounded-lg border ${isDark ? 'bg-red-900/20 border-red-800' : 'bg-red-50 border-red-200'}`}> <div className={`p-3 rounded-lg border ${isDark ? 'bg-red-900/20 border-red-800' : 'bg-red-50 border-red-200'}`}>
<p className={`text-sm ${isDark ? 'text-red-200' : 'text-red-800'}`}>{error}</p> <p className={`text-sm ${isDark ? 'text-red-200' : 'text-red-800'}`}>{error}</p>
@ -332,7 +334,7 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
)} )}
{/* Email Field */} {/* Email Field */}
<div className="space-y-2"> <div className="space-y-1.5 sm:space-y-2">
<label htmlFor="login-email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}> <label htmlFor="login-email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Email address Email address
</label> </label>
@ -342,13 +344,13 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
placeholder="Email address" placeholder="Email address"
value={loginData.email} value={loginData.email}
onChange={(e) => setLoginData({ ...loginData, email: e.target.value })} onChange={(e) => setLoginData({ ...loginData, email: e.target.value })}
className={`h-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`} className={`h-11 sm:h-12 text-sm sm:text-base ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
required required
/> />
</div> </div>
{/* Password Field */} {/* Password Field */}
<div className="space-y-2"> <div className="space-y-1.5 sm:space-y-2">
<label htmlFor="login-password" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}> <label htmlFor="login-password" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Your password Your password
</label> </label>
@ -359,7 +361,7 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
placeholder="Your password" placeholder="Your password"
value={loginData.password} value={loginData.password}
onChange={(e) => setLoginData({ ...loginData, password: e.target.value })} onChange={(e) => setLoginData({ ...loginData, password: e.target.value })}
className={`h-12 pr-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`} className={`h-11 sm:h-12 pr-12 text-sm sm:text-base ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
required required
/> />
<Button <Button
@ -367,13 +369,13 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
className={`absolute right-4 top-1/2 -translate-y-1/2 h-auto w-auto p-0 ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-500 hover:text-gray-700'}`} className={`absolute right-3 sm:right-4 top-1/2 -translate-y-1/2 h-auto w-auto p-0 ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-500 hover:text-gray-700'}`}
aria-label={showPassword ? "Hide password" : "Show password"} aria-label={showPassword ? "Hide password" : "Show password"}
> >
{showPassword ? ( {showPassword ? (
<EyeOff className="w-5 h-5" /> <EyeOff className="w-4 h-4 sm:w-5 sm:h-5" />
) : ( ) : (
<Eye className="w-5 h-5" /> <Eye className="w-4 h-4 sm:w-5 sm:h-5" />
)} )}
</Button> </Button>
</div> </div>
@ -383,7 +385,7 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
<Button <Button
type="submit" type="submit"
disabled={loginMutation.isPending} disabled={loginMutation.isPending}
className="w-full h-12 text-base font-semibold bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white shadow-lg hover:shadow-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed" className="w-full h-11 sm:h-12 text-sm sm:text-base font-semibold bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white shadow-lg hover:shadow-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed mt-4 sm:mt-6"
> >
{loginMutation.isPending ? ( {loginMutation.isPending ? (
<> <>
@ -396,7 +398,7 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
</Button> </Button>
{/* Remember Me & Forgot Password */} {/* Remember Me & Forgot Password */}
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-xs sm:text-sm">
<label className="flex items-center gap-2 cursor-pointer"> <label className="flex items-center gap-2 cursor-pointer">
<input <input
type="checkbox" type="checkbox"
@ -408,7 +410,7 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
</label> </label>
<button <button
type="button" type="button"
className={`font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`} className={`font-medium text-xs sm:text-sm ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
> >
Forgot password? Forgot password?
@ -416,7 +418,7 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
</div> </div>
{/* Sign Up Prompt */} {/* Sign Up Prompt */}
<p className={`text-sm text-center ${isDark ? 'text-gray-400' : 'text-gray-600'}`}> <p className={`text-xs sm:text-sm text-center pt-2 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
New to Attune Heart Therapy?{" "} New to Attune Heart Therapy?{" "}
<button <button
type="button" type="button"
@ -428,6 +430,7 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
</p> </p>
</form> </form>
)} )}
</div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );