Refactor LoginDialog to support pre-filling email and integrate Forgot Password functionality. Update Navbar to manage dialog states for login and signup, enhancing user experience with smoother transitions between dialogs. Remove unused signup logic from LoginDialog for a cleaner implementation.

This commit is contained in:
iamkiddy 2025-11-24 22:35:07 +00:00
parent 3465961819
commit 343c2c66b6
5 changed files with 1053 additions and 299 deletions

View File

@ -99,19 +99,7 @@ export function Header() {
<Calendar className="w-4 h-4 sm:w-5 sm:h-5" />
<span className="hidden sm:inline">Book Appointment</span>
</Link>
<Link
href="/deliverables"
className={`flex items-center gap-1 sm:gap-2 px-2 sm:px-3 md:px-4 py-1.5 sm:py-2 rounded-lg text-xs sm:text-sm font-medium transition-colors ${
pathname === "/deliverables"
? "bg-linear-to-r from-rose-500 to-pink-600 text-white"
: isDark
? "text-gray-300 hover:bg-gray-800"
: "text-gray-600 hover:bg-gray-100"
}`}
>
<FileText className="w-4 h-4 sm:w-5 sm:h-5" />
<span className="hidden sm:inline">Documentation</span>
</Link>
</nav>
{/* Right Side Actions */}

View File

@ -0,0 +1,459 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { useAppTheme } from "@/components/ThemeProvider";
import { Input } from "@/components/ui/input";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Eye, EyeOff, Loader2, X, CheckCircle2 } from "lucide-react";
import { useAuth } from "@/hooks/useAuth";
import {
forgotPasswordSchema,
verifyPasswordResetOtpSchema,
resetPasswordSchema,
type ForgotPasswordInput,
type VerifyPasswordResetOtpInput,
type ResetPasswordInput
} from "@/lib/schema/auth";
import { toast } from "sonner";
interface ForgotPasswordDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
}
type Step = "request" | "verify" | "reset";
export function ForgotPasswordDialog({ open, onOpenChange, onSuccess }: ForgotPasswordDialogProps) {
const { theme } = useAppTheme();
const isDark = theme === "dark";
const {
forgotPasswordMutation,
verifyPasswordResetOtpMutation,
resetPasswordMutation,
resendOtpMutation
} = useAuth();
const [step, setStep] = useState<Step>("request");
const [email, setEmail] = useState("");
const [otpData, setOtpData] = useState<VerifyPasswordResetOtpInput>({
email: "",
otp: "",
});
const [resetData, setResetData] = useState<ResetPasswordInput>({
email: "",
otp: "",
new_password: "",
confirm_password: "",
});
const [showPassword, setShowPassword] = useState(false);
const [showPassword2, setShowPassword2] = useState(false);
const handleRequestOtp = async (e: React.FormEvent) => {
e.preventDefault();
const validation = forgotPasswordSchema.safeParse({ email });
if (!validation.success) {
const firstError = validation.error.issues[0];
toast.error(firstError.message);
return;
}
try {
await forgotPasswordMutation.mutateAsync({ email });
setOtpData({ email, otp: "" });
setResetData({ email, otp: "", new_password: "", confirm_password: "" });
setStep("verify");
toast.success("Password reset OTP sent! Please check your email.");
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Failed to send OTP. Please try again.";
toast.error(errorMessage);
}
};
const handleVerifyOtp = async (e: React.FormEvent) => {
e.preventDefault();
const emailToVerify = email || otpData.email;
if (!emailToVerify) {
toast.error("Email is required");
return;
}
const validation = verifyPasswordResetOtpSchema.safeParse({
email: emailToVerify,
otp: otpData.otp,
});
if (!validation.success) {
const firstError = validation.error.issues[0];
toast.error(firstError.message);
return;
}
try {
await verifyPasswordResetOtpMutation.mutateAsync({
email: emailToVerify,
otp: otpData.otp,
});
setResetData({
email: emailToVerify,
otp: otpData.otp,
new_password: "",
confirm_password: ""
});
setStep("reset");
toast.success("OTP verified! Please set your new password.");
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "OTP verification failed. Please try again.";
toast.error(errorMessage);
}
};
const handleResetPassword = async (e: React.FormEvent) => {
e.preventDefault();
const validation = resetPasswordSchema.safeParse(resetData);
if (!validation.success) {
const firstError = validation.error.issues[0];
toast.error(firstError.message);
return;
}
try {
await resetPasswordMutation.mutateAsync(resetData);
toast.success("Password reset successful! Please log in with your new password.");
handleDialogChange(false);
if (onSuccess) {
onSuccess();
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Password reset failed. Please try again.";
toast.error(errorMessage);
}
};
const handleResendOtp = async () => {
const emailToResend = email || otpData.email;
if (!emailToResend) {
toast.error("Email is required");
return;
}
try {
await resendOtpMutation.mutateAsync({
email: emailToResend,
context: "password_reset"
});
toast.success("OTP resent successfully! Please check your email.");
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Failed to resend OTP";
toast.error(errorMessage);
}
};
const handleOtpChange = (field: keyof VerifyPasswordResetOtpInput, value: string) => {
setOtpData((prev) => ({ ...prev, [field]: value }));
};
// Reset step when dialog closes
const handleDialogChange = (isOpen: boolean) => {
if (!isOpen) {
setStep("request");
setEmail("");
setOtpData({ email: "", otp: "" });
setResetData({ email: "", otp: "", new_password: "", confirm_password: "" });
}
onOpenChange(isOpen);
};
return (
<Dialog open={open} onOpenChange={handleDialogChange}>
<DialogContent
showCloseButton={false}
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 - Fixed */}
<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 pr-2">
<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">
{step === "request" && "Reset Password"}
{step === "verify" && "Verify OTP"}
{step === "reset" && "Set New Password"}
</DialogTitle>
<DialogDescription className={`text-sm sm:text-base mt-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
{step === "request" && "Enter your email to receive a password reset code"}
{step === "verify" && "Enter the verification code sent to your email"}
{step === "reset" && "Enter your new password"}
</DialogDescription>
</DialogHeader>
{/* Close Button */}
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => handleDialogChange(false)}
className={`flex-shrink-0 w-8 h-8 rounded-full ${isDark ? 'text-gray-400 hover:text-gray-300 hover:bg-gray-700' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'}`}
aria-label="Close"
>
<X className="w-5 h-5" />
</Button>
</div>
{/* Scrollable Content */}
<div className="overflow-y-auto flex-1 px-6">
{/* Request OTP Form */}
{step === "request" && (
<form className="space-y-4 sm:space-y-5 py-4 sm:py-6" onSubmit={handleRequestOtp}>
<div className="space-y-1.5 sm:space-y-2">
<label htmlFor="forgot-email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Email address *
</label>
<Input
id="forgot-email"
type="email"
placeholder="Email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
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
/>
</div>
<Button
type="submit"
disabled={forgotPasswordMutation.isPending}
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"
>
{forgotPasswordMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Sending...
</>
) : (
"Send Reset Code"
)}
</Button>
</form>
)}
{/* Verify OTP Form */}
{step === "verify" && (
<form className="space-y-4 sm:space-y-5 py-4 sm:py-6" onSubmit={handleVerifyOtp}>
<div className={`p-3 sm:p-4 rounded-lg border ${isDark ? 'bg-blue-900/20 border-blue-800' : 'bg-blue-50 border-blue-200'}`}>
<div className="flex items-start gap-3">
<CheckCircle2 className={`w-5 h-5 mt-0.5 flex-shrink-0 ${isDark ? 'text-blue-400' : 'text-blue-600'}`} />
<div>
<p className={`text-sm font-medium ${isDark ? 'text-blue-200' : 'text-blue-900'}`}>
Check your email
</p>
<p className={`text-xs sm:text-sm mt-1 ${isDark ? 'text-blue-300' : 'text-blue-700'}`}>
We've sent a 6-digit verification code to {email || otpData.email || "your email address"}.
</p>
</div>
</div>
</div>
{/* Email Field (if not set) */}
{!email && (
<div className="space-y-1.5 sm:space-y-2">
<label htmlFor="verify-email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Email address *
</label>
<Input
id="verify-email"
type="email"
placeholder="Email address"
value={otpData.email}
onChange={(e) => handleOtpChange("email", e.target.value)}
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
/>
</div>
)}
{/* OTP Field */}
<div className="space-y-1.5 sm:space-y-2">
<label className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Verification Code *
</label>
<div className="flex justify-center">
<InputOTP
maxLength={6}
value={otpData.otp}
onChange={(value) => handleOtpChange("otp", value)}
>
<InputOTPGroup className="gap-2 sm:gap-3">
<InputOTPSlot index={0} className="h-12 w-12 sm:h-14 sm:w-14 text-lg sm:text-xl font-semibold" />
<InputOTPSlot index={1} className="h-12 w-12 sm:h-14 sm:w-14 text-lg sm:text-xl font-semibold" />
<InputOTPSlot index={2} className="h-12 w-12 sm:h-14 sm:w-14 text-lg sm:text-xl font-semibold" />
<InputOTPSlot index={3} className="h-12 w-12 sm:h-14 sm:w-14 text-lg sm:text-xl font-semibold" />
<InputOTPSlot index={4} className="h-12 w-12 sm:h-14 sm:w-14 text-lg sm:text-xl font-semibold" />
<InputOTPSlot index={5} className="h-12 w-12 sm:h-14 sm:w-14 text-lg sm:text-xl font-semibold" />
</InputOTPGroup>
</InputOTP>
</div>
</div>
{/* Resend OTP */}
<div className="text-center">
<Button
type="button"
variant="link"
onClick={handleResendOtp}
disabled={resendOtpMutation?.isPending}
className={`h-auto p-0 text-xs sm:text-sm font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
>
{resendOtpMutation?.isPending ? "Sending..." : "Didn't receive the code? Resend"}
</Button>
</div>
{/* Submit Button */}
<Button
type="submit"
disabled={verifyPasswordResetOtpMutation.isPending}
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"
>
{verifyPasswordResetOtpMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Verifying...
</>
) : (
"Verify Code"
)}
</Button>
{/* Back to request */}
<div className="text-center">
<Button
type="button"
variant="link"
onClick={() => {
setStep("request");
setOtpData({ email: "", otp: "" });
}}
className={`h-auto p-0 text-xs sm:text-sm font-medium ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-600 hover:text-gray-700'}`}
>
Back
</Button>
</div>
</form>
)}
{/* Reset Password Form */}
{step === "reset" && (
<form className="space-y-4 sm:space-y-5 py-4 sm:py-6" onSubmit={handleResetPassword}>
{/* New Password Field */}
<div className="space-y-1.5 sm:space-y-2">
<label htmlFor="reset-password" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
New Password *
</label>
<div className="relative">
<Input
id="reset-password"
type={showPassword ? "text" : "password"}
placeholder="New password (min 8 characters)"
value={resetData.new_password}
onChange={(e) => setResetData({ ...resetData, new_password: e.target.value })}
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
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setShowPassword(!showPassword)}
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"}
>
{showPassword ? (
<EyeOff className="w-4 h-4 sm:w-5 sm:h-5" />
) : (
<Eye className="w-4 h-4 sm:w-5 sm:h-5" />
)}
</Button>
</div>
</div>
{/* Confirm Password Field */}
<div className="space-y-1.5 sm:space-y-2">
<label htmlFor="reset-password2" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Confirm New Password *
</label>
<div className="relative">
<Input
id="reset-password2"
type={showPassword2 ? "text" : "password"}
placeholder="Confirm new password"
value={resetData.confirm_password}
onChange={(e) => setResetData({ ...resetData, confirm_password: e.target.value })}
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
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setShowPassword2(!showPassword2)}
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"}
>
{showPassword2 ? (
<EyeOff className="w-4 h-4 sm:w-5 sm:h-5" />
) : (
<Eye className="w-4 h-4 sm:w-5 sm:h-5" />
)}
</Button>
</div>
</div>
{/* Submit Button */}
<Button
type="submit"
disabled={resetPasswordMutation.isPending}
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"
>
{resetPasswordMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Resetting password...
</>
) : (
"Reset Password"
)}
</Button>
{/* Back to verify */}
<div className="text-center">
<Button
type="button"
variant="link"
onClick={() => {
setStep("verify");
setResetData({ ...resetData, new_password: "", confirm_password: "" });
}}
className={`h-auto p-0 text-xs sm:text-sm font-medium ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-600 hover:text-gray-700'}`}
>
Back
</Button>
</div>
</form>
)}
</div>
</DialogContent>
</Dialog>
);
}

View File

@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { useAppTheme } from "@/components/ThemeProvider";
import { Input } from "@/components/ui/input";
@ -13,43 +13,41 @@ import {
} from "@/components/ui/dialog";
import { Eye, EyeOff, Loader2, X } from "lucide-react";
import { useAuth } from "@/hooks/useAuth";
import { loginSchema, registerSchema, type LoginInput, type RegisterInput } from "@/lib/schema/auth";
import { loginSchema, type LoginInput } from "@/lib/schema/auth";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
import { ForgotPasswordDialog } from "./ForgotPasswordDialog";
interface LoginDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onLoginSuccess: () => void;
prefillEmail?: string;
onSwitchToSignup?: () => void;
}
// Login Dialog component
export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogProps) {
export function LoginDialog({ open, onOpenChange, onLoginSuccess, prefillEmail, onSwitchToSignup }: LoginDialogProps) {
const { theme } = useAppTheme();
const isDark = theme === "dark";
const router = useRouter();
const { login, register, loginMutation, registerMutation } = useAuth();
const [isSignup, setIsSignup] = useState(false);
const { login, loginMutation } = useAuth();
const [loginData, setLoginData] = useState<LoginInput>({
email: "",
password: "",
});
const [signupData, setSignupData] = useState<RegisterInput>({
first_name: "",
last_name: "",
email: "",
phone_number: "",
password: "",
password2: "",
});
const [showPassword, setShowPassword] = useState(false);
const [showPassword2, setShowPassword2] = useState(false);
const [rememberMe, setRememberMe] = useState(false);
const [error, setError] = useState<string | null>(null);
const [forgotPasswordDialogOpen, setForgotPasswordDialogOpen] = useState(false);
// Pre-fill email if provided
useEffect(() => {
if (prefillEmail && open) {
setLoginData(prev => ({ ...prev, email: prefillEmail }));
}
}, [prefillEmail, open]);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
// Validate form
const validation = loginSchema.safeParse(loginData);
@ -66,6 +64,10 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
toast.success("Login successful!");
setShowPassword(false);
onOpenChange(false);
// Reset form
setLoginData({ email: "", password: "" });
// Redirect to user dashboard
router.push("/user/dashboard");
onLoginSuccess();
}
} catch (err) {
@ -74,62 +76,16 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
}
};
const handleSignup = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
// Validate form
const validation = registerSchema.safeParse(signupData);
if (!validation.success) {
const firstError = validation.error.issues[0];
toast.error(firstError.message);
return;
}
try {
const result = await register(signupData);
if (result.message) {
toast.success("Registration successful! Please check your email for OTP verification.");
// Switch to login after successful registration
setIsSignup(false);
setLoginData({ email: signupData.email, password: "" });
setSignupData({
first_name: "",
last_name: "",
email: "",
phone_number: "",
password: "",
password2: "",
});
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Signup failed. Please try again.";
toast.error(errorMessage);
}
};
const handleSwitchToSignup = () => {
setIsSignup(true);
setError(null);
// Reset form when dialog closes
const handleDialogChange = (isOpen: boolean) => {
if (!isOpen) {
setLoginData({ email: "", password: "" });
};
const handleSwitchToLogin = () => {
setIsSignup(false);
setError(null);
setSignupData({
first_name: "",
last_name: "",
email: "",
phone_number: "",
password: "",
password2: "",
});
}
onOpenChange(isOpen);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<Dialog open={open} onOpenChange={handleDialogChange}>
<DialogContent
showCloseButton={false}
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'}`}
@ -138,189 +94,29 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
<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 pr-2">
<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"}
Welcome back
</DialogTitle>
<DialogDescription className={`text-sm sm:text-base mt-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
{isSignup
? "Sign up to complete your booking"
: "Please log in to complete your booking"}
Please log in to complete your booking
</DialogDescription>
</DialogHeader>
{/* Close Button */}
<button
onClick={() => onOpenChange(false)}
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center transition-colors ${isDark ? 'text-gray-400 hover:text-gray-300 hover:bg-gray-700' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'}`}
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => handleDialogChange(false)}
className={`flex-shrink-0 w-8 h-8 rounded-full ${isDark ? 'text-gray-400 hover:text-gray-300 hover:bg-gray-700' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'}`}
aria-label="Close"
>
<X className="w-5 h-5" />
</button>
</Button>
</div>
{/* Scrollable Content */}
<div className="overflow-y-auto flex-1 px-6">
{/* Signup Form */}
{isSignup ? (
<form className="space-y-4 sm:space-y-5 py-4 sm:py-6" onSubmit={handleSignup}>
{/* First Name Field */}
<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'}`}>
First Name *
</label>
<Input
id="signup-firstName"
type="text"
placeholder="John"
value={signupData.first_name}
onChange={(e) => setSignupData({ ...signupData, first_name: e.target.value })}
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
/>
</div>
{/* Last Name Field */}
<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'}`}>
Last Name *
</label>
<Input
id="signup-lastName"
type="text"
placeholder="Doe"
value={signupData.last_name}
onChange={(e) => setSignupData({ ...signupData, last_name: e.target.value })}
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
/>
</div>
{/* Email Field */}
<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'}`}>
Email address *
</label>
<Input
id="signup-email"
type="email"
placeholder="Email address"
value={signupData.email}
onChange={(e) => setSignupData({ ...signupData, email: e.target.value })}
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
/>
</div>
{/* Phone Field */}
<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'}`}>
Phone Number (Optional)
</label>
<Input
id="signup-phone"
type="tel"
placeholder="+1 (555) 123-4567"
value={signupData.phone_number || ""}
onChange={(e) => setSignupData({ ...signupData, phone_number: e.target.value })}
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>
{/* Password Field */}
<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'}`}>
Password *
</label>
<div className="relative">
<Input
id="signup-password"
type={showPassword ? "text" : "password"}
placeholder="Password (min 8 characters)"
value={signupData.password}
onChange={(e) => setSignupData({ ...signupData, password: e.target.value })}
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
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setShowPassword(!showPassword)}
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"}
>
{showPassword ? (
<EyeOff className="w-4 h-4 sm:w-5 sm:h-5" />
) : (
<Eye className="w-4 h-4 sm:w-5 sm:h-5" />
)}
</Button>
</div>
</div>
{/* Confirm Password Field */}
<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'}`}>
Confirm Password *
</label>
<div className="relative">
<Input
id="signup-password2"
type={showPassword2 ? "text" : "password"}
placeholder="Confirm password"
value={signupData.password2}
onChange={(e) => setSignupData({ ...signupData, password2: e.target.value })}
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
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setShowPassword2(!showPassword2)}
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"}
>
{showPassword2 ? (
<EyeOff className="w-4 h-4 sm:w-5 sm:h-5" />
) : (
<Eye className="w-4 h-4 sm:w-5 sm:h-5" />
)}
</Button>
</div>
</div>
{/* Submit Button */}
<Button
type="submit"
disabled={registerMutation.isPending}
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 ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Creating account...
</>
) : (
"Sign up"
)}
</Button>
{/* Switch to Login */}
<p className={`text-xs sm:text-sm text-center pt-2 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
Already have an account?{" "}
<button
type="button"
onClick={handleSwitchToLogin}
className={`underline font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
>
Log in
</button>
</p>
</form>
) : (
/* Login Form */
{/* Login Form */}
<form className="space-y-4 sm:space-y-5 py-4 sm:py-6" onSubmit={handleLogin}>
{/* Email Field */}
<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'}`}>
@ -385,42 +181,49 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
)}
</Button>
{/* Remember Me & Forgot Password */}
<div className="flex items-center justify-between text-xs sm:text-sm">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
className={`w-4 h-4 rounded text-rose-600 focus:ring-2 focus:ring-rose-500 cursor-pointer ${isDark ? 'border-gray-600 bg-gray-700' : 'border-gray-300'}`}
/>
<span className={isDark ? 'text-gray-300' : 'text-black'}>Remember me</span>
</label>
<button
{/* Forgot Password */}
<div className="flex items-center justify-end text-xs sm:text-sm">
<Button
type="button"
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)}
variant="link"
className={`font-medium text-xs sm:text-sm h-auto p-0 ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
onClick={() => {
handleDialogChange(false);
setTimeout(() => {
setForgotPasswordDialogOpen(true);
}, 100);
}}
>
Forgot password?
</button>
</Button>
</div>
{/* Sign Up Prompt */}
<p className={`text-xs sm:text-sm text-center pt-2 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
New to Attune Heart Therapy?{" "}
<button
<Button
type="button"
onClick={handleSwitchToSignup}
className={`underline font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
variant="link"
onClick={() => {
handleDialogChange(false);
if (onSwitchToSignup) {
onSwitchToSignup();
}
}}
className={`h-auto p-0 font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
>
Sign up
</button>
</Button>
</p>
</form>
)}
</div>
</DialogContent>
{/* Forgot Password Dialog */}
<ForgotPasswordDialog
open={forgotPasswordDialogOpen}
onOpenChange={setForgotPasswordDialogOpen}
/>
</Dialog>
);
}

View File

@ -6,6 +6,7 @@ import { Heart, Menu, X, LogOut } from "lucide-react";
import { ThemeToggle } from "@/components/ThemeToggle";
import { useEffect, useState } from "react";
import { LoginDialog } from "@/components/LoginDialog";
import { SignupDialog } from "@/components/SignupDialog";
import { useRouter, usePathname } from "next/navigation";
import Link from "next/link";
import { useAppTheme } from "@/components/ThemeProvider";
@ -16,6 +17,8 @@ export function Navbar() {
const { theme } = useAppTheme();
const isDark = theme === "dark";
const [loginDialogOpen, setLoginDialogOpen] = useState(false);
const [signupDialogOpen, setSignupDialogOpen] = useState(false);
const [prefillEmail, setPrefillEmail] = useState<string | undefined>(undefined);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const router = useRouter();
const pathname = usePathname();
@ -258,6 +261,28 @@ export function Navbar() {
open={loginDialogOpen}
onOpenChange={setLoginDialogOpen}
onLoginSuccess={handleLoginSuccess}
prefillEmail={prefillEmail}
onSwitchToSignup={() => {
setLoginDialogOpen(false);
// Small delay to ensure dialog closes before opening signup
setTimeout(() => {
setSignupDialogOpen(true);
}, 100);
}}
/>
{/* Signup Dialog */}
<SignupDialog
open={signupDialogOpen}
onOpenChange={setSignupDialogOpen}
onSwitchToLogin={(email?: string) => {
setSignupDialogOpen(false);
setPrefillEmail(email);
// Small delay to ensure dialog closes before opening login
setTimeout(() => {
setLoginDialogOpen(true);
}, 100);
}}
/>
</motion.nav>
);

479
components/SignupDialog.tsx Normal file
View File

@ -0,0 +1,479 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { useAppTheme } from "@/components/ThemeProvider";
import { Input } from "@/components/ui/input";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Eye, EyeOff, Loader2, X, CheckCircle2 } from "lucide-react";
import { useAuth } from "@/hooks/useAuth";
import { registerSchema, verifyOtpSchema, type RegisterInput, type VerifyOtpInput } from "@/lib/schema/auth";
import { toast } from "sonner";
interface SignupDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSignupSuccess?: () => void;
onSwitchToLogin?: (email?: string) => void;
}
type Step = "signup" | "verify";
export function SignupDialog({ open, onOpenChange, onSignupSuccess, onSwitchToLogin }: SignupDialogProps) {
const { theme } = useAppTheme();
const isDark = theme === "dark";
const { register, verifyOtp, registerMutation, verifyOtpMutation, resendOtpMutation } = useAuth();
const [step, setStep] = useState<Step>("signup");
const [registeredEmail, setRegisteredEmail] = useState("");
const [signupData, setSignupData] = useState<RegisterInput>({
first_name: "",
last_name: "",
email: "",
phone_number: "",
password: "",
password2: "",
});
const [otpData, setOtpData] = useState<VerifyOtpInput>({
email: "",
otp: "",
});
const [showPassword, setShowPassword] = useState(false);
const [showPassword2, setShowPassword2] = useState(false);
const handleSignup = async (e: React.FormEvent) => {
e.preventDefault();
// Validate form
const validation = registerSchema.safeParse(signupData);
if (!validation.success) {
const firstError = validation.error.issues[0];
toast.error(firstError.message);
return;
}
try {
const result = await register(signupData);
// Always switch to OTP verification step after successful registration
const email = signupData.email;
setRegisteredEmail(email);
setOtpData({ email: email, otp: "" });
// Clear signup form
setSignupData({
first_name: "",
last_name: "",
email: "",
phone_number: "",
password: "",
password2: "",
});
// Switch to verify step
setStep("verify");
toast.success("Registration successful! Please check your email for OTP verification.");
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Signup failed. Please try again.";
toast.error(errorMessage);
}
};
const handleVerifyOtp = async (e: React.FormEvent) => {
e.preventDefault();
const emailToVerify = registeredEmail || otpData.email;
if (!emailToVerify) {
toast.error("Email is required");
return;
}
const validation = verifyOtpSchema.safeParse({
email: emailToVerify,
otp: otpData.otp,
});
if (!validation.success) {
const firstError = validation.error.issues[0];
toast.error(firstError.message);
return;
}
try {
const result = await verifyOtp({
email: emailToVerify,
otp: otpData.otp,
});
if (result.message) {
toast.success("Email verified successfully! Please log in.");
// Close signup dialog and open login dialog with email
const emailToPass = emailToVerify;
onOpenChange(false);
// Call onSwitchToLogin with email to open login dialog with pre-filled email
if (onSwitchToLogin) {
onSwitchToLogin(emailToPass);
}
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "OTP verification failed. Please try again.";
toast.error(errorMessage);
}
};
const handleResendOtp = async () => {
const emailToResend = registeredEmail || otpData.email;
if (!emailToResend) {
toast.error("Email is required");
return;
}
try {
await resendOtpMutation.mutateAsync({ email: emailToResend, context: "registration" });
toast.success("OTP resent successfully! Please check your email.");
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Failed to resend OTP";
toast.error(errorMessage);
}
};
const handleOtpChange = (field: keyof VerifyOtpInput, value: string) => {
setOtpData((prev) => ({ ...prev, [field]: value }));
};
// Reset step when dialog closes
const handleDialogChange = (isOpen: boolean) => {
if (!isOpen) {
setStep("signup");
setRegisteredEmail("");
setOtpData({ email: "", otp: "" });
setSignupData({
first_name: "",
last_name: "",
email: "",
phone_number: "",
password: "",
password2: "",
});
}
onOpenChange(isOpen);
};
return (
<Dialog open={open} onOpenChange={handleDialogChange}>
<DialogContent
showCloseButton={false}
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 - Fixed */}
<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 pr-2">
<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">
{step === "signup" && "Create an account"}
{step === "verify" && "Verify your email"}
</DialogTitle>
<DialogDescription className={`text-sm sm:text-base mt-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
{step === "signup" && "Sign up to complete your booking"}
{step === "verify" && "Enter the verification code sent to your email"}
</DialogDescription>
</DialogHeader>
{/* Close Button */}
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => handleDialogChange(false)}
className={`flex-shrink-0 w-8 h-8 rounded-full ${isDark ? 'text-gray-400 hover:text-gray-300 hover:bg-gray-700' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'}`}
aria-label="Close"
>
<X className="w-5 h-5" />
</Button>
</div>
{/* Scrollable Content */}
<div className="overflow-y-auto flex-1 px-6">
{/* Signup Form */}
{step === "signup" && (
<form className="space-y-4 sm:space-y-5 py-4 sm:py-6" onSubmit={handleSignup}>
{/* First Name Field */}
<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'}`}>
First Name *
</label>
<Input
id="signup-firstName"
type="text"
placeholder="John"
value={signupData.first_name}
onChange={(e) => setSignupData({ ...signupData, first_name: e.target.value })}
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
/>
</div>
{/* Last Name Field */}
<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'}`}>
Last Name *
</label>
<Input
id="signup-lastName"
type="text"
placeholder="Doe"
value={signupData.last_name}
onChange={(e) => setSignupData({ ...signupData, last_name: e.target.value })}
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
/>
</div>
{/* Email Field */}
<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'}`}>
Email address *
</label>
<Input
id="signup-email"
type="email"
placeholder="Email address"
value={signupData.email}
onChange={(e) => setSignupData({ ...signupData, email: e.target.value })}
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
/>
</div>
{/* Phone Field */}
<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'}`}>
Phone Number (Optional)
</label>
<Input
id="signup-phone"
type="tel"
placeholder="+1 (555) 123-4567"
value={signupData.phone_number || ""}
onChange={(e) => setSignupData({ ...signupData, phone_number: e.target.value })}
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>
{/* Password Field */}
<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'}`}>
Password *
</label>
<div className="relative">
<Input
id="signup-password"
type={showPassword ? "text" : "password"}
placeholder="Password (min 8 characters)"
value={signupData.password}
onChange={(e) => setSignupData({ ...signupData, password: e.target.value })}
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
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setShowPassword(!showPassword)}
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"}
>
{showPassword ? (
<EyeOff className="w-4 h-4 sm:w-5 sm:h-5" />
) : (
<Eye className="w-4 h-4 sm:w-5 sm:h-5" />
)}
</Button>
</div>
</div>
{/* Confirm Password Field */}
<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'}`}>
Confirm Password *
</label>
<div className="relative">
<Input
id="signup-password2"
type={showPassword2 ? "text" : "password"}
placeholder="Confirm password"
value={signupData.password2}
onChange={(e) => setSignupData({ ...signupData, password2: e.target.value })}
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
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setShowPassword2(!showPassword2)}
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"}
>
{showPassword2 ? (
<EyeOff className="w-4 h-4 sm:w-5 sm:h-5" />
) : (
<Eye className="w-4 h-4 sm:w-5 sm:h-5" />
)}
</Button>
</div>
</div>
{/* Submit Button */}
<Button
type="submit"
disabled={registerMutation.isPending}
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 ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Creating account...
</>
) : (
"Sign up"
)}
</Button>
{/* Switch to Login */}
<p className={`text-xs sm:text-sm text-center pt-2 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
Already have an account?{" "}
<Button
type="button"
variant="link"
onClick={() => {
handleDialogChange(false);
if (onSwitchToLogin) {
onSwitchToLogin();
}
}}
className={`h-auto p-0 font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
>
Log in
</Button>
</p>
</form>
)}
{/* OTP Verification Form */}
{step === "verify" && (
<form className="space-y-4 sm:space-y-5 py-4 sm:py-6" onSubmit={handleVerifyOtp}>
<div className={`p-3 sm:p-4 rounded-lg border ${isDark ? 'bg-blue-900/20 border-blue-800' : 'bg-blue-50 border-blue-200'}`}>
<div className="flex items-start gap-3">
<CheckCircle2 className={`w-5 h-5 mt-0.5 flex-shrink-0 ${isDark ? 'text-blue-400' : 'text-blue-600'}`} />
<div>
<p className={`text-sm font-medium ${isDark ? 'text-blue-200' : 'text-blue-900'}`}>
Check your email
</p>
<p className={`text-xs sm:text-sm mt-1 ${isDark ? 'text-blue-300' : 'text-blue-700'}`}>
We've sent a 6-digit verification code to {registeredEmail || otpData.email || "your email address"}.
</p>
</div>
</div>
</div>
{/* Email Field (if not set) */}
{!registeredEmail && (
<div className="space-y-1.5 sm:space-y-2">
<label htmlFor="verify-email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Email address *
</label>
<Input
id="verify-email"
type="email"
placeholder="Email address"
value={otpData.email}
onChange={(e) => handleOtpChange("email", e.target.value)}
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
/>
</div>
)}
{/* OTP Field */}
<div className="space-y-1.5 sm:space-y-2">
<label className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Verification Code *
</label>
<div className="flex justify-center">
<InputOTP
maxLength={6}
value={otpData.otp}
onChange={(value) => handleOtpChange("otp", value)}
>
<InputOTPGroup className="gap-2 sm:gap-3">
<InputOTPSlot index={0} className="h-12 w-12 sm:h-14 sm:w-14 text-lg sm:text-xl font-semibold" />
<InputOTPSlot index={1} className="h-12 w-12 sm:h-14 sm:w-14 text-lg sm:text-xl font-semibold" />
<InputOTPSlot index={2} className="h-12 w-12 sm:h-14 sm:w-14 text-lg sm:text-xl font-semibold" />
<InputOTPSlot index={3} className="h-12 w-12 sm:h-14 sm:w-14 text-lg sm:text-xl font-semibold" />
<InputOTPSlot index={4} className="h-12 w-12 sm:h-14 sm:w-14 text-lg sm:text-xl font-semibold" />
<InputOTPSlot index={5} className="h-12 w-12 sm:h-14 sm:w-14 text-lg sm:text-xl font-semibold" />
</InputOTPGroup>
</InputOTP>
</div>
</div>
{/* Resend OTP */}
<div className="text-center">
<Button
type="button"
variant="link"
onClick={handleResendOtp}
disabled={resendOtpMutation?.isPending}
className={`h-auto p-0 text-xs sm:text-sm font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
>
{resendOtpMutation?.isPending ? "Sending..." : "Didn't receive the code? Resend"}
</Button>
</div>
{/* Submit Button */}
<Button
type="submit"
disabled={verifyOtpMutation.isPending}
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"
>
{verifyOtpMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Verifying...
</>
) : (
"Verify Email"
)}
</Button>
{/* Back to signup */}
<div className="text-center">
<Button
type="button"
variant="link"
onClick={() => {
setStep("signup");
setOtpData({ email: "", otp: "" });
}}
className={`h-auto p-0 text-xs sm:text-sm font-medium ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-600 hover:text-gray-700'}`}
>
Back to signup
</Button>
</div>
</form>
)}
</div>
</DialogContent>
</Dialog>
);
}