integrate send-email,verifyOtp apis
This commit is contained in:
2
package-lock.json
generated
2
package-lock.json
generated
@@ -54,7 +54,7 @@
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"recharts": "^2.15.2",
|
||||
"sonner": "^2.0.3",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "*",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"vaul": "^1.1.2"
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"recharts": "^2.15.2",
|
||||
"sonner": "^2.0.3",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "*",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"vaul": "^1.1.2"
|
||||
|
||||
@@ -2,22 +2,25 @@ import { configureStore } from "@reduxjs/toolkit";
|
||||
import { fakeApi } from "./services/fakeApi.service";
|
||||
import { attractionsApi } from "./services/attractions.service";
|
||||
import { citiesApi } from "./services/cities.service";
|
||||
import { authApi } from "./services/auth.service";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
[fakeApi.reducerPath]:fakeApi.reducer,
|
||||
[attractionsApi.reducerPath]:attractionsApi.reducer,
|
||||
[citiesApi.reducerPath]:citiesApi.reducer
|
||||
[fakeApi.reducerPath]: fakeApi.reducer,
|
||||
[attractionsApi.reducerPath]: attractionsApi.reducer,
|
||||
[citiesApi.reducerPath]: citiesApi.reducer,
|
||||
[authApi.reducerPath]:authApi.reducer
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(
|
||||
|
||||
fakeApi.middleware,
|
||||
attractionsApi.middleware,
|
||||
citiesApi.middleware
|
||||
|
||||
fakeApi.middleware,
|
||||
attractionsApi.middleware,
|
||||
citiesApi.middleware,
|
||||
authApi.middleware
|
||||
),
|
||||
});
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
|
||||
@@ -14,4 +14,3 @@ export const baseQuery = fetchBaseQuery({
|
||||
return headers;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
53
src/Redux/services/auth.service.ts
Normal file
53
src/Redux/services/auth.service.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
|
||||
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
|
||||
import { baseQuery } from "../baseQuery";
|
||||
|
||||
export const authApi = createApi({
|
||||
reducerPath: "authApi",
|
||||
baseQuery: baseQuery,
|
||||
|
||||
tagTypes: ["profile", "Transaction"],
|
||||
|
||||
|
||||
endpoints: (builder) => ({
|
||||
// Login
|
||||
|
||||
login: builder.mutation({
|
||||
query: (credentials) => ({
|
||||
url: "/website/send-otp",
|
||||
method: "POST",
|
||||
body: credentials,
|
||||
}),
|
||||
}),
|
||||
|
||||
verifyOtp: builder.mutation({
|
||||
query: (credentials) => ({
|
||||
url: "/website/user/verify-otp",
|
||||
method: "POST",
|
||||
body: credentials,
|
||||
}),
|
||||
}),
|
||||
|
||||
register: builder.mutation({
|
||||
query: (credentials) => ({
|
||||
url: "/website/user/register",
|
||||
method: "POST",
|
||||
body: credentials,
|
||||
}),
|
||||
}),
|
||||
|
||||
logoutUser: builder.mutation({
|
||||
query: () => ({
|
||||
url: "/website/user/logout",
|
||||
method: "POST"
|
||||
})
|
||||
})
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useLoginMutation,
|
||||
useVerifyOtpMutation,
|
||||
useRegisterMutation,
|
||||
useLogoutUserMutation
|
||||
} = authApi;
|
||||
@@ -4,37 +4,45 @@ import { X } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { useLoginMutation, useVerifyOtpMutation } from '../Redux/services/auth.service';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface LoginModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
// onLoginSuccess: (userData: { email: string; name: string }) => void;
|
||||
}
|
||||
|
||||
export function LoginModal({ isOpen, onClose, }: LoginModalProps) {
|
||||
export function LoginModal({ isOpen, onClose }: LoginModalProps) {
|
||||
const [step, setStep] = useState<'email' | 'otp'>('email');
|
||||
const [email, setEmail] = useState('');
|
||||
const [otp, setOtp] = useState(['', '', '', '', '', '']);
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [helperText, setHelperText] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const { login } = useAuth(); // from AuthContext
|
||||
const { login } = useAuth();
|
||||
|
||||
// Reset modal state when closed
|
||||
const [sendOtp, { isLoading: isSendingOtp }] = useLoginMutation();
|
||||
const [verifyOtp, { isLoading: isVerifying }] = useVerifyOtpMutation();
|
||||
|
||||
// Reset modal when closed
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setStep('email');
|
||||
setEmail('');
|
||||
setOtp(['', '', '', '', '', '']);
|
||||
setCountdown(0);
|
||||
setHelperText('');
|
||||
resetModal();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Countdown timer for OTP resend
|
||||
const resetModal = () => {
|
||||
setStep('email');
|
||||
setEmail('');
|
||||
setOtp(['', '', '', '', '', '']);
|
||||
setCountdown(0);
|
||||
setHelperText('');
|
||||
setError('');
|
||||
};
|
||||
|
||||
// Countdown timer
|
||||
useEffect(() => {
|
||||
if (countdown > 0) {
|
||||
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
|
||||
@@ -42,67 +50,105 @@ export function LoginModal({ isOpen, onClose, }: LoginModalProps) {
|
||||
}
|
||||
}, [countdown]);
|
||||
|
||||
const handleSendOTP = async () => {
|
||||
if (!email || !email.includes('@')) {
|
||||
setHelperText('Please enter a valid email address');
|
||||
return;
|
||||
// ==================== PASTE OTP FEATURE ====================
|
||||
const handlePaste = (e: React.ClipboardEvent) => {
|
||||
e.preventDefault();
|
||||
const pastedData = e.clipboardData.getData('text').trim();
|
||||
|
||||
// Extract only digits
|
||||
const digits = pastedData.replace(/\D/g, '').slice(0, 6);
|
||||
|
||||
if (digits.length === 0) return;
|
||||
|
||||
const newOtp = [...otp];
|
||||
|
||||
// Fill the OTP array with pasted digits
|
||||
for (let i = 0; i < digits.length; i++) {
|
||||
newOtp[i] = digits[i];
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setHelperText('');
|
||||
setOtp(newOtp);
|
||||
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
setStep('otp');
|
||||
setCountdown(120); // 2 minutes countdown
|
||||
setIsLoading(false);
|
||||
setHelperText('OTP sent successfully');
|
||||
}, 1500);
|
||||
// Auto-focus the next empty field or the last one
|
||||
const nextIndex = digits.length < 6 ? digits.length : 5;
|
||||
const nextInput = document.querySelector(
|
||||
`input[data-otp-index="${nextIndex}"]`
|
||||
) as HTMLInputElement;
|
||||
|
||||
nextInput?.focus();
|
||||
};
|
||||
|
||||
const handleOTPChange = (index: number, value: string) => {
|
||||
if (value.length > 1) return; // Only allow single digit
|
||||
if (value.length > 1) return;
|
||||
|
||||
const newOtp = [...otp];
|
||||
newOtp[index] = value;
|
||||
setOtp(newOtp);
|
||||
|
||||
// Auto-focus next input
|
||||
if (value && index < 5) {
|
||||
const nextInput = document.querySelector(`input[data-otp-index="${index + 1}"]`) as HTMLInputElement;
|
||||
const nextInput = document.querySelector(
|
||||
`input[data-otp-index="${index + 1}"]`
|
||||
) as HTMLInputElement;
|
||||
nextInput?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleOTPKeyDown = (index: number, e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Backspace' && !otp[index] && index > 0) {
|
||||
const prevInput = document.querySelector(`input[data-otp-index="${index - 1}"]`) as HTMLInputElement;
|
||||
const prevInput = document.querySelector(
|
||||
`input[data-otp-index="${index - 1}"]`
|
||||
) as HTMLInputElement;
|
||||
prevInput?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// Rest of your functions remain the same
|
||||
const handleSendOTP = async () => {
|
||||
if (!email || !email.includes('@')) {
|
||||
setError('Please enter a valid email address');
|
||||
return;
|
||||
}
|
||||
|
||||
setError('');
|
||||
setHelperText('');
|
||||
|
||||
try {
|
||||
await sendOtp({ emailAddress: email }).unwrap();
|
||||
setStep('otp');
|
||||
setCountdown(120);
|
||||
setHelperText('OTP sent successfully to your email');
|
||||
} catch (err: any) {
|
||||
setError(err?.data?.message || 'Failed to send OTP. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyLogin = async () => {
|
||||
const otpString = otp.join('');
|
||||
if (otpString.length !== 6) {
|
||||
setHelperText('Please enter complete OTP');
|
||||
setError('Please enter complete 6-digit OTP');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setHelperText('');
|
||||
setError('');
|
||||
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
// Generate name from email for demo
|
||||
const emailParts = email.split('@')[0];
|
||||
const name = emailParts.charAt(0).toUpperCase() + emailParts.slice(1);
|
||||
try {
|
||||
const response = await verifyOtp({
|
||||
emailAddress: email,
|
||||
otp: otpString
|
||||
}).unwrap();
|
||||
|
||||
login({ email, name })
|
||||
const userData = {
|
||||
email: response?.email || email,
|
||||
name: response?.name || email.split('@')[0].charAt(0).toUpperCase() + email.split('@')[0].slice(1),
|
||||
};
|
||||
|
||||
setIsLoading(false);
|
||||
// navigate("/melbourne")
|
||||
login(userData);
|
||||
toast.success("User Logged in successfully")
|
||||
onClose();
|
||||
}, 1500);
|
||||
} catch (err: any) {
|
||||
setError(err?.data?.message || 'Invalid OTP. Please try again.');
|
||||
toast.error(err?.data?.message)
|
||||
}
|
||||
};
|
||||
|
||||
const formatCountdown = (seconds: number) => {
|
||||
@@ -115,26 +161,21 @@ export function LoginModal({ isOpen, onClose, }: LoginModalProps) {
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
className="fixed inset-0 flex items-center justify-center z-50 p-4"
|
||||
>
|
||||
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-md mx-auto overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="relative px-8 pt-8 pb-4">
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -147,73 +188,49 @@ export function LoginModal({ isOpen, onClose, }: LoginModalProps) {
|
||||
Login
|
||||
</h2>
|
||||
<p className="font-poppins text-sm text-gray-600">
|
||||
Enter your email Id and verify with OTP sent on it.
|
||||
Enter your email and verify with OTP
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="px-8 pb-8">
|
||||
{step === 'email' ? (
|
||||
// ... Email step (unchanged)
|
||||
<div className="space-y-6">
|
||||
{/* Email Input */}
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Email
|
||||
Email Address
|
||||
</Label>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Name@example.com"
|
||||
placeholder="name@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSendOTP()}
|
||||
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
|
||||
/>
|
||||
{helperText && (
|
||||
<p className={`font-poppins text-xs ${helperText.includes('success') ? 'text-green-600' : 'text-red-500'}`}>
|
||||
{helperText}
|
||||
</p>
|
||||
)}
|
||||
{error && <p className="text-red-500 text-xs">{error}</p>}
|
||||
{helperText && <p className="text-green-600 text-xs">{helperText}</p>}
|
||||
</div>
|
||||
|
||||
{/* Send OTP Button */}
|
||||
<Button
|
||||
onClick={handleSendOTP}
|
||||
disabled={isLoading}
|
||||
className="w-full h-12 bg-gray-800 hover:bg-gray-900 cursor-pointer text-white font-poppins font-semibold rounded-xl transition-colors"
|
||||
disabled={isSendingOtp}
|
||||
className="w-full h-12 bg-gray-800 hover:bg-gray-900 text-white font-poppins font-semibold rounded-xl"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin" />
|
||||
Sending OTP...
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
Send OTP
|
||||
<svg className="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
{isSendingOtp ? 'Sending OTP...' : 'Send OTP'}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{/* Email Display */}
|
||||
<div className="space-y-2">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Email
|
||||
</Label>
|
||||
<div className="h-12 bg-gray-50 rounded-xl flex items-center px-4">
|
||||
<span className="font-poppins text-base text-gray-600">{email}</span>
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">Email</Label>
|
||||
<div className="h-12 bg-gray-50 rounded-xl flex items-center px-4 font-poppins text-base text-gray-600">
|
||||
{email}
|
||||
</div>
|
||||
{helperText && (
|
||||
<p className={`font-poppins text-xs ${helperText.includes('success') ? 'text-green-600' : 'text-red-500'}`}>
|
||||
{helperText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* OTP Input */}
|
||||
{/* OTP Inputs with Paste Support */}
|
||||
<div className="space-y-3">
|
||||
<Label className="font-poppins text-sm font-medium text-gray-700">
|
||||
Enter OTP
|
||||
@@ -224,54 +241,42 @@ export function LoginModal({ isOpen, onClose, }: LoginModalProps) {
|
||||
key={index}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
maxLength={1}
|
||||
value={digit}
|
||||
onChange={(e) => handleOTPChange(index, e.target.value.replace(/\D/g, ''))}
|
||||
onKeyDown={(e) => handleOTPKeyDown(index, e)}
|
||||
onPaste={handlePaste} // ← Paste support added here
|
||||
data-otp-index={index}
|
||||
className="w-12 h-12 text-center font-poppins font-semibold text-lg bg-gray-300 border-0 rounded-xl focus:bg-white focus:ring-2 focus:ring-primary focus:outline-none transition-colors"
|
||||
className="w-12 h-12 text-center font-poppins font-semibold text-lg bg-gray-100 border-0 rounded-xl focus:bg-white focus:ring-2 focus:ring-gray-800 focus:outline-none transition-all"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Countdown */}
|
||||
{countdown > 0 && (
|
||||
<p className="font-poppins text-xs text-gray-500 text-center">
|
||||
{formatCountdown(countdown)}
|
||||
<p className="text-center text-xs text-gray-500">
|
||||
Resend OTP in {formatCountdown(countdown)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Verify Button */}
|
||||
{error && <p className="text-red-500 text-xs text-center">{error}</p>}
|
||||
|
||||
<Button
|
||||
onClick={handleVerifyLogin}
|
||||
disabled={isLoading || otp.join('').length !== 6}
|
||||
className="w-full h-12 bg-gray-800 hover:bg-gray-900 cursor-pointer text-white font-poppins font-semibold rounded-xl transition-colors disabled:opacity-50"
|
||||
disabled={isVerifying || otp.join('').length !== 6}
|
||||
className="w-full h-12 bg-gray-800 hover:bg-gray-900 text-white font-poppins font-semibold rounded-xl disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin" />
|
||||
Verifying...
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
Verify and Login
|
||||
<svg className="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
{isVerifying ? 'Verifying...' : 'Verify & Login'}
|
||||
</Button>
|
||||
|
||||
{/* Resend OTP */}
|
||||
{countdown === 0 && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setStep('email');
|
||||
setOtp(['', '', '', '', '', '']);
|
||||
setError('');
|
||||
}}
|
||||
className="w-full font-poppins text-sm text-gray-600 hover:text-gray-800 transition-colors"
|
||||
className="w-full text-sm text-gray-600 hover:text-gray-800 font-poppins"
|
||||
>
|
||||
Didn't receive OTP? Send again
|
||||
</button>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import App from "./App";
|
||||
import "./index.css";
|
||||
import { Provider } from "react-redux";
|
||||
import { store } from "./Redux/Store";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<Toaster position="top-right" richColors />
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
|
||||
Reference in New Issue
Block a user