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:
parent
3465961819
commit
343c2c66b6
@ -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 */}
|
||||
|
||||
459
components/ForgotPasswordDialog.tsx
Normal file
459
components/ForgotPasswordDialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
@ -64,9 +62,13 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
|
||||
|
||||
if (result.tokens && result.user) {
|
||||
toast.success("Login successful!");
|
||||
setShowPassword(false);
|
||||
onOpenChange(false);
|
||||
onLoginSuccess();
|
||||
setShowPassword(false);
|
||||
onOpenChange(false);
|
||||
// Reset form
|
||||
setLoginData({ email: "", password: "" });
|
||||
// Redirect to user dashboard
|
||||
router.push("/user/dashboard");
|
||||
onLoginSuccess();
|
||||
}
|
||||
} catch (err) {
|
||||
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) => {
|
||||
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;
|
||||
// Reset form when dialog closes
|
||||
const handleDialogChange = (isOpen: boolean) => {
|
||||
if (!isOpen) {
|
||||
setLoginData({ email: "", password: "" });
|
||||
}
|
||||
|
||||
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: "",
|
||||
});
|
||||
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,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">
|
||||
<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>
|
||||
|
||||
{/* 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="signup-email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
|
||||
Email address *
|
||||
<label htmlFor="login-email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
|
||||
Email address
|
||||
</label>
|
||||
<Input
|
||||
id="signup-email"
|
||||
id="login-email"
|
||||
type="email"
|
||||
placeholder="Email address"
|
||||
value={signupData.email}
|
||||
onChange={(e) => setSignupData({ ...signupData, email: e.target.value })}
|
||||
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>
|
||||
|
||||
{/* 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 htmlFor="login-password" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
|
||||
Your password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="signup-password"
|
||||
id="login-password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Password (min 8 characters)"
|
||||
value={signupData.password}
|
||||
onChange={(e) => setSignupData({ ...signupData, password: e.target.value })}
|
||||
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
|
||||
/>
|
||||
@ -257,170 +165,65 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
|
||||
</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}
|
||||
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"
|
||||
>
|
||||
{registerMutation.isPending ? (
|
||||
{loginMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Creating account...
|
||||
Logging in...
|
||||
</>
|
||||
) : (
|
||||
"Sign up"
|
||||
"Log in"
|
||||
)}
|
||||
</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 */
|
||||
<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
|
||||
/>
|
||||
{/* Forgot Password */}
|
||||
<div className="flex items-center justify-end text-xs sm:text-sm">
|
||||
<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"}
|
||||
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);
|
||||
}}
|
||||
>
|
||||
{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" />
|
||||
)}
|
||||
Forgot password?
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
type="submit"
|
||||
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"
|
||||
>
|
||||
{loginMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Logging in...
|
||||
</>
|
||||
) : (
|
||||
"Log in"
|
||||
)}
|
||||
</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
|
||||
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>
|
||||
)}
|
||||
{/* 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"
|
||||
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>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
{/* Forgot Password Dialog */}
|
||||
<ForgotPasswordDialog
|
||||
open={forgotPasswordDialogOpen}
|
||||
onOpenChange={setForgotPasswordDialogOpen}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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
479
components/SignupDialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user