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" /> <Calendar className="w-4 h-4 sm:w-5 sm:h-5" />
<span className="hidden sm:inline">Book Appointment</span> <span className="hidden sm:inline">Book Appointment</span>
</Link> </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> </nav>
{/* Right Side Actions */} {/* 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"; "use client";
import { useState } from "react"; import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useAppTheme } from "@/components/ThemeProvider"; import { useAppTheme } from "@/components/ThemeProvider";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@ -13,43 +13,41 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Eye, EyeOff, Loader2, X } from "lucide-react"; import { Eye, EyeOff, Loader2, X } from "lucide-react";
import { useAuth } from "@/hooks/useAuth"; 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 { toast } from "sonner";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { ForgotPasswordDialog } from "./ForgotPasswordDialog";
interface LoginDialogProps { interface LoginDialogProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
onLoginSuccess: () => void; onLoginSuccess: () => void;
prefillEmail?: string;
onSwitchToSignup?: () => void;
} }
// Login Dialog component // Login Dialog component
export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogProps) { export function LoginDialog({ open, onOpenChange, onLoginSuccess, prefillEmail, onSwitchToSignup }: LoginDialogProps) {
const { theme } = useAppTheme(); const { theme } = useAppTheme();
const isDark = theme === "dark"; const isDark = theme === "dark";
const router = useRouter(); const router = useRouter();
const { login, register, loginMutation, registerMutation } = useAuth(); const { login, loginMutation } = useAuth();
const [isSignup, setIsSignup] = useState(false);
const [loginData, setLoginData] = useState<LoginInput>({ const [loginData, setLoginData] = useState<LoginInput>({
email: "", email: "",
password: "", password: "",
}); });
const [signupData, setSignupData] = useState<RegisterInput>({
first_name: "",
last_name: "",
email: "",
phone_number: "",
password: "",
password2: "",
});
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [showPassword2, setShowPassword2] = useState(false); const [forgotPasswordDialogOpen, setForgotPasswordDialogOpen] = useState(false);
const [rememberMe, setRememberMe] = useState(false);
const [error, setError] = useState<string | null>(null); // Pre-fill email if provided
useEffect(() => {
if (prefillEmail && open) {
setLoginData(prev => ({ ...prev, email: prefillEmail }));
}
}, [prefillEmail, open]);
const handleLogin = async (e: React.FormEvent) => { const handleLogin = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setError(null);
// Validate form // Validate form
const validation = loginSchema.safeParse(loginData); const validation = loginSchema.safeParse(loginData);
@ -64,9 +62,13 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
if (result.tokens && result.user) { if (result.tokens && result.user) {
toast.success("Login successful!"); toast.success("Login successful!");
setShowPassword(false); setShowPassword(false);
onOpenChange(false); onOpenChange(false);
onLoginSuccess(); // Reset form
setLoginData({ email: "", password: "" });
// Redirect to user dashboard
router.push("/user/dashboard");
onLoginSuccess();
} }
} catch (err) { } catch (err) {
const errorMessage = err instanceof Error ? err.message : "Login failed. Please try again."; const errorMessage = err instanceof Error ? err.message : "Login failed. Please try again.";
@ -74,62 +76,16 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
} }
}; };
const handleSignup = async (e: React.FormEvent) => { // Reset form when dialog closes
e.preventDefault(); const handleDialogChange = (isOpen: boolean) => {
setError(null); if (!isOpen) {
setLoginData({ email: "", password: "" });
// Validate form
const validation = registerSchema.safeParse(signupData);
if (!validation.success) {
const firstError = validation.error.issues[0];
toast.error(firstError.message);
return;
} }
onOpenChange(isOpen);
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);
setLoginData({ email: "", password: "" });
};
const handleSwitchToLogin = () => {
setIsSignup(false);
setError(null);
setSignupData({
first_name: "",
last_name: "",
email: "",
phone_number: "",
password: "",
password2: "",
});
}; };
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={handleDialogChange}>
<DialogContent <DialogContent
showCloseButton={false} 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'}`} 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,105 +94,57 @@ 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"> <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"> <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"> <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> </DialogTitle>
<DialogDescription className={`text-sm sm:text-base mt-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}> <DialogDescription className={`text-sm sm:text-base mt-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
{isSignup Please log in to complete your booking
? "Sign up to complete your booking"
: "Please log in to complete your booking"}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{/* Close Button */} {/* Close Button */}
<button <Button
onClick={() => onOpenChange(false)} type="button"
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'}`} 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" aria-label="Close"
> >
<X className="w-5 h-5" /> <X className="w-5 h-5" />
</button> </Button>
</div> </div>
{/* Scrollable Content */} {/* Scrollable Content */}
<div className="overflow-y-auto flex-1 px-6"> <div className="overflow-y-auto flex-1 px-6">
{/* Signup Form */} {/* Login Form */}
{isSignup ? ( <form className="space-y-4 sm:space-y-5 py-4 sm:py-6" onSubmit={handleLogin}>
<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 */} {/* Email Field */}
<div className="space-y-1.5 sm: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="login-email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Email address * Email address
</label> </label>
<Input <Input
id="signup-email" id="login-email"
type="email" type="email"
placeholder="Email address" placeholder="Email address"
value={signupData.email} value={loginData.email}
onChange={(e) => setSignupData({ ...signupData, email: e.target.value })} onChange={(e) => setLoginData({ ...loginData, 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'}`} 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 */}
<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 */} {/* Password Field */}
<div className="space-y-1.5 sm: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="login-password" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
Password * Your password
</label> </label>
<div className="relative"> <div className="relative">
<Input <Input
id="signup-password" id="login-password"
type={showPassword ? "text" : "password"} type={showPassword ? "text" : "password"}
placeholder="Password (min 8 characters)" placeholder="Your password"
value={signupData.password} value={loginData.password}
onChange={(e) => setSignupData({ ...signupData, password: e.target.value })} onChange={(e) => setLoginData({ ...loginData, 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'}`} 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
/> />
@ -257,170 +165,65 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
</div> </div>
</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 */} {/* Submit Button */}
<Button <Button
type="submit" type="submit"
disabled={registerMutation.isPending} disabled={loginMutation.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" 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 ? ( {loginMutation.isPending ? (
<> <>
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> <Loader2 className="w-4 h-4 mr-2 animate-spin" />
Creating account... Logging in...
</> </>
) : ( ) : (
"Sign up" "Log in"
)} )}
</Button> </Button>
{/* Switch to Login */} {/* Forgot Password */}
<p className={`text-xs sm:text-sm text-center pt-2 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}> <div className="flex items-center justify-end text-xs sm:text-sm">
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 */
<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'}`}>
Email address
</label>
<Input
id="login-email"
type="email"
placeholder="Email address"
value={loginData.email}
onChange={(e) => setLoginData({ ...loginData, 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>
{/* Password Field */}
<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'}`}>
Your password
</label>
<div className="relative">
<Input
id="login-password"
type={showPassword ? "text" : "password"}
placeholder="Your password"
value={loginData.password}
onChange={(e) => setLoginData({ ...loginData, 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 <Button
type="button" type="button"
variant="ghost" variant="link"
size="icon" 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={() => setShowPassword(!showPassword)} onClick={() => {
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'}`} handleDialogChange(false);
aria-label={showPassword ? "Hide password" : "Show password"} setTimeout(() => {
setForgotPasswordDialogOpen(true);
}, 100);
}}
> >
{showPassword ? ( Forgot password?
<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> </Button>
</div> </div>
</div>
{/* Submit Button */} {/* Sign Up Prompt */}
<Button <p className={`text-xs sm:text-sm text-center pt-2 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
type="submit" New to Attune Heart Therapy?{" "}
disabled={loginMutation.isPending} <Button
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" type="button"
> variant="link"
{loginMutation.isPending ? ( onClick={() => {
<> handleDialogChange(false);
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> if (onSwitchToSignup) {
Logging in... onSwitchToSignup();
</> }
) : ( }}
"Log in" className={`h-auto p-0 font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
)} >
</Button> Sign up
</Button>
{/* Remember Me & Forgot Password */} </p>
<div className="flex items-center justify-between text-xs sm:text-sm"> </form>
<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
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)}
>
Forgot password?
</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
type="button"
onClick={handleSwitchToSignup}
className={`underline font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
>
Sign up
</button>
</p>
</form>
)}
</div> </div>
</DialogContent> </DialogContent>
{/* Forgot Password Dialog */}
<ForgotPasswordDialog
open={forgotPasswordDialogOpen}
onOpenChange={setForgotPasswordDialogOpen}
/>
</Dialog> </Dialog>
); );
} }

View File

@ -6,6 +6,7 @@ import { Heart, Menu, X, LogOut } from "lucide-react";
import { ThemeToggle } from "@/components/ThemeToggle"; import { ThemeToggle } from "@/components/ThemeToggle";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { LoginDialog } from "@/components/LoginDialog"; import { LoginDialog } from "@/components/LoginDialog";
import { SignupDialog } from "@/components/SignupDialog";
import { useRouter, usePathname } from "next/navigation"; import { useRouter, usePathname } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { useAppTheme } from "@/components/ThemeProvider"; import { useAppTheme } from "@/components/ThemeProvider";
@ -16,6 +17,8 @@ export function Navbar() {
const { theme } = useAppTheme(); const { theme } = useAppTheme();
const isDark = theme === "dark"; const isDark = theme === "dark";
const [loginDialogOpen, setLoginDialogOpen] = useState(false); const [loginDialogOpen, setLoginDialogOpen] = useState(false);
const [signupDialogOpen, setSignupDialogOpen] = useState(false);
const [prefillEmail, setPrefillEmail] = useState<string | undefined>(undefined);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
@ -258,6 +261,28 @@ export function Navbar() {
open={loginDialogOpen} open={loginDialogOpen}
onOpenChange={setLoginDialogOpen} onOpenChange={setLoginDialogOpen}
onLoginSuccess={handleLoginSuccess} 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> </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>
);
}