removed captacha
This commit is contained in:
parent
9745092a33
commit
de3a72ca35
@ -1782,7 +1782,7 @@ const CompanyProfile = () => {
|
|||||||
try {
|
try {
|
||||||
const countResponse = await fetchEstablishments({ params: countParams, signal: controller.signal });
|
const countResponse = await fetchEstablishments({ params: countParams, signal: controller.signal });
|
||||||
const countPayload = countResponse?.data ?? countResponse;
|
const countPayload = countResponse?.data ?? countResponse;
|
||||||
const totalRecords = countPayload?.pagination?.total_records || 0;
|
const totalRecords = countResponse?.pagination?.total_records || 0;
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
search: debouncedSearch || undefined,
|
search: debouncedSearch || undefined,
|
||||||
|
|||||||
@ -1,50 +1,36 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { login } from '@/services/auth/authService.js';
|
import { login } from '@/services/auth/authService.js';
|
||||||
import { RefreshCw, AlertCircle, Clock } from "lucide-react";
|
import { AlertCircle, Clock } from "lucide-react";
|
||||||
|
|
||||||
const logoSrc = '/assets/images/FCSCLogo.svg';
|
const logoSrc = '/assets/images/FCSCLogo.svg';
|
||||||
|
|
||||||
const MAX_FAILED_ATTEMPTS = 3;
|
const MAX_FAILED_ATTEMPTS = 3;
|
||||||
const SHOW_CAPTCHA_AFTER = 0;
|
const LOCK_DURATION_MINUTES = 2;
|
||||||
const LOCK_DURATION_MINUTES = 2;
|
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const [email, setEmail] = React.useState('');
|
const [email, setEmail] = React.useState('');
|
||||||
const [password, setPassword] = React.useState('');
|
const [password, setPassword] = React.useState('');
|
||||||
const [showPassword, setShowPassword] = React.useState(false);
|
const [showPassword, setShowPassword] = React.useState(false);
|
||||||
const [rememberMe, setRememberMe] = React.useState(true);
|
const [rememberMe, setRememberMe] = React.useState(true);
|
||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
const [toast, setToast] = React.useState(null);
|
const [toast, setToast] = React.useState(null);
|
||||||
|
|
||||||
const [emailError, setEmailError] = React.useState('');
|
const [emailError, setEmailError] = React.useState('');
|
||||||
const [passwordError, setPasswordError] = React.useState('');
|
const [passwordError, setPasswordError] = React.useState('');
|
||||||
|
|
||||||
const [captcha, setCaptcha] = React.useState('');
|
|
||||||
const [userCaptcha, setUserCaptcha] = React.useState('');
|
|
||||||
const [captchaError, setCaptchaError] = React.useState('');
|
|
||||||
|
|
||||||
const [isLocked, setIsLocked] = React.useState(false);
|
const [isLocked, setIsLocked] = React.useState(false);
|
||||||
const [lockExpiry, setLockExpiry] = React.useState(null);
|
const [lockExpiry, setLockExpiry] = React.useState(null);
|
||||||
const [failedAttempts, setFailedAttempts] = React.useState(0);
|
const [failedAttempts, setFailedAttempts] = React.useState(0);
|
||||||
const [remainingTime, setRemainingTime] = React.useState(0);
|
const [remainingTime, setRemainingTime] = React.useState(0);
|
||||||
|
|
||||||
const toastTimeoutRef = React.useRef(null);
|
const toastTimeoutRef = React.useRef(null);
|
||||||
const navigateTimeoutRef = React.useRef(null);
|
const navigateTimeoutRef = React.useRef(null);
|
||||||
const countdownIntervalRef = React.useRef(null);
|
const countdownIntervalRef = React.useRef(null);
|
||||||
|
|
||||||
const generateCaptcha = React.useCallback(() => {
|
|
||||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|
||||||
let code = '';
|
|
||||||
for (let i = 0; i < 6; i++) {
|
|
||||||
code += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
||||||
}
|
|
||||||
setCaptcha(code);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const closeToast = React.useCallback(() => {
|
const closeToast = React.useCallback(() => {
|
||||||
if (toastTimeoutRef.current) {
|
if (toastTimeoutRef.current) {
|
||||||
clearTimeout(toastTimeoutRef.current);
|
clearTimeout(toastTimeoutRef.current);
|
||||||
@ -52,7 +38,7 @@ const Login = () => {
|
|||||||
}
|
}
|
||||||
setToast(null);
|
setToast(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const showToast = React.useCallback((type, message) => {
|
const showToast = React.useCallback((type, message) => {
|
||||||
if (!message) return;
|
if (!message) return;
|
||||||
if (toastTimeoutRef.current) {
|
if (toastTimeoutRef.current) {
|
||||||
@ -65,7 +51,7 @@ const Login = () => {
|
|||||||
toastTimeoutRef.current = null;
|
toastTimeoutRef.current = null;
|
||||||
}, 4000);
|
}, 4000);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const scheduleNavigate = React.useCallback(
|
const scheduleNavigate = React.useCallback(
|
||||||
(path) => {
|
(path) => {
|
||||||
if (!path) return;
|
if (!path) return;
|
||||||
@ -80,18 +66,18 @@ const Login = () => {
|
|||||||
},
|
},
|
||||||
[navigate]
|
[navigate]
|
||||||
);
|
);
|
||||||
|
|
||||||
const startCountdown = React.useCallback((lockUntil) => {
|
const startCountdown = React.useCallback((lockUntil) => {
|
||||||
if (countdownIntervalRef.current) {
|
if (countdownIntervalRef.current) {
|
||||||
clearInterval(countdownIntervalRef.current);
|
clearInterval(countdownIntervalRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateTimer = () => {
|
const updateTimer = () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const remaining = Math.max(0, Math.ceil((parseInt(lockUntil, 10) - now) / 1000));
|
const remaining = Math.max(0, Math.ceil((parseInt(lockUntil, 10) - now) / 1000));
|
||||||
|
|
||||||
setRemainingTime(remaining);
|
setRemainingTime(remaining);
|
||||||
|
|
||||||
if (remaining <= 0) {
|
if (remaining <= 0) {
|
||||||
clearInterval(countdownIntervalRef.current);
|
clearInterval(countdownIntervalRef.current);
|
||||||
countdownIntervalRef.current = null;
|
countdownIntervalRef.current = null;
|
||||||
@ -101,20 +87,21 @@ const Login = () => {
|
|||||||
showToast('success', 'Account unlocked. You can try logging in again.');
|
showToast('success', 'Account unlocked. You can try logging in again.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
updateTimer();
|
updateTimer();
|
||||||
countdownIntervalRef.current = setInterval(updateTimer, 1000);
|
countdownIntervalRef.current = setInterval(updateTimer, 1000);
|
||||||
}, [showToast]);
|
}, [showToast]);
|
||||||
|
|
||||||
|
// Check lock status when email changes
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
generateCaptcha();
|
if (!email) return;
|
||||||
|
|
||||||
// Get the stored attempts and lock status for the current email
|
// Get the stored attempts and lock status for the current email
|
||||||
const storedAttempts = JSON.parse(localStorage.getItem('login_attempts') || '{}');
|
const storedAttempts = JSON.parse(localStorage.getItem('login_attempts') || '{}');
|
||||||
const emailAttempts = storedAttempts[email] || { attempts: 0, lockUntil: null };
|
const emailAttempts = storedAttempts[email] || { attempts: 0, lockUntil: null };
|
||||||
|
|
||||||
setFailedAttempts(emailAttempts.attempts);
|
setFailedAttempts(emailAttempts.attempts);
|
||||||
|
|
||||||
if (emailAttempts.lockUntil && Date.now() < emailAttempts.lockUntil) {
|
if (emailAttempts.lockUntil && Date.now() < emailAttempts.lockUntil) {
|
||||||
setIsLocked(true);
|
setIsLocked(true);
|
||||||
setLockExpiry(emailAttempts.lockUntil);
|
setLockExpiry(emailAttempts.lockUntil);
|
||||||
@ -128,8 +115,8 @@ const Login = () => {
|
|||||||
} else {
|
} else {
|
||||||
setIsLocked(false);
|
setIsLocked(false);
|
||||||
}
|
}
|
||||||
}, [generateCaptcha, startCountdown, email]);
|
}, [email, startCountdown]);
|
||||||
|
|
||||||
React.useEffect(
|
React.useEffect(
|
||||||
() => () => {
|
() => () => {
|
||||||
if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current);
|
if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current);
|
||||||
@ -138,29 +125,17 @@ const Login = () => {
|
|||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCaptchaChange = (e) => {
|
|
||||||
setUserCaptcha(e.target.value.toUpperCase());
|
|
||||||
if (captchaError) setCaptchaError('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const refreshCaptcha = () => {
|
|
||||||
generateCaptcha();
|
|
||||||
setUserCaptcha('');
|
|
||||||
setCaptchaError('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
let valid = true;
|
let valid = true;
|
||||||
setEmailError('');
|
setEmailError('');
|
||||||
setPasswordError('');
|
setPasswordError('');
|
||||||
setCaptchaError('');
|
|
||||||
|
|
||||||
const trimmedEmail = email.trim();
|
const trimmedEmail = email.trim();
|
||||||
const trimmedPassword = password.trim();
|
const trimmedPassword = password.trim();
|
||||||
|
|
||||||
if (!trimmedEmail) {
|
if (!trimmedEmail) {
|
||||||
setEmailError('Email is required');
|
setEmailError('Email is required');
|
||||||
valid = false;
|
valid = false;
|
||||||
@ -168,47 +143,39 @@ const Login = () => {
|
|||||||
setEmailError('Enter a valid email address.');
|
setEmailError('Enter a valid email address.');
|
||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!trimmedPassword) {
|
if (!trimmedPassword) {
|
||||||
setPasswordError('Password is required');
|
setPasswordError('Password is required');
|
||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userCaptcha.trim()) {
|
|
||||||
setCaptchaError('Please enter the CAPTCHA.');
|
|
||||||
valid = false;
|
|
||||||
} else if (userCaptcha !== captcha) {
|
|
||||||
setCaptchaError('Invalid CAPTCHA. Try again or refresh for a new code.');
|
|
||||||
valid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return valid;
|
return valid;
|
||||||
};
|
};
|
||||||
|
|
||||||
const submit = async (e) => {
|
const submit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (isLocked) {
|
if (isLocked) {
|
||||||
const minutes = Math.floor(remainingTime / 60);
|
const minutes = Math.floor(remainingTime / 60);
|
||||||
const seconds = remainingTime % 60;
|
const seconds = remainingTime % 60;
|
||||||
showToast('error', `Account locked. Please wait ${minutes}m ${seconds}s or contact support.`);
|
showToast('error', `Account locked. Please wait ${minutes}m ${seconds}s or contact support.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateForm()) return;
|
if (!validateForm()) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await login({ email, password });
|
const response = await login({ email, password });
|
||||||
|
|
||||||
if (response?.status !== 'success' || !response?.data) {
|
if (response?.status !== 'success' || !response?.data) {
|
||||||
throw new Error('Invalid credentials');
|
throw new Error('Invalid credentials');
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = response.data;
|
const token = response.data;
|
||||||
sessionStorage.setItem('auth_token', token);
|
sessionStorage.setItem('auth_token', token);
|
||||||
|
|
||||||
let payload;
|
let payload;
|
||||||
try {
|
try {
|
||||||
const [, base64Payload] = token.split('.');
|
const [, base64Payload] = token.split('.');
|
||||||
@ -216,29 +183,29 @@ const Login = () => {
|
|||||||
} catch {
|
} catch {
|
||||||
throw new Error('Invalid token format.');
|
throw new Error('Invalid token format.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const role = payload?.role ?? (payload?.is_admin ? 'Admin' : undefined);
|
const role = payload?.role ?? (payload?.is_admin ? 'Admin' : undefined);
|
||||||
if (role) sessionStorage.setItem('user_role', role);
|
if (role) sessionStorage.setItem('user_role', role);
|
||||||
|
|
||||||
const profile = {
|
const profile = {
|
||||||
id: payload?.id || payload?.user_id || '',
|
id: payload?.id || payload?.user_id || '',
|
||||||
name: payload?.name || payload?.username || '',
|
name: payload?.name || payload?.username || '',
|
||||||
email: payload?.email || '',
|
email: payload?.email || '',
|
||||||
};
|
};
|
||||||
sessionStorage.setItem('user_profile', JSON.stringify(profile));
|
sessionStorage.setItem('user_profile', JSON.stringify(profile));
|
||||||
|
|
||||||
const establishmentId = payload?.establishment_id ?? payload?.establishmentId;
|
const establishmentId = payload?.establishment_id ?? payload?.establishmentId;
|
||||||
const isAdmin =
|
const isAdmin =
|
||||||
String(role || '').toLowerCase() === 'admin' || payload?.is_admin === true;
|
String(role || '').toLowerCase() === 'admin' || payload?.is_admin === true;
|
||||||
const successMessage = response?.message || 'Login successful.';
|
const successMessage = response?.message || 'Login successful.';
|
||||||
|
|
||||||
// Clear login attempts for this email
|
// Clear login attempts for this email
|
||||||
const storedAttempts = JSON.parse(localStorage.getItem('login_attempts') || '{}');
|
const storedAttempts = JSON.parse(localStorage.getItem('login_attempts') || '{}');
|
||||||
if (storedAttempts[email]) {
|
if (storedAttempts[email]) {
|
||||||
delete storedAttempts[email];
|
delete storedAttempts[email];
|
||||||
localStorage.setItem('login_attempts', JSON.stringify(storedAttempts));
|
localStorage.setItem('login_attempts', JSON.stringify(storedAttempts));
|
||||||
}
|
}
|
||||||
|
|
||||||
setFailedAttempts(0);
|
setFailedAttempts(0);
|
||||||
setIsLocked(false);
|
setIsLocked(false);
|
||||||
setRemainingTime(0);
|
setRemainingTime(0);
|
||||||
@ -246,14 +213,14 @@ const Login = () => {
|
|||||||
clearInterval(countdownIntervalRef.current);
|
clearInterval(countdownIntervalRef.current);
|
||||||
countdownIntervalRef.current = null;
|
countdownIntervalRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
sessionStorage.removeItem('establishment_id');
|
sessionStorage.removeItem('establishment_id');
|
||||||
showToast('success', successMessage);
|
showToast('success', successMessage);
|
||||||
scheduleNavigate('/admin/dashboard');
|
scheduleNavigate('/admin/dashboard');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rememberMe) {
|
if (rememberMe) {
|
||||||
sessionStorage.setItem('remember_me', 'true');
|
sessionStorage.setItem('remember_me', 'true');
|
||||||
sessionStorage.setItem('remembered_email', email);
|
sessionStorage.setItem('remembered_email', email);
|
||||||
@ -263,7 +230,7 @@ const Login = () => {
|
|||||||
sessionStorage.removeItem('remembered_email');
|
sessionStorage.removeItem('remembered_email');
|
||||||
sessionStorage.removeItem('remembered_password');
|
sessionStorage.removeItem('remembered_password');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (establishmentId) {
|
if (establishmentId) {
|
||||||
sessionStorage.setItem('establishment_id', String(establishmentId));
|
sessionStorage.setItem('establishment_id', String(establishmentId));
|
||||||
showToast('success', successMessage);
|
showToast('success', successMessage);
|
||||||
@ -273,26 +240,24 @@ const Login = () => {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Login failed', err);
|
console.error('Login failed', err);
|
||||||
|
|
||||||
const storedAttempts = JSON.parse(localStorage.getItem('login_attempts') || '{}');
|
const storedAttempts = JSON.parse(localStorage.getItem('login_attempts') || '{}');
|
||||||
const attempts = (storedAttempts[email]?.attempts || 0) + 1;
|
const attempts = (storedAttempts[email]?.attempts || 0) + 1;
|
||||||
|
|
||||||
// Update attempts for this email
|
// Update attempts for this email
|
||||||
const updatedAttempts = {
|
const updatedAttempts = {
|
||||||
...storedAttempts,
|
...storedAttempts,
|
||||||
[email]: {
|
[email]: {
|
||||||
attempts,
|
attempts,
|
||||||
lockUntil: attempts >= MAX_FAILED_ATTEMPTS
|
lockUntil: attempts >= MAX_FAILED_ATTEMPTS
|
||||||
? Date.now() + LOCK_DURATION_MINUTES * 60 * 1000
|
? Date.now() + LOCK_DURATION_MINUTES * 60 * 1000
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
localStorage.setItem('login_attempts', JSON.stringify(updatedAttempts));
|
localStorage.setItem('login_attempts', JSON.stringify(updatedAttempts));
|
||||||
setFailedAttempts(attempts);
|
setFailedAttempts(attempts);
|
||||||
|
|
||||||
refreshCaptcha();
|
|
||||||
|
|
||||||
if (attempts >= MAX_FAILED_ATTEMPTS) {
|
if (attempts >= MAX_FAILED_ATTEMPTS) {
|
||||||
const lockUntil = Date.now() + LOCK_DURATION_MINUTES * 60 * 1000;
|
const lockUntil = Date.now() + LOCK_DURATION_MINUTES * 60 * 1000;
|
||||||
setIsLocked(true);
|
setIsLocked(true);
|
||||||
@ -307,13 +272,13 @@ const Login = () => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTime = (seconds) => {
|
const formatTime = (seconds) => {
|
||||||
const mins = Math.floor(seconds / 60);
|
const mins = Math.floor(seconds / 60);
|
||||||
const secs = seconds % 60;
|
const secs = seconds % 60;
|
||||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#F7F7F7] flex items-center justify-center px-4">
|
<div className="min-h-screen bg-[#F7F7F7] flex items-center justify-center px-4">
|
||||||
{loading && (
|
{loading && (
|
||||||
@ -321,7 +286,7 @@ const Login = () => {
|
|||||||
<div className="h-12 w-12 animate-spin rounded-full border-[3px] border-white border-t-transparent" />
|
<div className="h-12 w-12 animate-spin rounded-full border-[3px] border-white border-t-transparent" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{toast && (
|
{toast && (
|
||||||
<div className="fixed top-6 inset-x-0 z-50 flex justify-center px-4">
|
<div className="fixed top-6 inset-x-0 z-50 flex justify-center px-4">
|
||||||
<div
|
<div
|
||||||
@ -345,7 +310,7 @@ const Login = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md">
|
||||||
<div className="bg-white rounded-lg shadow-sm ring-1 ring-gray-200 overflow-hidden">
|
<div className="bg-white rounded-lg shadow-sm ring-1 ring-gray-200 overflow-hidden">
|
||||||
<div className="px-7 py-6 border-b border-gray-200 bg-[#F2ECCF] text-center">
|
<div className="px-7 py-6 border-b border-gray-200 bg-[#F2ECCF] text-center">
|
||||||
@ -356,7 +321,7 @@ const Login = () => {
|
|||||||
Industrial Production Index (IPI) Survey Portal
|
Industrial Production Index (IPI) Survey Portal
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={submit} className="px-7 py-8 space-y-4">
|
<form onSubmit={submit} className="px-7 py-8 space-y-4">
|
||||||
{isLocked && remainingTime > 0 && (
|
{isLocked && remainingTime > 0 && (
|
||||||
<div className="bg-red-50 border-l-4 border-red-500 p-4 rounded">
|
<div className="bg-red-50 border-l-4 border-red-500 p-4 rounded">
|
||||||
@ -376,7 +341,7 @@ const Login = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLocked && failedAttempts > 0 && failedAttempts < MAX_FAILED_ATTEMPTS && (
|
{!isLocked && failedAttempts > 0 && failedAttempts < MAX_FAILED_ATTEMPTS && (
|
||||||
<div className="bg-yellow-50 border-l-4 border-yellow-500 p-3 rounded">
|
<div className="bg-yellow-50 border-l-4 border-yellow-500 p-3 rounded">
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
@ -387,7 +352,7 @@ const Login = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Email address
|
Email address
|
||||||
@ -402,16 +367,16 @@ const Login = () => {
|
|||||||
placeholder="name@company.com"
|
placeholder="name@company.com"
|
||||||
disabled={isLocked}
|
disabled={isLocked}
|
||||||
className={`block w-full h-10 rounded-md border-2 px-4 text-sm focus:ring-0 ${
|
className={`block w-full h-10 rounded-md border-2 px-4 text-sm focus:ring-0 ${
|
||||||
emailError
|
emailError
|
||||||
? 'border-red-500 bg-red-50'
|
? 'border-red-500 bg-red-50'
|
||||||
: isLocked
|
: isLocked
|
||||||
? 'border-gray-300 bg-gray-100 cursor-not-allowed'
|
? 'border-gray-300 bg-gray-100 cursor-not-allowed'
|
||||||
: 'border-[#92722A] bg-white'
|
: 'border-[#92722A] bg-white'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
{emailError && <p className="text-xs text-red-600 mt-1">{emailError}</p>}
|
{emailError && <p className="text-xs text-red-600 mt-1">{emailError}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Password
|
Password
|
||||||
@ -426,10 +391,10 @@ const Login = () => {
|
|||||||
}}
|
}}
|
||||||
disabled={isLocked}
|
disabled={isLocked}
|
||||||
className={`block w-full h-10 rounded-md border-2 px-4 pr-11 text-sm focus:ring-0 ${
|
className={`block w-full h-10 rounded-md border-2 px-4 pr-11 text-sm focus:ring-0 ${
|
||||||
passwordError
|
passwordError
|
||||||
? 'border-red-500 bg-red-50'
|
? 'border-red-500 bg-red-50'
|
||||||
: isLocked
|
: isLocked
|
||||||
? 'border-gray-300 bg-gray-100 cursor-not-allowed'
|
? 'border-gray-300 bg-gray-100 cursor-not-allowed'
|
||||||
: 'border-[#92722A] bg-white'
|
: 'border-[#92722A] bg-white'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
@ -441,7 +406,7 @@ const Login = () => {
|
|||||||
aria-label="Toggle password visibility"
|
aria-label="Toggle password visibility"
|
||||||
>
|
>
|
||||||
{showPassword ? (
|
{showPassword ? (
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-[#92722A]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-[#92722A]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8S1 12 1 12z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8S1 12 1 12z" />
|
||||||
<circle cx="12" cy="12" r="3" strokeWidth="2" />
|
<circle cx="12" cy="12" r="3" strokeWidth="2" />
|
||||||
@ -456,56 +421,7 @@ const Login = () => {
|
|||||||
</div>
|
</div>
|
||||||
{passwordError && <p className="text-xs text-red-600 mt-1">{passwordError}</p>}
|
{passwordError && <p className="text-xs text-red-600 mt-1">{passwordError}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4">
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
CAPTCHA Verification <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="flex justify-center items-center font-mono text-lg tracking-widest bg-gray-100 border-2 border-[#92722A] rounded-md px-4 py-2 select-none h-10">
|
|
||||||
{captcha}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={refreshCaptcha}
|
|
||||||
disabled={isLocked}
|
|
||||||
className={`flex items-center gap-1 ${
|
|
||||||
isLocked
|
|
||||||
? 'text-gray-400 cursor-not-allowed'
|
|
||||||
: 'text-[#92722A] hover:text-[#7b5c1f]'
|
|
||||||
}`}
|
|
||||||
title="Refresh CAPTCHA"
|
|
||||||
>
|
|
||||||
<RefreshCw className="h-4 w-4" />
|
|
||||||
<span className="text-sm leading-none">Refresh</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
|
||||||
Enter the characters shown above. Can't read it? Click refresh for a new code.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="captcha"
|
|
||||||
value={userCaptcha}
|
|
||||||
onChange={handleCaptchaChange}
|
|
||||||
placeholder="Type the characters"
|
|
||||||
disabled={isLocked}
|
|
||||||
className={`mt-2 block w-full h-10 rounded-md border-2 px-4 text-sm focus:ring-0 ${
|
|
||||||
captchaError
|
|
||||||
? 'border-red-500 bg-red-50'
|
|
||||||
: isLocked
|
|
||||||
? 'border-gray-300 bg-gray-100 cursor-not-allowed'
|
|
||||||
: 'border-[#92722A] bg-white'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
{captchaError && <p className="text-xs text-red-600 mt-1">{captchaError}</p>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<label className="inline-flex items-center gap-2 cursor-pointer">
|
<label className="inline-flex items-center gap-2 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
@ -527,13 +443,12 @@ const Login = () => {
|
|||||||
Forgot password?
|
Forgot password?
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={
|
disabled={
|
||||||
loading ||
|
loading ||
|
||||||
isLocked ||
|
isLocked
|
||||||
!userCaptcha.trim()
|
|
||||||
}
|
}
|
||||||
className={`inline-flex items-center justify-center w-full h-12 rounded-lg text-white font-medium ${
|
className={`inline-flex items-center justify-center w-full h-12 rounded-lg text-white font-medium ${
|
||||||
isLocked
|
isLocked
|
||||||
@ -543,10 +458,10 @@ const Login = () => {
|
|||||||
: 'bg-[#92722A] hover:bg-[#7b5c1f]'
|
: 'bg-[#92722A] hover:bg-[#7b5c1f]'
|
||||||
} transition-colors`}
|
} transition-colors`}
|
||||||
>
|
>
|
||||||
{isLocked
|
{isLocked
|
||||||
? `Locked (${formatTime(remainingTime)})`
|
? `Locked (${formatTime(remainingTime)})`
|
||||||
: loading
|
: loading
|
||||||
? 'Signing in…'
|
? 'Signing in…'
|
||||||
: 'Sign in'}
|
: 'Sign in'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@ -555,5 +470,6 @@ const Login = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Login;
|
export default Login;
|
||||||
|
|
||||||
@ -1,48 +1,32 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import apiClient from '@/services/api/apiClient';
|
import apiClient from '@/services/api/apiClient';
|
||||||
import { RefreshCw } from "lucide-react";
|
|
||||||
const logoSrc = '/assets/images/FCSCLogo.svg';
|
const logoSrc = '/assets/images/FCSCLogo.svg';
|
||||||
|
|
||||||
const PublicContactForm = () => {
|
const PublicContactForm = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
userType: "establishment_user",
|
userType: "establishment_user",
|
||||||
establishmentName: "",
|
establishmentName: "",
|
||||||
establishmentId: "",
|
establishmentId: "",
|
||||||
email: "",
|
email: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
const [isValid, setIsValid] = useState(false);
|
const [isValid, setIsValid] = useState(false);
|
||||||
const [captcha, setCaptcha] = useState("");
|
|
||||||
const [userCaptcha, setUserCaptcha] = useState("");
|
|
||||||
const [captchaError, setCaptchaError] = useState("");
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [success, setSuccess] = useState("");
|
const [success, setSuccess] = useState("");
|
||||||
const [errorMsg, setErrorMsg] = useState("");
|
const [errorMsg, setErrorMsg] = useState("");
|
||||||
const [otpStep, setOtpStep] = useState(false);
|
const [otpStep, setOtpStep] = useState(false);
|
||||||
const [otp, setOtp] = useState("");
|
const [otp, setOtp] = useState("");
|
||||||
const [otpMsg, setOtpMsg] = useState("");
|
const [otpMsg, setOtpMsg] = useState("");
|
||||||
const [otpError, setOtpError] = useState("");
|
const [otpError, setOtpError] = useState("");
|
||||||
const [toast, setToast] = useState(null);
|
const [toast, setToast] = useState(null);
|
||||||
|
|
||||||
const toastTimeoutRef = React.useRef(null);
|
const toastTimeoutRef = React.useRef(null);
|
||||||
|
|
||||||
const generateCaptcha = () => {
|
|
||||||
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
||||||
let code = "";
|
|
||||||
for (let i = 0; i < 6; i++) {
|
|
||||||
code += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
||||||
}
|
|
||||||
setCaptcha(code);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
generateCaptcha();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (toastTimeoutRef.current) {
|
if (toastTimeoutRef.current) {
|
||||||
@ -50,7 +34,7 @@ const PublicContactForm = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const showToast = (type, message) => {
|
const showToast = (type, message) => {
|
||||||
if (!message) return;
|
if (!message) return;
|
||||||
if (toastTimeoutRef.current) {
|
if (toastTimeoutRef.current) {
|
||||||
@ -63,7 +47,7 @@ const PublicContactForm = () => {
|
|||||||
toastTimeoutRef.current = null;
|
toastTimeoutRef.current = null;
|
||||||
}, 4000);
|
}, 4000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeToast = () => {
|
const closeToast = () => {
|
||||||
if (toastTimeoutRef.current) {
|
if (toastTimeoutRef.current) {
|
||||||
window.clearTimeout(toastTimeoutRef.current);
|
window.clearTimeout(toastTimeoutRef.current);
|
||||||
@ -71,52 +55,41 @@ const PublicContactForm = () => {
|
|||||||
}
|
}
|
||||||
setToast(null);
|
setToast(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const validate = (data = formData) => {
|
const validate = (data = formData) => {
|
||||||
const newErrors = {};
|
const newErrors = {};
|
||||||
|
|
||||||
if (!data.userType) {
|
if (!data.userType) {
|
||||||
newErrors.userType = "Please select user type";
|
newErrors.userType = "Please select user type";
|
||||||
} else if (data.userType === 'establishment_user') {
|
} else if (data.userType === 'establishment_user') {
|
||||||
if (!data.establishmentName.trim()) newErrors.establishmentName = "Required.";
|
if (!data.establishmentName.trim()) newErrors.establishmentName = "Required.";
|
||||||
if (!data.establishmentId.trim()) newErrors.establishmentId = "Required.";
|
if (!data.establishmentId.trim()) newErrors.establishmentId = "Required.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.email.trim()) newErrors.email = "Required.";
|
if (!data.email.trim()) newErrors.email = "Required.";
|
||||||
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email))
|
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email))
|
||||||
newErrors.email = "Enter a valid email address.";
|
newErrors.email = "Enter a valid email address.";
|
||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors);
|
||||||
setIsValid(Object.keys(newErrors).length === 0);
|
setIsValid(Object.keys(newErrors).length === 0);
|
||||||
return newErrors;
|
return newErrors;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
const updated = { ...formData, [name]: value };
|
const updated = { ...formData, [name]: value };
|
||||||
setFormData(updated);
|
setFormData(updated);
|
||||||
validate(updated);
|
validate(updated);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCaptchaChange = (e) => {
|
|
||||||
setUserCaptcha(e.target.value.toUpperCase());
|
|
||||||
setCaptchaError("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setErrorMsg("");
|
setErrorMsg("");
|
||||||
setSuccess("");
|
setSuccess("");
|
||||||
|
|
||||||
const validationErrors = validate();
|
const validationErrors = validate();
|
||||||
if (Object.keys(validationErrors).length > 0) return;
|
if (Object.keys(validationErrors).length > 0) return;
|
||||||
|
|
||||||
if (userCaptcha !== captcha) {
|
|
||||||
setCaptchaError("Incorrect CAPTCHA. Please try again.");
|
|
||||||
generateCaptcha();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
@ -125,14 +98,14 @@ const PublicContactForm = () => {
|
|||||||
establishment_code: formData.establishmentId.trim(),
|
establishment_code: formData.establishmentId.trim(),
|
||||||
registered_email: formData.email.trim(),
|
registered_email: formData.email.trim(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await apiClient.post("/forgot-password/request-otp", payload);
|
const response = await apiClient.post("/forgot-password/request-otp", payload);
|
||||||
|
|
||||||
if (response?.data) {
|
if (response?.data) {
|
||||||
showToast('success', 'Verification Code sent to your registered email successfully!');
|
showToast('success', 'Verification Code sent to your registered email successfully!');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate("/change-password", {
|
navigate("/change-password", {
|
||||||
state: {
|
state: {
|
||||||
email: formData.email.trim(),
|
email: formData.email.trim(),
|
||||||
user_type: formData.userType
|
user_type: formData.userType
|
||||||
},
|
},
|
||||||
@ -151,38 +124,38 @@ const PublicContactForm = () => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirmOtp = async () => {
|
const handleConfirmOtp = async () => {
|
||||||
if (!otp.trim()) {
|
if (!otp.trim()) {
|
||||||
setOtpError("Verification code is required");
|
setOtpError("Verification code is required");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setOtpError("");
|
setOtpError("");
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setErrorMsg("");
|
setErrorMsg("");
|
||||||
setSuccess("");
|
setSuccess("");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
registered_email: formData.email.trim(),
|
registered_email: formData.email.trim(),
|
||||||
otp: otp.trim(),
|
otp: otp.trim(),
|
||||||
user_type: formData.userType // Add user_type to the verification request
|
user_type: formData.userType // Add user_type to the verification request
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await apiClient.post("/forgot-password/verify-otp", payload);
|
const response = await apiClient.post("/forgot-password/verify-otp", payload);
|
||||||
|
|
||||||
if (response?.data?.status === "success") {
|
if (response?.data?.status === "success") {
|
||||||
setSuccess("Verification Code verified successfully. Redirecting to change password...");
|
setSuccess("Verification Code verified successfully. Redirecting to change password...");
|
||||||
setTimeout(() => navigate("/change-password", {
|
setTimeout(() => navigate("/change-password", {
|
||||||
state: {
|
state: {
|
||||||
email: formData.email.trim(),
|
email: formData.email.trim(),
|
||||||
user_type: formData.userType // Ensure user_type is passed
|
user_type: formData.userType // Ensure user_type is passed
|
||||||
}
|
}
|
||||||
}), 1500);
|
}), 1500);
|
||||||
} else {
|
} else {
|
||||||
const serverMsg = response?.data?.message?.toLowerCase() || "";
|
const serverMsg = response?.data?.message?.toLowerCase() || "";
|
||||||
|
|
||||||
if (serverMsg.includes("expired")) {
|
if (serverMsg.includes("expired")) {
|
||||||
setOtpError("Verification code has expired. Please request a new one.");
|
setOtpError("Verification code has expired. Please request a new one.");
|
||||||
} else if (serverMsg.includes("invalid")) {
|
} else if (serverMsg.includes("invalid")) {
|
||||||
@ -193,7 +166,7 @@ const PublicContactForm = () => {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const serverMsg = err.response?.data?.message?.toLowerCase() || "";
|
const serverMsg = err.response?.data?.message?.toLowerCase() || "";
|
||||||
|
|
||||||
if (serverMsg.includes("expired")) {
|
if (serverMsg.includes("expired")) {
|
||||||
setOtpError("Verification code has expired. Please request a new one.");
|
setOtpError("Verification code has expired. Please request a new one.");
|
||||||
} else if (serverMsg.includes("invalid")) {
|
} else if (serverMsg.includes("invalid")) {
|
||||||
@ -205,12 +178,12 @@ const PublicContactForm = () => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResendOtp = () => {
|
const handleResendOtp = () => {
|
||||||
setOtpMsg("A new Verification Code has been sent to your registered email.");
|
setOtpMsg("A new Verification Code has been sent to your registered email.");
|
||||||
setOtp("");
|
setOtp("");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#F7F7F7] flex items-center justify-center px-4">
|
<div className="min-h-screen bg-[#F7F7F7] flex items-center justify-center px-4">
|
||||||
{toast && (
|
{toast && (
|
||||||
@ -235,7 +208,7 @@ const PublicContactForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md">
|
||||||
<div className="bg-white rounded-lg shadow-sm ring-1 ring-gray-200 overflow-hidden">
|
<div className="bg-white rounded-lg shadow-sm ring-1 ring-gray-200 overflow-hidden">
|
||||||
<div className="px-7 py-8 border-b border-gray-200 bg-[#F2ECCF] text-center">
|
<div className="px-7 py-8 border-b border-gray-200 bg-[#F2ECCF] text-center">
|
||||||
@ -249,7 +222,7 @@ const PublicContactForm = () => {
|
|||||||
: "Enter the verification code sent to your registered email."}
|
: "Enter the verification code sent to your registered email."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!otpStep ? (
|
{!otpStep ? (
|
||||||
<form onSubmit={handleSubmit} className="px-7 py-8 space-y-4">
|
<form onSubmit={handleSubmit} className="px-7 py-8 space-y-4">
|
||||||
{errorMsg && (
|
{errorMsg && (
|
||||||
@ -257,7 +230,7 @@ const PublicContactForm = () => {
|
|||||||
{errorMsg}
|
{errorMsg}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
User Type <span className="text-red-500">*</span>
|
User Type <span className="text-red-500">*</span>
|
||||||
@ -282,7 +255,7 @@ const PublicContactForm = () => {
|
|||||||
{success}
|
{success}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{formData.userType === 'establishment_user' && (
|
{formData.userType === 'establishment_user' && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
@ -303,7 +276,7 @@ const PublicContactForm = () => {
|
|||||||
<p className="text-xs text-red-600 mt-1">{errors.establishmentName}</p>
|
<p className="text-xs text-red-600 mt-1">{errors.establishmentName}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Establishment ID <span className="text-red-500">*</span>
|
Establishment ID <span className="text-red-500">*</span>
|
||||||
@ -324,7 +297,7 @@ const PublicContactForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<InputField
|
<InputField
|
||||||
label="Registered Email"
|
label="Registered Email"
|
||||||
name="email"
|
name="email"
|
||||||
@ -334,56 +307,19 @@ const PublicContactForm = () => {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
error={errors.email}
|
error={errors.email}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mt-4">
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
CAPTCHA Verification <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div
|
|
||||||
className="flex justify-center items-center font-mono text-lg tracking-widest bg-gray-100 border-2 border-[#92722A] rounded-md px-4 py-2 select-none"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
{captcha}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={generateCaptcha}
|
|
||||||
className="flex items-center gap-1 text-sm text-[#92722A] hover:underline"
|
|
||||||
>
|
|
||||||
<RefreshCw size={14} className="text-[#92722A]" />
|
|
||||||
Refresh
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
|
||||||
Enter the characters in the image. Can't read it? Refresh for a new code.
|
|
||||||
<span className="font-mono"></span>
|
|
||||||
</p>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="captcha"
|
|
||||||
value={userCaptcha}
|
|
||||||
onChange={handleCaptchaChange}
|
|
||||||
placeholder="Type the characters"
|
|
||||||
className={`mt-2 block w-full h-10 rounded-md border-2 ${
|
|
||||||
captchaError ? "border-red-500" : "border-[#92722A]"
|
|
||||||
} focus:border-[#92722A] focus:ring-0 px-4 text-sm bg-white`}
|
|
||||||
/>
|
|
||||||
{captchaError && <p className="text-xs text-red-600 mt-1">{captchaError}</p>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isValid || loading || !userCaptcha}
|
disabled={!isValid || loading}
|
||||||
className={`inline-flex items-center justify-center w-full h-12 rounded-lg text-white ${
|
className={`inline-flex items-center justify-center w-full h-12 rounded-lg text-white ${
|
||||||
!isValid || loading || !userCaptcha
|
!isValid || loading
|
||||||
? "bg-[#bfa06a] cursor-not-allowed"
|
? "bg-[#bfa06a] cursor-not-allowed"
|
||||||
: "bg-[#92722A] hover:bg-[#7b5c1f]"
|
: "bg-[#92722A] hover:bg-[#7b5c1f]"
|
||||||
} transition-colors`}
|
} transition-colors`}
|
||||||
>
|
>
|
||||||
{loading ? "Submitting…" : "Submit"}
|
{loading ? "Submitting…" : "Submit"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigate(-1)}
|
onClick={() => navigate(-1)}
|
||||||
@ -399,7 +335,7 @@ const PublicContactForm = () => {
|
|||||||
{otpMsg}
|
{otpMsg}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<InputField
|
<InputField
|
||||||
label="Verification Code"
|
label="Verification Code"
|
||||||
name="otp"
|
name="otp"
|
||||||
@ -408,7 +344,7 @@ const PublicContactForm = () => {
|
|||||||
placeholder="Enter the 6-digit code"
|
placeholder="Enter the 6-digit code"
|
||||||
error={otpError}
|
error={otpError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={handleConfirmOtp}
|
onClick={handleConfirmOtp}
|
||||||
@ -430,7 +366,7 @@ const PublicContactForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const InputField = ({
|
const InputField = ({
|
||||||
label,
|
label,
|
||||||
name,
|
name,
|
||||||
@ -458,5 +394,5 @@ const InputField = ({
|
|||||||
{error && <p className="text-xs text-red-600 mt-1">{error}</p>}
|
{error && <p className="text-xs text-red-600 mt-1">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default PublicContactForm;
|
export default PublicContactForm;
|
||||||
Loading…
Reference in New Issue
Block a user