diff --git a/package-lock.json b/package-lock.json index 13d84a8..071ee2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index 2e11959..078328a 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/Redux/Store.tsx b/src/Redux/Store.tsx index 7d03f11..04e0e46 100644 --- a/src/Redux/Store.tsx +++ b/src/Redux/Store.tsx @@ -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; diff --git a/src/Redux/baseQuery.ts b/src/Redux/baseQuery.ts index 37c41cc..7ffc8d8 100644 --- a/src/Redux/baseQuery.ts +++ b/src/Redux/baseQuery.ts @@ -14,4 +14,3 @@ export const baseQuery = fetchBaseQuery({ return headers; }, }); - diff --git a/src/Redux/services/auth.service.ts b/src/Redux/services/auth.service.ts new file mode 100644 index 0000000..100cd07 --- /dev/null +++ b/src/Redux/services/auth.service.ts @@ -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; \ No newline at end of file diff --git a/src/components/LoginModal.tsx b/src/components/LoginModal.tsx index b70ce28..5eca180 100644 --- a/src/components/LoginModal.tsx +++ b/src/components/LoginModal.tsx @@ -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) { {isOpen && ( <> - {/* Backdrop */} - {/* Modal */}
- {/* Header */}
- {/* Content */}
{step === 'email' ? ( + // ... Email step (unchanged)
- {/* Email Input */}
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 && ( -

- {helperText} -

- )} + {error &&

{error}

} + {helperText &&

{helperText}

}
- {/* Send OTP Button */}
) : (
{/* Email Display */}
- -
- {email} + +
+ {email}
- {helperText && ( -

- {helperText} -

- )}
- {/* OTP Input */} + {/* OTP Inputs with Paste Support */}
- {/* Countdown */} {countdown > 0 && ( -

- {formatCountdown(countdown)} +

+ Resend OTP in {formatCountdown(countdown)}

)}
- {/* Verify Button */} + {error &&

{error}

} + - {/* Resend OTP */} {countdown === 0 && ( diff --git a/src/main.tsx b/src/main.tsx index 241b0b3..9cf6d51 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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( +