updates
This commit is contained in:
@@ -11,11 +11,14 @@ import { GlobalLoadingProvider } from './contexts/GlobalLoadingContext';
|
|||||||
import { CookieConsentProvider } from './contexts/CookieConsentContext';
|
import { CookieConsentProvider } from './contexts/CookieConsentContext';
|
||||||
import { CurrencyProvider } from './contexts/CurrencyContext';
|
import { CurrencyProvider } from './contexts/CurrencyContext';
|
||||||
import { CompanySettingsProvider } from './contexts/CompanySettingsContext';
|
import { CompanySettingsProvider } from './contexts/CompanySettingsContext';
|
||||||
|
import { AuthModalProvider } from './contexts/AuthModalContext';
|
||||||
import OfflineIndicator from './components/common/OfflineIndicator';
|
import OfflineIndicator from './components/common/OfflineIndicator';
|
||||||
import CookieConsentBanner from './components/common/CookieConsentBanner';
|
import CookieConsentBanner from './components/common/CookieConsentBanner';
|
||||||
import AnalyticsLoader from './components/common/AnalyticsLoader';
|
import AnalyticsLoader from './components/common/AnalyticsLoader';
|
||||||
import Loading from './components/common/Loading';
|
import Loading from './components/common/Loading';
|
||||||
import ScrollToTop from './components/common/ScrollToTop';
|
import ScrollToTop from './components/common/ScrollToTop';
|
||||||
|
import AuthModalManager from './components/modals/AuthModalManager';
|
||||||
|
import ResetPasswordRouteHandler from './components/ResetPasswordRouteHandler';
|
||||||
|
|
||||||
import useAuthStore from './store/useAuthStore';
|
import useAuthStore from './store/useAuthStore';
|
||||||
import useFavoritesStore from './store/useFavoritesStore';
|
import useFavoritesStore from './store/useFavoritesStore';
|
||||||
@@ -50,10 +53,6 @@ const InvoicePage = lazy(() => import('./pages/customer/InvoicePage'));
|
|||||||
const ProfilePage = lazy(() => import('./pages/customer/ProfilePage'));
|
const ProfilePage = lazy(() => import('./pages/customer/ProfilePage'));
|
||||||
const AboutPage = lazy(() => import('./pages/AboutPage'));
|
const AboutPage = lazy(() => import('./pages/AboutPage'));
|
||||||
const ContactPage = lazy(() => import('./pages/ContactPage'));
|
const ContactPage = lazy(() => import('./pages/ContactPage'));
|
||||||
const LoginPage = lazy(() => import('./pages/auth/LoginPage'));
|
|
||||||
const RegisterPage = lazy(() => import('./pages/auth/RegisterPage'));
|
|
||||||
const ForgotPasswordPage = lazy(() => import('./pages/auth/ForgotPasswordPage'));
|
|
||||||
const ResetPasswordPage = lazy(() => import('./pages/auth/ResetPasswordPage'));
|
|
||||||
|
|
||||||
const AdminDashboardPage = lazy(() => import('./pages/admin/DashboardPage'));
|
const AdminDashboardPage = lazy(() => import('./pages/admin/DashboardPage'));
|
||||||
const InvoiceManagementPage = lazy(() => import('./pages/admin/InvoiceManagementPage'));
|
const InvoiceManagementPage = lazy(() => import('./pages/admin/InvoiceManagementPage'));
|
||||||
@@ -69,17 +68,7 @@ const ReceptionDashboardPage = lazy(() => import('./pages/admin/ReceptionDashboa
|
|||||||
const StaffDashboardPage = lazy(() => import('./pages/staff/DashboardPage'));
|
const StaffDashboardPage = lazy(() => import('./pages/staff/DashboardPage'));
|
||||||
const ChatManagementPage = lazy(() => import('./pages/staff/ChatManagementPage'));
|
const ChatManagementPage = lazy(() => import('./pages/staff/ChatManagementPage'));
|
||||||
const StaffLayout = lazy(() => import('./pages/StaffLayout'));
|
const StaffLayout = lazy(() => import('./pages/StaffLayout'));
|
||||||
|
const NotFoundPage = lazy(() => import('./pages/NotFoundPage'));
|
||||||
const DemoPage: React.FC<{ title: string }> = ({ title }) => (
|
|
||||||
<div className="container mx-auto px-4 py-8">
|
|
||||||
<h1 className="text-3xl font-bold text-gray-800">
|
|
||||||
{title}
|
|
||||||
</h1>
|
|
||||||
<p className="text-gray-600 mt-4">
|
|
||||||
This page is under development...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
||||||
@@ -129,6 +118,7 @@ function App() {
|
|||||||
<CookieConsentProvider>
|
<CookieConsentProvider>
|
||||||
<CurrencyProvider>
|
<CurrencyProvider>
|
||||||
<CompanySettingsProvider>
|
<CompanySettingsProvider>
|
||||||
|
<AuthModalProvider>
|
||||||
<BrowserRouter
|
<BrowserRouter
|
||||||
future={{
|
future={{
|
||||||
v7_startTransition: true,
|
v7_startTransition: true,
|
||||||
@@ -275,21 +265,9 @@ function App() {
|
|||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{}
|
{}
|
||||||
<Route
|
|
||||||
path="/login"
|
|
||||||
element={<LoginPage />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/register"
|
|
||||||
element={<RegisterPage />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/forgot-password"
|
|
||||||
element={<ForgotPasswordPage />}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="/reset-password/:token"
|
path="/reset-password/:token"
|
||||||
element={<ResetPasswordPage />}
|
element={<ResetPasswordRouteHandler />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{}
|
{}
|
||||||
@@ -383,7 +361,7 @@ function App() {
|
|||||||
{}
|
{}
|
||||||
<Route
|
<Route
|
||||||
path="*"
|
path="*"
|
||||||
element={<DemoPage title="404 - Page not found" />}
|
element={<NotFoundPage />}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|
||||||
@@ -404,8 +382,10 @@ function App() {
|
|||||||
<OfflineIndicator />
|
<OfflineIndicator />
|
||||||
<CookieConsentBanner />
|
<CookieConsentBanner />
|
||||||
<AnalyticsLoader />
|
<AnalyticsLoader />
|
||||||
|
<AuthModalManager />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
</AuthModalProvider>
|
||||||
</CompanySettingsProvider>
|
</CompanySettingsProvider>
|
||||||
</CurrencyProvider>
|
</CurrencyProvider>
|
||||||
</CookieConsentProvider>
|
</CookieConsentProvider>
|
||||||
|
|||||||
22
Frontend/src/components/ResetPasswordRouteHandler.tsx
Normal file
22
Frontend/src/components/ResetPasswordRouteHandler.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import { useAuthModal } from '../contexts/AuthModalContext';
|
||||||
|
|
||||||
|
const ResetPasswordRouteHandler: React.FC = () => {
|
||||||
|
const { token } = useParams<{ token: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { openModal } = useAuthModal();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (token) {
|
||||||
|
openModal('reset-password', { token });
|
||||||
|
// Navigate to home to keep user on current page
|
||||||
|
navigate('/', { replace: true });
|
||||||
|
}
|
||||||
|
}, [token, openModal, navigate]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResetPasswordRouteHandler;
|
||||||
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Navigate, useLocation } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
import useAuthStore from '../../store/useAuthStore';
|
import useAuthStore from '../../store/useAuthStore';
|
||||||
|
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||||
|
|
||||||
interface AdminRouteProps {
|
interface AdminRouteProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -9,10 +10,16 @@ interface AdminRouteProps {
|
|||||||
const AdminRoute: React.FC<AdminRouteProps> = ({
|
const AdminRoute: React.FC<AdminRouteProps> = ({
|
||||||
children
|
children
|
||||||
}) => {
|
}) => {
|
||||||
const location = useLocation();
|
|
||||||
const { isAuthenticated, userInfo, isLoading } = useAuthStore();
|
const { isAuthenticated, userInfo, isLoading } = useAuthStore();
|
||||||
|
const { openModal } = useAuthModal();
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading && !isAuthenticated) {
|
||||||
|
openModal('login');
|
||||||
|
}
|
||||||
|
}, [isLoading, isAuthenticated, openModal]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -34,13 +41,7 @@ const AdminRoute: React.FC<AdminRouteProps> = ({
|
|||||||
|
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return null; // Modal will be shown by AuthModalManager
|
||||||
<Navigate
|
|
||||||
to="/login"
|
|
||||||
state={{ from: location }}
|
|
||||||
replace
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Navigate, useLocation } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
import useAuthStore from '../../store/useAuthStore';
|
import useAuthStore from '../../store/useAuthStore';
|
||||||
|
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||||
|
|
||||||
interface CustomerRouteProps {
|
interface CustomerRouteProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -9,10 +10,16 @@ interface CustomerRouteProps {
|
|||||||
const CustomerRoute: React.FC<CustomerRouteProps> = ({
|
const CustomerRoute: React.FC<CustomerRouteProps> = ({
|
||||||
children
|
children
|
||||||
}) => {
|
}) => {
|
||||||
const location = useLocation();
|
|
||||||
const { isAuthenticated, userInfo, isLoading } = useAuthStore();
|
const { isAuthenticated, userInfo, isLoading } = useAuthStore();
|
||||||
|
const { openModal } = useAuthModal();
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading && !isAuthenticated) {
|
||||||
|
openModal('login');
|
||||||
|
}
|
||||||
|
}, [isLoading, isAuthenticated, openModal]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -34,13 +41,7 @@ const CustomerRoute: React.FC<CustomerRouteProps> = ({
|
|||||||
|
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return null; // Modal will be shown by AuthModalManager
|
||||||
<Navigate
|
|
||||||
to="/login"
|
|
||||||
state={{ from: location }}
|
|
||||||
replace
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Navigate, useLocation } from 'react-router-dom';
|
|
||||||
import useAuthStore from '../../store/useAuthStore';
|
import useAuthStore from '../../store/useAuthStore';
|
||||||
|
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||||
|
|
||||||
interface ProtectedRouteProps {
|
interface ProtectedRouteProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -9,8 +9,15 @@ interface ProtectedRouteProps {
|
|||||||
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||||
children
|
children
|
||||||
}) => {
|
}) => {
|
||||||
const location = useLocation();
|
|
||||||
const { isAuthenticated, isLoading } = useAuthStore();
|
const { isAuthenticated, isLoading } = useAuthStore();
|
||||||
|
const { openModal } = useAuthModal();
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading && !isAuthenticated) {
|
||||||
|
openModal('login');
|
||||||
|
}
|
||||||
|
}, [isLoading, isAuthenticated, openModal]);
|
||||||
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -34,13 +41,7 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
|||||||
|
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return null; // Modal will be shown by AuthModalManager
|
||||||
<Navigate
|
|
||||||
to="/login"
|
|
||||||
state={{ from: location }}
|
|
||||||
replace
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Navigate, useLocation } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
import useAuthStore from '../../store/useAuthStore';
|
import useAuthStore from '../../store/useAuthStore';
|
||||||
|
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||||
|
|
||||||
interface StaffRouteProps {
|
interface StaffRouteProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -9,10 +10,16 @@ interface StaffRouteProps {
|
|||||||
const StaffRoute: React.FC<StaffRouteProps> = ({
|
const StaffRoute: React.FC<StaffRouteProps> = ({
|
||||||
children
|
children
|
||||||
}) => {
|
}) => {
|
||||||
const location = useLocation();
|
|
||||||
const { isAuthenticated, userInfo, isLoading } = useAuthStore();
|
const { isAuthenticated, userInfo, isLoading } = useAuthStore();
|
||||||
|
const { openModal } = useAuthModal();
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading && !isAuthenticated) {
|
||||||
|
openModal('login');
|
||||||
|
}
|
||||||
|
}, [isLoading, isAuthenticated, openModal]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -34,13 +41,7 @@ const StaffRoute: React.FC<StaffRouteProps> = ({
|
|||||||
|
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return null; // Modal will be shown by AuthModalManager
|
||||||
<Navigate
|
|
||||||
to="/login"
|
|
||||||
state={{ from: location }}
|
|
||||||
replace
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useClickOutside } from '../../hooks/useClickOutside';
|
import { useClickOutside } from '../../hooks/useClickOutside';
|
||||||
import { useCompanySettings } from '../../contexts/CompanySettingsContext';
|
import { useCompanySettings } from '../../contexts/CompanySettingsContext';
|
||||||
|
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||||
import { normalizeImageUrl } from '../../utils/imageUtils';
|
import { normalizeImageUrl } from '../../utils/imageUtils';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
@@ -34,6 +35,7 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
onLogout
|
onLogout
|
||||||
}) => {
|
}) => {
|
||||||
const { settings } = useCompanySettings();
|
const { settings } = useCompanySettings();
|
||||||
|
const { openModal } = useAuthModal();
|
||||||
|
|
||||||
|
|
||||||
const displayPhone = settings.company_phone || '+1 (234) 567-890';
|
const displayPhone = settings.company_phone || '+1 (234) 567-890';
|
||||||
@@ -174,8 +176,8 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
>
|
>
|
||||||
{!isAuthenticated ? (
|
{!isAuthenticated ? (
|
||||||
<>
|
<>
|
||||||
<Link
|
<button
|
||||||
to="/login"
|
onClick={() => openModal('login')}
|
||||||
className="flex items-center space-x-2
|
className="flex items-center space-x-2
|
||||||
px-5 py-2 text-white/90
|
px-5 py-2 text-white/90
|
||||||
hover:text-[#d4af37] transition-all duration-300
|
hover:text-[#d4af37] transition-all duration-300
|
||||||
@@ -184,9 +186,9 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
<LogIn className="w-4 h-4 relative z-10" />
|
<LogIn className="w-4 h-4 relative z-10" />
|
||||||
<span className="relative z-10">Login</span>
|
<span className="relative z-10">Login</span>
|
||||||
<span className="absolute inset-0 border border-[#d4af37]/30 rounded-sm opacity-0 group-hover:opacity-100 transition-opacity duration-300"></span>
|
<span className="absolute inset-0 border border-[#d4af37]/30 rounded-sm opacity-0 group-hover:opacity-100 transition-opacity duration-300"></span>
|
||||||
</Link>
|
</button>
|
||||||
<Link
|
<button
|
||||||
to="/register"
|
onClick={() => openModal('register')}
|
||||||
className="flex items-center space-x-2
|
className="flex items-center space-x-2
|
||||||
px-6 py-2.5 bg-gradient-to-r from-[#d4af37] to-[#c9a227]
|
px-6 py-2.5 bg-gradient-to-r from-[#d4af37] to-[#c9a227]
|
||||||
text-[#0f0f0f] rounded-sm hover:from-[#f5d76e]
|
text-[#0f0f0f] rounded-sm hover:from-[#f5d76e]
|
||||||
@@ -197,7 +199,7 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
<span className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/20 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700"></span>
|
<span className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/20 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700"></span>
|
||||||
<UserPlus className="w-4 h-4 relative z-10" />
|
<UserPlus className="w-4 h-4 relative z-10" />
|
||||||
<span className="relative z-10">Register</span>
|
<span className="relative z-10">Register</span>
|
||||||
</Link>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative" ref={userMenuRef}>
|
<div className="relative" ref={userMenuRef}>
|
||||||
@@ -402,37 +404,37 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
>
|
>
|
||||||
{!isAuthenticated ? (
|
{!isAuthenticated ? (
|
||||||
<>
|
<>
|
||||||
<Link
|
<button
|
||||||
to="/login"
|
onClick={() => {
|
||||||
onClick={() =>
|
setIsMobileMenuOpen(false);
|
||||||
setIsMobileMenuOpen(false)
|
openModal('login');
|
||||||
}
|
}}
|
||||||
className="flex items-center
|
className="flex items-center
|
||||||
space-x-2 px-4 py-3 text-white/90
|
space-x-2 px-4 py-3 text-white/90
|
||||||
hover:bg-[#d4af37]/10 hover:text-[#d4af37]
|
hover:bg-[#d4af37]/10 hover:text-[#d4af37]
|
||||||
rounded-sm transition-all duration-300
|
rounded-sm transition-all duration-300
|
||||||
border-l-2 border-transparent
|
border-l-2 border-transparent
|
||||||
hover:border-[#d4af37] font-light tracking-wide"
|
hover:border-[#d4af37] font-light tracking-wide w-full text-left"
|
||||||
>
|
>
|
||||||
<LogIn className="w-4 h-4" />
|
<LogIn className="w-4 h-4" />
|
||||||
<span>Login</span>
|
<span>Login</span>
|
||||||
</Link>
|
</button>
|
||||||
<Link
|
<button
|
||||||
to="/register"
|
onClick={() => {
|
||||||
onClick={() =>
|
setIsMobileMenuOpen(false);
|
||||||
setIsMobileMenuOpen(false)
|
openModal('register');
|
||||||
}
|
}}
|
||||||
className="flex items-center
|
className="flex items-center
|
||||||
space-x-2 px-4 py-3 bg-gradient-to-r
|
space-x-2 px-4 py-3 bg-gradient-to-r
|
||||||
from-[#d4af37] to-[#c9a227] text-[#0f0f0f]
|
from-[#d4af37] to-[#c9a227] text-[#0f0f0f]
|
||||||
rounded-sm hover:from-[#f5d76e]
|
rounded-sm hover:from-[#f5d76e]
|
||||||
hover:to-[#d4af37] transition-all
|
hover:to-[#d4af37] transition-all
|
||||||
duration-300 font-medium tracking-wide
|
duration-300 font-medium tracking-wide
|
||||||
mt-2"
|
mt-2 w-full text-left"
|
||||||
>
|
>
|
||||||
<UserPlus className="w-4 h-4" />
|
<UserPlus className="w-4 h-4" />
|
||||||
<span>Register</span>
|
<span>Register</span>
|
||||||
</Link>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const SidebarAdmin: React.FC<SidebarAdminProps> = ({
|
|||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
await logout();
|
await logout();
|
||||||
navigate('/login');
|
navigate('/');
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
setIsMobileOpen(false);
|
setIsMobileOpen(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const SidebarStaff: React.FC<SidebarStaffProps> = ({
|
|||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
await logout();
|
await logout();
|
||||||
navigate('/login');
|
navigate('/');
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
setIsMobileOpen(false);
|
setIsMobileOpen(false);
|
||||||
}
|
}
|
||||||
|
|||||||
30
Frontend/src/components/modals/AuthModalManager.tsx
Normal file
30
Frontend/src/components/modals/AuthModalManager.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||||
|
import LoginModal from './LoginModal';
|
||||||
|
import RegisterModal from './RegisterModal';
|
||||||
|
import ForgotPasswordModal from './ForgotPasswordModal';
|
||||||
|
import ResetPasswordModal from './ResetPasswordModal';
|
||||||
|
|
||||||
|
const AuthModalManager: React.FC = () => {
|
||||||
|
const { isOpen, modalType, resetPasswordParams } = useAuthModal();
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (modalType) {
|
||||||
|
case 'login':
|
||||||
|
return <LoginModal />;
|
||||||
|
case 'register':
|
||||||
|
return <RegisterModal />;
|
||||||
|
case 'forgot-password':
|
||||||
|
return <ForgotPasswordModal />;
|
||||||
|
case 'reset-password':
|
||||||
|
return resetPasswordParams?.token ? <ResetPasswordModal token={resetPasswordParams.token} /> : null;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthModalManager;
|
||||||
|
|
||||||
277
Frontend/src/components/modals/ForgotPasswordModal.tsx
Normal file
277
Frontend/src/components/modals/ForgotPasswordModal.tsx
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import { X, Mail, ArrowLeft, Send, Loader2, CheckCircle } from 'lucide-react';
|
||||||
|
import useAuthStore from '../../store/useAuthStore';
|
||||||
|
import { forgotPasswordSchema, ForgotPasswordFormData } from '../../utils/validationSchemas';
|
||||||
|
import { useCompanySettings } from '../../contexts/CompanySettingsContext';
|
||||||
|
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||||
|
|
||||||
|
const ForgotPasswordModal: React.FC = () => {
|
||||||
|
const { closeModal, openModal } = useAuthModal();
|
||||||
|
const { forgotPassword, isLoading, error, clearError } = useAuthStore();
|
||||||
|
const { settings } = useCompanySettings();
|
||||||
|
|
||||||
|
const [isSuccess, setIsSuccess] = useState(false);
|
||||||
|
const [submittedEmail, setSubmittedEmail] = useState('');
|
||||||
|
|
||||||
|
const supportEmail = settings.company_email || 'support@hotel.com';
|
||||||
|
const supportPhone = settings.company_phone || '1900-xxxx';
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<ForgotPasswordFormData>({
|
||||||
|
resolver: yupResolver(forgotPasswordSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: ForgotPasswordFormData) => {
|
||||||
|
try {
|
||||||
|
clearError();
|
||||||
|
setSubmittedEmail(data.email);
|
||||||
|
await forgotPassword({ email: data.email });
|
||||||
|
setIsSuccess(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Forgot password error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle escape key
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', handleEscape);
|
||||||
|
return () => document.removeEventListener('keydown', handleEscape);
|
||||||
|
}, [closeModal]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[9999] flex items-center justify-center p-3 sm:p-4 md:p-6"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<div className="relative w-full max-w-md max-h-[95vh] overflow-y-auto bg-gradient-to-br from-gray-50 via-white to-gray-50 rounded-lg shadow-2xl border border-[#d4af37]/20">
|
||||||
|
{/* Close button */}
|
||||||
|
<button
|
||||||
|
onClick={closeModal}
|
||||||
|
className="absolute top-3 right-3 z-10 p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-500 hover:text-gray-900"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="p-4 sm:p-6 lg:p-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center mb-4 sm:mb-6">
|
||||||
|
<div className="flex justify-center mb-3 sm:mb-4">
|
||||||
|
{settings.company_logo_url ? (
|
||||||
|
<img
|
||||||
|
src={settings.company_logo_url.startsWith('http')
|
||||||
|
? settings.company_logo_url
|
||||||
|
: `${import.meta.env.VITE_API_URL || 'http://localhost:8000'}${settings.company_logo_url}`}
|
||||||
|
alt={settings.company_name || 'Logo'}
|
||||||
|
className="h-12 sm:h-14 lg:h-16 w-auto max-w-[120px] sm:max-w-[150px] object-contain"
|
||||||
|
style={{ filter: 'drop-shadow(0 4px 6px rgba(0,0,0,0.1))' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="relative p-2.5 sm:p-3 bg-gradient-to-br from-[#d4af37] to-[#c9a227] rounded-full shadow-lg shadow-[#d4af37]/30">
|
||||||
|
<Mail className="w-6 h-6 sm:w-8 sm:h-8 text-[#0f0f0f] relative z-10" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{settings.company_tagline && (
|
||||||
|
<p className="text-[10px] sm:text-xs text-[#d4af37] uppercase tracking-[1.5px] sm:tracking-[2px] mb-1 sm:mb-2 font-light">
|
||||||
|
{settings.company_tagline}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<h2 className="text-xl sm:text-2xl lg:text-3xl font-serif font-semibold text-gray-900 tracking-tight">
|
||||||
|
Forgot Password?
|
||||||
|
</h2>
|
||||||
|
<p className="mt-1 sm:mt-2 text-xs sm:text-sm text-gray-600 font-light tracking-wide">
|
||||||
|
Enter your email to receive a password reset link for {settings.company_name || 'Luxury Hotel'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isSuccess ? (
|
||||||
|
<div className="text-center space-y-4 sm:space-y-5">
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div className="w-12 h-12 sm:w-14 sm:h-14 lg:w-16 lg:h-16 bg-green-100 rounded-full flex items-center justify-center">
|
||||||
|
<CheckCircle className="w-6 h-6 sm:w-8 sm:h-8 lg:w-10 lg:h-10 text-green-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5 sm:space-y-2">
|
||||||
|
<h3 className="text-lg sm:text-xl font-semibold text-gray-900">
|
||||||
|
Email Sent!
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs sm:text-sm text-gray-600">
|
||||||
|
We have sent a password reset link to
|
||||||
|
</p>
|
||||||
|
<p className="text-xs sm:text-sm font-medium text-[#d4af37] break-all">
|
||||||
|
{submittedEmail}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3 sm:p-4 text-left">
|
||||||
|
<p className="text-xs sm:text-sm text-gray-700">
|
||||||
|
<strong>Note:</strong>
|
||||||
|
</p>
|
||||||
|
<ul className="mt-2 space-y-1 text-xs sm:text-sm text-gray-600 list-disc list-inside">
|
||||||
|
<li>Link is valid for 1 hour</li>
|
||||||
|
<li>Check your Spam/Junk folder</li>
|
||||||
|
<li>If you don't receive the email, please try again</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2.5 sm:space-y-3">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsSuccess(false);
|
||||||
|
clearError();
|
||||||
|
}}
|
||||||
|
className="w-full flex items-center justify-center py-2.5 sm:py-3 px-4 border border-gray-300 rounded-lg text-xs sm:text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#d4af37] transition-colors"
|
||||||
|
>
|
||||||
|
<Mail className="-ml-1 mr-2 h-4 w-4 sm:h-5 sm:w-5" />
|
||||||
|
Resend Email
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => openModal('login')}
|
||||||
|
className="w-full flex items-center justify-center py-2.5 sm:py-3 px-4 border border-transparent rounded-lg text-xs sm:text-sm font-medium text-white bg-gradient-to-r from-[#d4af37] to-[#c9a227] hover:from-[#f5d76e] hover:to-[#d4af37] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#d4af37] transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="-ml-1 mr-2 h-4 w-4 sm:h-5 sm:w-5" />
|
||||||
|
Back to Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 sm:space-y-5">
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 border border-red-200 text-red-700 px-3 sm:px-4 py-2.5 sm:py-3 rounded-lg text-xs sm:text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email" className="block text-xs sm:text-sm font-medium text-gray-700 mb-1.5 sm:mb-2">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Mail className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
{...register('email')}
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
autoComplete="email"
|
||||||
|
autoFocus
|
||||||
|
className={`block w-full pl-9 sm:pl-10 pr-3 py-2.5 sm:py-3 border rounded-lg focus:outline-none focus:ring-2 transition-colors text-sm sm:text-base luxury-input ${
|
||||||
|
errors.email
|
||||||
|
? 'border-red-300 focus:ring-red-500'
|
||||||
|
: 'border-gray-300 focus:ring-[#d4af37]'
|
||||||
|
}`}
|
||||||
|
placeholder="email@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.email && (
|
||||||
|
<p className="mt-1 text-xs sm:text-sm text-red-600">
|
||||||
|
{errors.email.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full flex items-center justify-center py-2.5 sm:py-3 px-4 border border-transparent rounded-lg shadow-sm text-xs sm:text-sm font-medium text-white bg-gradient-to-r from-[#d4af37] to-[#c9a227] hover:from-[#f5d76e] hover:to-[#d4af37] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#d4af37] disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="animate-spin -ml-1 mr-2 h-4 w-4 sm:h-5 sm:w-5" />
|
||||||
|
Processing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Send className="-ml-1 mr-2 h-4 w-4 sm:h-5 sm:w-5" />
|
||||||
|
Send Reset Link
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => openModal('login')}
|
||||||
|
className="inline-flex items-center text-xs sm:text-sm font-medium text-[#d4af37] hover:text-[#c9a227] transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="mr-1 h-3.5 w-3.5 sm:h-4 sm:w-4" />
|
||||||
|
Back to Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isSuccess && (
|
||||||
|
<div className="mt-4 sm:mt-6 text-center">
|
||||||
|
<p className="text-xs sm:text-sm text-gray-600 font-light tracking-wide">
|
||||||
|
Don't have an account?{' '}
|
||||||
|
<button
|
||||||
|
onClick={() => openModal('register')}
|
||||||
|
className="font-medium text-[#d4af37] hover:text-[#c9a227] transition-colors"
|
||||||
|
>
|
||||||
|
Register now
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isSuccess && (
|
||||||
|
<div className="mt-4 bg-white rounded-lg shadow-sm border border-gray-200 p-3 sm:p-4">
|
||||||
|
<h3 className="text-xs sm:text-sm font-semibold text-gray-900 mb-1.5 sm:mb-2">
|
||||||
|
Need Help?
|
||||||
|
</h3>
|
||||||
|
<p className="text-[10px] sm:text-xs text-gray-600 leading-relaxed">
|
||||||
|
If you're having trouble resetting your password, please contact our support team via email{' '}
|
||||||
|
<a
|
||||||
|
href={`mailto:${supportEmail}`}
|
||||||
|
className="text-[#d4af37] hover:underline break-all"
|
||||||
|
>
|
||||||
|
{supportEmail}
|
||||||
|
</a>
|
||||||
|
{supportPhone && (
|
||||||
|
<>
|
||||||
|
{' '}or hotline{' '}
|
||||||
|
<a
|
||||||
|
href={`tel:${supportPhone.replace(/\s+/g, '').replace(/[()]/g, '')}`}
|
||||||
|
className="text-[#d4af37] hover:underline"
|
||||||
|
>
|
||||||
|
{supportPhone}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ForgotPasswordModal;
|
||||||
|
|
||||||
390
Frontend/src/components/modals/LoginModal.tsx
Normal file
390
Frontend/src/components/modals/LoginModal.tsx
Normal file
@@ -0,0 +1,390 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import { X, Eye, EyeOff, LogIn, Loader2, Mail, Lock, Shield, ArrowLeft } from 'lucide-react';
|
||||||
|
import useAuthStore from '../../store/useAuthStore';
|
||||||
|
import { loginSchema, LoginFormData } from '../../utils/validationSchemas';
|
||||||
|
import { useCompanySettings } from '../../contexts/CompanySettingsContext';
|
||||||
|
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import Recaptcha from '../common/Recaptcha';
|
||||||
|
import { recaptchaService } from '../../services/api/systemSettingsService';
|
||||||
|
|
||||||
|
const mfaTokenSchema = yup.object().shape({
|
||||||
|
mfaToken: yup
|
||||||
|
.string()
|
||||||
|
.required('MFA token is required')
|
||||||
|
.min(6, 'MFA token must be 6 digits')
|
||||||
|
.max(8, 'MFA token must be 6-8 characters')
|
||||||
|
.matches(/^\d+$|^[A-Z0-9]{8}$/, 'Invalid token format'),
|
||||||
|
});
|
||||||
|
|
||||||
|
type MFATokenFormData = yup.InferType<typeof mfaTokenSchema>;
|
||||||
|
|
||||||
|
const LoginModal: React.FC = () => {
|
||||||
|
const { closeModal, openModal } = useAuthModal();
|
||||||
|
const { login, verifyMFA, isLoading, error, clearError, requiresMFA, clearMFA, isAuthenticated } = useAuthStore();
|
||||||
|
const { settings } = useCompanySettings();
|
||||||
|
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [recaptchaToken, setRecaptchaToken] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
register: registerMFA,
|
||||||
|
handleSubmit: handleSubmitMFA,
|
||||||
|
formState: { errors: mfaErrors },
|
||||||
|
} = useForm<MFATokenFormData>({
|
||||||
|
resolver: yupResolver(mfaTokenSchema),
|
||||||
|
defaultValues: {
|
||||||
|
mfaToken: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal on successful authentication
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading && isAuthenticated && !requiresMFA) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
}, [isLoading, isAuthenticated, requiresMFA, closeModal]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<LoginFormData>({
|
||||||
|
resolver: yupResolver(loginSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
rememberMe: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: LoginFormData) => {
|
||||||
|
try {
|
||||||
|
clearError();
|
||||||
|
|
||||||
|
if (recaptchaToken) {
|
||||||
|
try {
|
||||||
|
const verifyResponse = await recaptchaService.verifyRecaptcha(recaptchaToken);
|
||||||
|
if (verifyResponse.status === 'error' || !verifyResponse.data.verified) {
|
||||||
|
toast.error('reCAPTCHA verification failed. Please try again.');
|
||||||
|
setRecaptchaToken(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('reCAPTCHA verification failed. Please try again.');
|
||||||
|
setRecaptchaToken(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await login({
|
||||||
|
email: data.email,
|
||||||
|
password: data.password,
|
||||||
|
rememberMe: data.rememberMe,
|
||||||
|
});
|
||||||
|
|
||||||
|
setRecaptchaToken(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
setRecaptchaToken(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmitMFA = async (data: MFATokenFormData) => {
|
||||||
|
try {
|
||||||
|
clearError();
|
||||||
|
await verifyMFA(data.mfaToken);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('MFA verification error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackToLogin = () => {
|
||||||
|
clearMFA();
|
||||||
|
clearError();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle escape key
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', handleEscape);
|
||||||
|
return () => document.removeEventListener('keydown', handleEscape);
|
||||||
|
}, [closeModal]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[9999] flex items-center justify-center p-3 sm:p-4 md:p-6"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<div className="relative w-full max-w-md max-h-[95vh] overflow-y-auto bg-gradient-to-br from-gray-50 via-white to-gray-50 rounded-lg shadow-2xl border border-[#d4af37]/20">
|
||||||
|
{/* Close button */}
|
||||||
|
<button
|
||||||
|
onClick={closeModal}
|
||||||
|
className="absolute top-3 right-3 z-10 p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-500 hover:text-gray-900"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="p-4 sm:p-6 lg:p-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center mb-4 sm:mb-6">
|
||||||
|
<div className="flex justify-center mb-3 sm:mb-4">
|
||||||
|
{settings.company_logo_url ? (
|
||||||
|
<img
|
||||||
|
src={settings.company_logo_url.startsWith('http')
|
||||||
|
? settings.company_logo_url
|
||||||
|
: `${import.meta.env.VITE_API_URL || 'http://localhost:8000'}${settings.company_logo_url}`}
|
||||||
|
alt={settings.company_name || 'Logo'}
|
||||||
|
className="h-12 sm:h-14 lg:h-16 w-auto max-w-[120px] sm:max-w-[150px] object-contain"
|
||||||
|
style={{ filter: 'drop-shadow(0 4px 6px rgba(0,0,0,0.1))' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="relative p-2.5 sm:p-3 bg-gradient-to-br from-[#d4af37] to-[#c9a227] rounded-full shadow-lg shadow-[#d4af37]/30">
|
||||||
|
<Shield className="w-6 h-6 sm:w-8 sm:h-8 text-[#0f0f0f] relative z-10" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{settings.company_tagline && (
|
||||||
|
<p className="text-[10px] sm:text-xs text-[#d4af37] uppercase tracking-[1.5px] sm:tracking-[2px] mb-1 sm:mb-2 font-light">
|
||||||
|
{settings.company_tagline}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<h2 className="text-xl sm:text-2xl lg:text-3xl font-serif font-semibold text-gray-900 tracking-tight">
|
||||||
|
{requiresMFA ? 'Verify Your Identity' : 'Welcome Back'}
|
||||||
|
</h2>
|
||||||
|
<p className="mt-1 sm:mt-2 text-xs sm:text-sm text-gray-600 font-light tracking-wide">
|
||||||
|
{requiresMFA
|
||||||
|
? 'Enter the 6-digit code from your authenticator app'
|
||||||
|
: `Sign in to ${settings.company_name || 'Luxury Hotel'}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{requiresMFA ? (
|
||||||
|
<form onSubmit={handleSubmitMFA(onSubmitMFA)} className="space-y-4 sm:space-y-5">
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50/80 backdrop-blur-sm border border-red-200 text-red-700 px-4 py-3 rounded-sm text-sm font-light">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="mfaToken" className="block text-xs sm:text-sm font-medium text-gray-700 mb-1.5 sm:mb-2 tracking-wide">
|
||||||
|
Authentication Code
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Shield className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
{...registerMFA('mfaToken')}
|
||||||
|
id="mfaToken"
|
||||||
|
type="text"
|
||||||
|
autoComplete="one-time-code"
|
||||||
|
maxLength={8}
|
||||||
|
className={`luxury-input pl-9 sm:pl-10 py-2.5 sm:py-3 text-sm sm:text-base text-center tracking-widest w-full ${
|
||||||
|
mfaErrors.mfaToken ? 'border-red-300 focus:ring-red-500 focus:border-red-500' : ''
|
||||||
|
}`}
|
||||||
|
placeholder="000000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{mfaErrors.mfaToken && (
|
||||||
|
<p className="mt-1 text-xs sm:text-sm text-red-600 font-light">
|
||||||
|
{mfaErrors.mfaToken.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="mt-1.5 text-xs text-gray-500 font-light">
|
||||||
|
Enter the 6-digit code from your authenticator app or an 8-character backup code
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="btn-luxury-primary w-full flex items-center justify-center py-2.5 sm:py-3 px-4 text-xs sm:text-sm relative"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="animate-spin -ml-1 mr-2 h-4 w-4 sm:h-5 sm:w-5 relative z-10" />
|
||||||
|
<span className="relative z-10">Verifying...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Shield className="-ml-1 mr-2 h-4 w-4 sm:h-5 sm:w-5 relative z-10" />
|
||||||
|
<span className="relative z-10">Verify</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleBackToLogin}
|
||||||
|
className="inline-flex items-center text-xs sm:text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="mr-1 h-3.5 w-3.5 sm:h-4 sm:w-4" />
|
||||||
|
Back to Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 sm:space-y-5">
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50/80 backdrop-blur-sm border border-red-200 text-red-700 px-4 py-3 rounded-sm text-sm font-light">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email" className="block text-xs sm:text-sm font-medium text-gray-700 mb-1.5 sm:mb-2 tracking-wide">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Mail className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
{...register('email')}
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
autoComplete="email"
|
||||||
|
className={`luxury-input pl-9 sm:pl-10 py-2.5 sm:py-3 text-sm sm:text-base w-full ${
|
||||||
|
errors.email ? 'border-red-300 focus:ring-red-500 focus:border-red-500' : ''
|
||||||
|
}`}
|
||||||
|
placeholder="email@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.email && (
|
||||||
|
<p className="mt-1 text-sm text-red-600 font-light">
|
||||||
|
{errors.email.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="password" className="block text-xs sm:text-sm font-medium text-gray-700 mb-1.5 sm:mb-2 tracking-wide">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Lock className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
{...register('password')}
|
||||||
|
id="password"
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
autoComplete="current-password"
|
||||||
|
className={`luxury-input pl-9 sm:pl-10 pr-9 sm:pr-10 py-2.5 sm:py-3 text-sm sm:text-base w-full ${
|
||||||
|
errors.password ? 'border-red-300 focus:ring-red-500 focus:border-red-500' : ''
|
||||||
|
}`}
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute inset-y-0 right-0 pr-3 flex items-center transition-colors hover:text-[#d4af37]"
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<EyeOff className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{errors.password && (
|
||||||
|
<p className="mt-1 text-sm text-red-600 font-light">
|
||||||
|
{errors.password.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 sm:gap-0">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
{...register('rememberMe')}
|
||||||
|
id="rememberMe"
|
||||||
|
type="checkbox"
|
||||||
|
className="h-4 w-4 text-[#d4af37] focus:ring-[#d4af37]/50 border-gray-300 rounded-sm cursor-pointer accent-[#d4af37]"
|
||||||
|
/>
|
||||||
|
<label htmlFor="rememberMe" className="ml-2 block text-xs sm:text-sm text-gray-700 cursor-pointer font-light tracking-wide">
|
||||||
|
Remember me
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => openModal('forgot-password')}
|
||||||
|
className="text-xs sm:text-sm font-medium text-[#d4af37] hover:text-[#c9a227] transition-colors tracking-wide"
|
||||||
|
>
|
||||||
|
Forgot password?
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Recaptcha
|
||||||
|
onChange={(token) => setRecaptchaToken(token)}
|
||||||
|
onError={(error) => {
|
||||||
|
console.error('reCAPTCHA error:', error);
|
||||||
|
setRecaptchaToken(null);
|
||||||
|
}}
|
||||||
|
theme="light"
|
||||||
|
size="normal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="btn-luxury-primary w-full flex items-center justify-center py-2.5 sm:py-3 px-4 text-xs sm:text-sm relative"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="animate-spin -ml-1 mr-2 h-4 w-4 sm:h-5 sm:w-5 relative z-10" />
|
||||||
|
<span className="relative z-10">Processing...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<LogIn className="-ml-1 mr-2 h-4 w-4 sm:h-5 sm:w-5 relative z-10" />
|
||||||
|
<span className="relative z-10">Sign In</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!requiresMFA && (
|
||||||
|
<div className="mt-4 sm:mt-6 text-center">
|
||||||
|
<p className="text-xs sm:text-sm text-gray-600 font-light tracking-wide">
|
||||||
|
Don't have an account?{' '}
|
||||||
|
<button
|
||||||
|
onClick={() => openModal('register')}
|
||||||
|
className="font-medium text-[#d4af37] hover:text-[#c9a227] transition-colors"
|
||||||
|
>
|
||||||
|
Register now
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginModal;
|
||||||
|
|
||||||
419
Frontend/src/components/modals/RegisterModal.tsx
Normal file
419
Frontend/src/components/modals/RegisterModal.tsx
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import { X, Eye, EyeOff, UserPlus, Loader2, Mail, Lock, User, Phone, CheckCircle2, XCircle } from 'lucide-react';
|
||||||
|
import useAuthStore from '../../store/useAuthStore';
|
||||||
|
import { registerSchema, RegisterFormData } from '../../utils/validationSchemas';
|
||||||
|
import { useCompanySettings } from '../../contexts/CompanySettingsContext';
|
||||||
|
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import Recaptcha from '../common/Recaptcha';
|
||||||
|
import { recaptchaService } from '../../services/api/systemSettingsService';
|
||||||
|
|
||||||
|
const PasswordRequirement: React.FC<{ met: boolean; text: string }> = ({ met, text }) => (
|
||||||
|
<div className="flex items-center gap-1.5 sm:gap-2 text-[10px] sm:text-xs font-light">
|
||||||
|
{met ? (
|
||||||
|
<CheckCircle2 className="h-3.5 w-3.5 sm:h-4 sm:w-4 text-[#d4af37] flex-shrink-0" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="h-3.5 w-3.5 sm:h-4 sm:w-4 text-gray-300 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
<span className={met ? 'text-[#c9a227] font-medium' : 'text-gray-500'}>
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const RegisterModal: React.FC = () => {
|
||||||
|
const { closeModal, openModal } = useAuthModal();
|
||||||
|
const { register: registerUser, isLoading, error, clearError, isAuthenticated } = useAuthStore();
|
||||||
|
const { settings } = useCompanySettings();
|
||||||
|
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
|
const [recaptchaToken, setRecaptchaToken] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading && isAuthenticated) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
}, [isLoading, isAuthenticated, closeModal]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<RegisterFormData>({
|
||||||
|
resolver: yupResolver(registerSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
phone: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const password = watch('password');
|
||||||
|
|
||||||
|
const getPasswordStrength = (pwd: string) => {
|
||||||
|
if (!pwd) return { strength: 0, label: '', color: '' };
|
||||||
|
|
||||||
|
let strength = 0;
|
||||||
|
if (pwd.length >= 8) strength++;
|
||||||
|
if (/[a-z]/.test(pwd)) strength++;
|
||||||
|
if (/[A-Z]/.test(pwd)) strength++;
|
||||||
|
if (/\d/.test(pwd)) strength++;
|
||||||
|
if (/[@$!%*?&]/.test(pwd)) strength++;
|
||||||
|
|
||||||
|
const labels = [
|
||||||
|
{ label: 'Very Weak', color: 'bg-red-500' },
|
||||||
|
{ label: 'Weak', color: 'bg-orange-500' },
|
||||||
|
{ label: 'Medium', color: 'bg-yellow-500' },
|
||||||
|
{ label: 'Strong', color: 'bg-blue-500' },
|
||||||
|
{ label: 'Very Strong', color: 'bg-green-500' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return { strength, ...labels[strength] };
|
||||||
|
};
|
||||||
|
|
||||||
|
const passwordStrength = getPasswordStrength(password || '');
|
||||||
|
|
||||||
|
const onSubmit = async (data: RegisterFormData) => {
|
||||||
|
try {
|
||||||
|
clearError();
|
||||||
|
|
||||||
|
if (recaptchaToken) {
|
||||||
|
try {
|
||||||
|
const verifyResponse = await recaptchaService.verifyRecaptcha(recaptchaToken);
|
||||||
|
if (verifyResponse.status === 'error' || !verifyResponse.data.verified) {
|
||||||
|
toast.error('reCAPTCHA verification failed. Please try again.');
|
||||||
|
setRecaptchaToken(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('reCAPTCHA verification failed. Please try again.');
|
||||||
|
setRecaptchaToken(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await registerUser({
|
||||||
|
name: data.name,
|
||||||
|
email: data.email,
|
||||||
|
password: data.password,
|
||||||
|
phone: data.phone,
|
||||||
|
});
|
||||||
|
|
||||||
|
setRecaptchaToken(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Register error:', error);
|
||||||
|
setRecaptchaToken(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle escape key
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', handleEscape);
|
||||||
|
return () => document.removeEventListener('keydown', handleEscape);
|
||||||
|
}, [closeModal]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[9999] flex items-center justify-center p-3 sm:p-4 md:p-6"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<div className="relative w-full max-w-md max-h-[95vh] overflow-y-auto bg-gradient-to-br from-gray-50 via-white to-gray-50 rounded-lg shadow-2xl border border-[#d4af37]/20">
|
||||||
|
{/* Close button */}
|
||||||
|
<button
|
||||||
|
onClick={closeModal}
|
||||||
|
className="absolute top-3 right-3 z-10 p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-500 hover:text-gray-900"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="p-4 sm:p-6 lg:p-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center mb-4 sm:mb-6">
|
||||||
|
<div className="flex justify-center mb-3 sm:mb-4">
|
||||||
|
{settings.company_logo_url ? (
|
||||||
|
<img
|
||||||
|
src={settings.company_logo_url.startsWith('http')
|
||||||
|
? settings.company_logo_url
|
||||||
|
: `${import.meta.env.VITE_API_URL || 'http://localhost:8000'}${settings.company_logo_url}`}
|
||||||
|
alt={settings.company_name || 'Logo'}
|
||||||
|
className="h-12 sm:h-14 lg:h-16 w-auto max-w-[120px] sm:max-w-[150px] object-contain"
|
||||||
|
style={{ filter: 'drop-shadow(0 4px 6px rgba(0,0,0,0.1))' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="relative p-2.5 sm:p-3 bg-gradient-to-br from-[#d4af37] to-[#c9a227] rounded-full shadow-lg shadow-[#d4af37]/30">
|
||||||
|
<UserPlus className="w-6 h-6 sm:w-8 sm:h-8 text-[#0f0f0f] relative z-10" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{settings.company_tagline && (
|
||||||
|
<p className="text-[10px] sm:text-xs text-[#d4af37] uppercase tracking-[1.5px] sm:tracking-[2px] mb-1 sm:mb-2 font-light">
|
||||||
|
{settings.company_tagline}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<h2 className="text-xl sm:text-2xl lg:text-3xl font-serif font-semibold text-gray-900 tracking-tight">
|
||||||
|
Create Account
|
||||||
|
</h2>
|
||||||
|
<p className="mt-1 sm:mt-2 text-xs sm:text-sm text-gray-600 font-light tracking-wide">
|
||||||
|
Join {settings.company_name || 'Luxury Hotel'} for exclusive benefits
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 sm:space-y-5">
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50/80 backdrop-blur-sm border border-red-200 text-red-700 px-4 py-3 rounded-sm text-sm font-light">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="name" className="block text-xs sm:text-sm font-medium text-gray-700 mb-1.5 sm:mb-2 tracking-wide">
|
||||||
|
Full Name
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<User className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
{...register('name')}
|
||||||
|
id="name"
|
||||||
|
type="text"
|
||||||
|
autoComplete="name"
|
||||||
|
className={`luxury-input pl-9 sm:pl-10 py-2.5 sm:py-3 text-sm sm:text-base w-full ${
|
||||||
|
errors.name ? 'border-red-300 focus:ring-red-500 focus:border-red-500' : ''
|
||||||
|
}`}
|
||||||
|
placeholder="John Doe"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.name && (
|
||||||
|
<p className="mt-1 text-xs sm:text-sm text-red-600 font-light">
|
||||||
|
{errors.name.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email" className="block text-xs sm:text-sm font-medium text-gray-700 mb-1.5 sm:mb-2 tracking-wide">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Mail className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
{...register('email')}
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
autoComplete="email"
|
||||||
|
className={`luxury-input pl-9 sm:pl-10 py-2.5 sm:py-3 text-sm sm:text-base w-full ${
|
||||||
|
errors.email ? 'border-red-300 focus:ring-red-500 focus:border-red-500' : ''
|
||||||
|
}`}
|
||||||
|
placeholder="email@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.email && (
|
||||||
|
<p className="mt-1 text-xs sm:text-sm text-red-600 font-light">
|
||||||
|
{errors.email.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="phone" className="block text-xs sm:text-sm font-medium text-gray-700 mb-1.5 sm:mb-2 tracking-wide">
|
||||||
|
Phone Number (Optional)
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Phone className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
{...register('phone')}
|
||||||
|
id="phone"
|
||||||
|
type="tel"
|
||||||
|
autoComplete="tel"
|
||||||
|
className={`luxury-input pl-9 sm:pl-10 py-2.5 sm:py-3 text-sm sm:text-base w-full ${
|
||||||
|
errors.phone ? 'border-red-300 focus:ring-red-500 focus:border-red-500' : ''
|
||||||
|
}`}
|
||||||
|
placeholder="0123456789"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.phone && (
|
||||||
|
<p className="mt-1 text-xs sm:text-sm text-red-600 font-light">
|
||||||
|
{errors.phone.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="password" className="block text-xs sm:text-sm font-medium text-gray-700 mb-1.5 sm:mb-2 tracking-wide">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Lock className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
{...register('password')}
|
||||||
|
id="password"
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
autoComplete="new-password"
|
||||||
|
className={`luxury-input pl-9 sm:pl-10 pr-9 sm:pr-10 py-2.5 sm:py-3 text-sm sm:text-base w-full ${
|
||||||
|
errors.password ? 'border-red-300 focus:ring-red-500 focus:border-red-500' : ''
|
||||||
|
}`}
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute inset-y-0 right-0 pr-3 flex items-center transition-colors hover:text-[#d4af37]"
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<EyeOff className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{errors.password && (
|
||||||
|
<p className="mt-1 text-xs sm:text-sm text-red-600 font-light">
|
||||||
|
{errors.password.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{password && password.length > 0 && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full transition-all duration-300 ${
|
||||||
|
passwordStrength.strength >= 4
|
||||||
|
? 'bg-[#d4af37]'
|
||||||
|
: passwordStrength.strength >= 3
|
||||||
|
? 'bg-yellow-500'
|
||||||
|
: 'bg-red-500'
|
||||||
|
}`}
|
||||||
|
style={{ width: `${(passwordStrength.strength / 5) * 100}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] sm:text-xs font-medium text-gray-600 tracking-wide">
|
||||||
|
{passwordStrength.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
<PasswordRequirement met={password.length >= 8} text="At least 8 characters" />
|
||||||
|
<PasswordRequirement met={/[a-z]/.test(password)} text="Lowercase letter (a-z)" />
|
||||||
|
<PasswordRequirement met={/[A-Z]/.test(password)} text="Uppercase letter (A-Z)" />
|
||||||
|
<PasswordRequirement met={/\d/.test(password)} text="Number (0-9)" />
|
||||||
|
<PasswordRequirement met={/[@$!%*?&]/.test(password)} text="Special character (@$!%*?&)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="confirmPassword" className="block text-xs sm:text-sm font-medium text-gray-700 mb-1.5 sm:mb-2 tracking-wide">
|
||||||
|
Confirm Password
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Lock className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
{...register('confirmPassword')}
|
||||||
|
id="confirmPassword"
|
||||||
|
type={showConfirmPassword ? 'text' : 'password'}
|
||||||
|
autoComplete="new-password"
|
||||||
|
className={`luxury-input pl-9 sm:pl-10 pr-9 sm:pr-10 py-2.5 sm:py-3 text-sm sm:text-base w-full ${
|
||||||
|
errors.confirmPassword ? 'border-red-300 focus:ring-red-500 focus:border-red-500' : ''
|
||||||
|
}`}
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||||
|
className="absolute inset-y-0 right-0 pr-3 flex items-center transition-colors hover:text-[#d4af37]"
|
||||||
|
>
|
||||||
|
{showConfirmPassword ? (
|
||||||
|
<EyeOff className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{errors.confirmPassword && (
|
||||||
|
<p className="mt-1 text-xs sm:text-sm text-red-600 font-light">
|
||||||
|
{errors.confirmPassword.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Recaptcha
|
||||||
|
onChange={(token) => setRecaptchaToken(token)}
|
||||||
|
onError={(error) => {
|
||||||
|
console.error('reCAPTCHA error:', error);
|
||||||
|
setRecaptchaToken(null);
|
||||||
|
}}
|
||||||
|
theme="light"
|
||||||
|
size="normal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="btn-luxury-primary w-full flex items-center justify-center py-2.5 sm:py-3 px-4 text-xs sm:text-sm relative"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="animate-spin -ml-1 mr-2 h-4 w-4 sm:h-5 sm:w-5 relative z-10" />
|
||||||
|
<span className="relative z-10">Processing...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<UserPlus className="-ml-1 mr-2 h-4 w-4 sm:h-5 sm:w-5 relative z-10" />
|
||||||
|
<span className="relative z-10">Register</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-4 sm:mt-6 text-center">
|
||||||
|
<p className="text-xs sm:text-sm text-gray-600 font-light tracking-wide">
|
||||||
|
Already have an account?{' '}
|
||||||
|
<button
|
||||||
|
onClick={() => openModal('login')}
|
||||||
|
className="font-medium text-[#d4af37] hover:text-[#c9a227] transition-colors"
|
||||||
|
>
|
||||||
|
Login now
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RegisterModal;
|
||||||
|
|
||||||
390
Frontend/src/components/modals/ResetPasswordModal.tsx
Normal file
390
Frontend/src/components/modals/ResetPasswordModal.tsx
Normal file
@@ -0,0 +1,390 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import { X, Eye, EyeOff, Lock, Loader2, CheckCircle2, XCircle, AlertCircle, KeyRound } from 'lucide-react';
|
||||||
|
import useAuthStore from '../../store/useAuthStore';
|
||||||
|
import { resetPasswordSchema, ResetPasswordFormData } from '../../utils/validationSchemas';
|
||||||
|
import { useCompanySettings } from '../../contexts/CompanySettingsContext';
|
||||||
|
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||||
|
|
||||||
|
const PasswordRequirement: React.FC<{ met: boolean; text: string }> = ({ met, text }) => (
|
||||||
|
<div className="flex items-center gap-1.5 sm:gap-2 text-[10px] sm:text-xs">
|
||||||
|
{met ? (
|
||||||
|
<CheckCircle2 className="h-3.5 w-3.5 sm:h-4 sm:w-4 text-green-500 flex-shrink-0" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="h-3.5 w-3.5 sm:h-4 sm:w-4 text-gray-300 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
<span className={met ? 'text-green-600' : 'text-gray-500'}>
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface ResetPasswordModalProps {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResetPasswordModal: React.FC<ResetPasswordModalProps> = ({ token }) => {
|
||||||
|
const { closeModal, openModal } = useAuthModal();
|
||||||
|
const { resetPassword, isLoading, error, clearError, isAuthenticated } = useAuthStore();
|
||||||
|
const { settings } = useCompanySettings();
|
||||||
|
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
|
const [isSuccess, setIsSuccess] = useState(false);
|
||||||
|
|
||||||
|
// Close modal on successful reset
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading && isAuthenticated) {
|
||||||
|
setTimeout(() => {
|
||||||
|
closeModal();
|
||||||
|
openModal('login');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}, [isLoading, isAuthenticated, closeModal, openModal]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<ResetPasswordFormData>({
|
||||||
|
resolver: yupResolver(resetPasswordSchema),
|
||||||
|
defaultValues: {
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const password = watch('password');
|
||||||
|
|
||||||
|
const getPasswordStrength = (pwd: string) => {
|
||||||
|
if (!pwd) return { strength: 0, label: '', color: '' };
|
||||||
|
|
||||||
|
let strength = 0;
|
||||||
|
if (pwd.length >= 8) strength++;
|
||||||
|
if (/[a-z]/.test(pwd)) strength++;
|
||||||
|
if (/[A-Z]/.test(pwd)) strength++;
|
||||||
|
if (/\d/.test(pwd)) strength++;
|
||||||
|
if (/[@$!%*?&]/.test(pwd)) strength++;
|
||||||
|
|
||||||
|
const labels = [
|
||||||
|
{ label: 'Very Weak', color: 'bg-red-500' },
|
||||||
|
{ label: 'Weak', color: 'bg-orange-500' },
|
||||||
|
{ label: 'Medium', color: 'bg-yellow-500' },
|
||||||
|
{ label: 'Strong', color: 'bg-blue-500' },
|
||||||
|
{ label: 'Very Strong', color: 'bg-green-500' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return { strength, ...labels[strength] };
|
||||||
|
};
|
||||||
|
|
||||||
|
const passwordStrength = getPasswordStrength(password || '');
|
||||||
|
|
||||||
|
const onSubmit = async (data: ResetPasswordFormData) => {
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
clearError();
|
||||||
|
await resetPassword({
|
||||||
|
token,
|
||||||
|
password: data.password,
|
||||||
|
confirmPassword: data.confirmPassword,
|
||||||
|
});
|
||||||
|
setIsSuccess(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Reset password error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTokenError = error?.includes('token') || error?.includes('expired');
|
||||||
|
const isReuseError = error?.toLowerCase().includes('must be different') || error?.toLowerCase().includes('different from old');
|
||||||
|
|
||||||
|
// Handle escape key
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape' && !isSuccess) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', handleEscape);
|
||||||
|
return () => document.removeEventListener('keydown', handleEscape);
|
||||||
|
}, [closeModal, isSuccess]);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[9999] flex items-center justify-center p-3 sm:p-4 md:p-6"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget && !isSuccess) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<div className="relative w-full max-w-md max-h-[95vh] overflow-y-auto bg-gradient-to-br from-gray-50 via-white to-gray-50 rounded-lg shadow-2xl border border-[#d4af37]/20">
|
||||||
|
{/* Close button */}
|
||||||
|
{!isSuccess && (
|
||||||
|
<button
|
||||||
|
onClick={closeModal}
|
||||||
|
className="absolute top-3 right-3 z-10 p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-500 hover:text-gray-900"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="p-4 sm:p-6 lg:p-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center mb-4 sm:mb-6">
|
||||||
|
<div className="flex justify-center mb-3 sm:mb-4">
|
||||||
|
{settings.company_logo_url ? (
|
||||||
|
<img
|
||||||
|
src={settings.company_logo_url.startsWith('http')
|
||||||
|
? settings.company_logo_url
|
||||||
|
: `${import.meta.env.VITE_API_URL || 'http://localhost:8000'}${settings.company_logo_url}`}
|
||||||
|
alt={settings.company_name || 'Logo'}
|
||||||
|
className="h-12 sm:h-14 lg:h-16 w-auto max-w-[120px] sm:max-w-[150px] object-contain"
|
||||||
|
style={{ filter: 'drop-shadow(0 4px 6px rgba(0,0,0,0.1))' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="relative p-2.5 sm:p-3 bg-gradient-to-br from-[#d4af37] to-[#c9a227] rounded-full shadow-lg shadow-[#d4af37]/30">
|
||||||
|
<KeyRound className="w-6 h-6 sm:w-8 sm:h-8 text-[#0f0f0f] relative z-10" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{settings.company_tagline && (
|
||||||
|
<p className="text-[10px] sm:text-xs text-[#d4af37] uppercase tracking-[1.5px] sm:tracking-[2px] mb-1 sm:mb-2 font-light">
|
||||||
|
{settings.company_tagline}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<h2 className="text-xl sm:text-2xl lg:text-3xl font-serif font-semibold text-gray-900 tracking-tight">
|
||||||
|
{isSuccess ? 'Complete!' : 'Reset Password'}
|
||||||
|
</h2>
|
||||||
|
<p className="mt-1 sm:mt-2 text-xs sm:text-sm text-gray-600 font-light tracking-wide">
|
||||||
|
{isSuccess
|
||||||
|
? 'Password has been reset successfully'
|
||||||
|
: `Enter a new password for your ${settings.company_name || 'Luxury Hotel'} account`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isSuccess ? (
|
||||||
|
<div className="text-center space-y-4 sm:space-y-5">
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div className="w-12 h-12 sm:w-14 sm:h-14 lg:w-16 lg:h-16 bg-green-100 rounded-full flex items-center justify-center">
|
||||||
|
<CheckCircle2 className="w-6 h-6 sm:w-8 sm:h-8 lg:w-10 lg:h-10 text-green-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5 sm:space-y-2">
|
||||||
|
<h3 className="text-lg sm:text-xl font-semibold text-gray-900">
|
||||||
|
Password reset successful!
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs sm:text-sm text-gray-600">
|
||||||
|
Your password has been updated.
|
||||||
|
</p>
|
||||||
|
<p className="text-xs sm:text-sm text-gray-600">
|
||||||
|
You can now login with your new password.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3 sm:p-4">
|
||||||
|
<p className="text-xs sm:text-sm text-gray-700">
|
||||||
|
Redirecting to login...
|
||||||
|
</p>
|
||||||
|
<div className="mt-2 flex justify-center">
|
||||||
|
<Loader2 className="animate-spin h-4 w-4 sm:h-5 sm:w-5 text-[#d4af37]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
closeModal();
|
||||||
|
openModal('login');
|
||||||
|
}}
|
||||||
|
className="inline-flex items-center justify-center w-full py-2.5 sm:py-3 px-4 border border-transparent rounded-lg text-xs sm:text-sm font-medium text-white bg-gradient-to-r from-[#d4af37] to-[#c9a227] hover:from-[#f5d76e] hover:to-[#d4af37] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#d4af37] transition-colors"
|
||||||
|
>
|
||||||
|
<KeyRound className="-ml-1 mr-2 h-4 w-4 sm:h-5 sm:w-5" />
|
||||||
|
Login Now
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 sm:space-y-5">
|
||||||
|
{error && (
|
||||||
|
<div
|
||||||
|
className={`border px-3 sm:px-4 py-2.5 sm:py-3 rounded-lg text-xs sm:text-sm flex items-start gap-2 ${
|
||||||
|
isTokenError
|
||||||
|
? 'bg-yellow-50 border-yellow-200 text-yellow-800'
|
||||||
|
: 'bg-red-50 border-red-200 text-red-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<AlertCircle className="h-4 w-4 sm:h-5 sm:w-5 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="font-medium">
|
||||||
|
{isReuseError
|
||||||
|
? 'New password must be different from old password'
|
||||||
|
: error}
|
||||||
|
</p>
|
||||||
|
{isTokenError && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
closeModal();
|
||||||
|
openModal('forgot-password');
|
||||||
|
}}
|
||||||
|
className="mt-2 inline-block text-xs sm:text-sm font-medium underline hover:text-yellow-900"
|
||||||
|
>
|
||||||
|
Request new link
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="password" className="block text-xs sm:text-sm font-medium text-gray-700 mb-1.5 sm:mb-2">
|
||||||
|
New Password
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Lock className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
{...register('password')}
|
||||||
|
id="password"
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
autoComplete="new-password"
|
||||||
|
autoFocus
|
||||||
|
className={`block w-full pl-9 sm:pl-10 pr-9 sm:pr-10 py-2.5 sm:py-3 border rounded-lg focus:outline-none focus:ring-2 transition-colors text-sm sm:text-base luxury-input ${
|
||||||
|
errors.password
|
||||||
|
? 'border-red-300 focus:ring-red-500'
|
||||||
|
: 'border-gray-300 focus:ring-[#d4af37]'
|
||||||
|
}`}
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<EyeOff className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400 hover:text-gray-600" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400 hover:text-gray-600" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{errors.password && (
|
||||||
|
<p className="mt-1 text-xs sm:text-sm text-red-600">
|
||||||
|
{errors.password.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{password && password.length > 0 && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full transition-all duration-300 ${passwordStrength.color}`}
|
||||||
|
style={{ width: `${(passwordStrength.strength / 5) * 100}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] sm:text-xs font-medium text-gray-600">
|
||||||
|
{passwordStrength.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
<PasswordRequirement met={password.length >= 8} text="At least 8 characters" />
|
||||||
|
<PasswordRequirement met={/[a-z]/.test(password)} text="Lowercase letter (a-z)" />
|
||||||
|
<PasswordRequirement met={/[A-Z]/.test(password)} text="Uppercase letter (A-Z)" />
|
||||||
|
<PasswordRequirement met={/\d/.test(password)} text="Number (0-9)" />
|
||||||
|
<PasswordRequirement met={/[@$!%*?&]/.test(password)} text="Special character (@$!%*?&)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="confirmPassword" className="block text-xs sm:text-sm font-medium text-gray-700 mb-1.5 sm:mb-2">
|
||||||
|
Confirm Password
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Lock className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
{...register('confirmPassword')}
|
||||||
|
id="confirmPassword"
|
||||||
|
type={showConfirmPassword ? 'text' : 'password'}
|
||||||
|
autoComplete="new-password"
|
||||||
|
className={`block w-full pl-9 sm:pl-10 pr-9 sm:pr-10 py-2.5 sm:py-3 border rounded-lg focus:outline-none focus:ring-2 transition-colors text-sm sm:text-base luxury-input ${
|
||||||
|
errors.confirmPassword
|
||||||
|
? 'border-red-300 focus:ring-red-500'
|
||||||
|
: 'border-gray-300 focus:ring-[#d4af37]'
|
||||||
|
}`}
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||||
|
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||||
|
>
|
||||||
|
{showConfirmPassword ? (
|
||||||
|
<EyeOff className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400 hover:text-gray-600" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400 hover:text-gray-600" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{errors.confirmPassword && (
|
||||||
|
<p className="mt-1 text-xs sm:text-sm text-red-600">
|
||||||
|
{errors.confirmPassword.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full flex items-center justify-center py-2.5 sm:py-3 px-4 border border-transparent rounded-lg shadow-sm text-xs sm:text-sm font-medium text-white bg-gradient-to-r from-[#d4af37] to-[#c9a227] hover:from-[#f5d76e] hover:to-[#d4af37] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#d4af37] disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="animate-spin -ml-1 mr-2 h-4 w-4 sm:h-5 sm:w-5" />
|
||||||
|
Processing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<KeyRound className="-ml-1 mr-2 h-4 w-4 sm:h-5 sm:w-5" />
|
||||||
|
Reset Password
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => openModal('login')}
|
||||||
|
className="text-xs sm:text-sm font-medium text-[#d4af37] hover:text-[#c9a227] transition-colors"
|
||||||
|
>
|
||||||
|
Back to Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResetPasswordModal;
|
||||||
|
|
||||||
57
Frontend/src/contexts/AuthModalContext.tsx
Normal file
57
Frontend/src/contexts/AuthModalContext.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import React, { createContext, useContext, useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
type AuthModalType = 'login' | 'register' | 'forgot-password' | 'reset-password' | null;
|
||||||
|
type ResetPasswordParams = { token: string } | null;
|
||||||
|
|
||||||
|
interface AuthModalContextType {
|
||||||
|
isOpen: boolean;
|
||||||
|
modalType: AuthModalType;
|
||||||
|
resetPasswordParams: ResetPasswordParams;
|
||||||
|
openModal: (type: AuthModalType, params?: ResetPasswordParams) => void;
|
||||||
|
closeModal: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthModalContext = createContext<AuthModalContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const AuthModalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [modalType, setModalType] = useState<AuthModalType>(null);
|
||||||
|
const [resetPasswordParams, setResetPasswordParams] = useState<ResetPasswordParams>(null);
|
||||||
|
|
||||||
|
const openModal = useCallback((type: AuthModalType, params?: ResetPasswordParams) => {
|
||||||
|
setModalType(type);
|
||||||
|
setResetPasswordParams(params || null);
|
||||||
|
setIsOpen(true);
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeModal = useCallback(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setModalType(null);
|
||||||
|
setResetPasswordParams(null);
|
||||||
|
document.body.style.overflow = 'unset';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthModalContext.Provider
|
||||||
|
value={{
|
||||||
|
isOpen,
|
||||||
|
modalType,
|
||||||
|
resetPasswordParams,
|
||||||
|
openModal,
|
||||||
|
closeModal,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AuthModalContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuthModal = () => {
|
||||||
|
const context = useContext(AuthModalContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useAuthModal must be used within an AuthModalProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
101
Frontend/src/pages/NotFoundPage.tsx
Normal file
101
Frontend/src/pages/NotFoundPage.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Home, Hotel, AlertCircle } from 'lucide-react';
|
||||||
|
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
||||||
|
|
||||||
|
const NotFoundPage: React.FC = () => {
|
||||||
|
const { settings } = useCompanySettings();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-gray-100 to-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 relative overflow-hidden">
|
||||||
|
{/* Background Pattern */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 opacity-5"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9 0h2v20H9zM25 0h2v20h-2zM41 0h2v20h-2zM57 0h2v20h-2zM0 9h20v2H0zM0 25h20v2H0zM0 41h20v2H0zM0 57h20v2H0zM40 9h20v2H40zM40 25h20v2H40zM40 41h20v2H40zM40 57h20v2H40zM9 40h2v20H9zM25 40h2v20h-2zM41 40h2v20h-2zM57 40h2v20h-2z' fill='%23d4af37' opacity='0.05'/%3E%3C/svg%3E")`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="max-w-2xl w-full text-center relative z-10">
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="flex justify-center mb-6 sm:mb-8">
|
||||||
|
{settings.company_logo_url ? (
|
||||||
|
<img
|
||||||
|
src={settings.company_logo_url.startsWith('http')
|
||||||
|
? settings.company_logo_url
|
||||||
|
: `${import.meta.env.VITE_API_URL || 'http://localhost:8000'}${settings.company_logo_url}`}
|
||||||
|
alt={settings.company_name || 'Logo'}
|
||||||
|
className="h-16 sm:h-20 lg:h-24 w-auto max-w-[200px] sm:max-w-[250px] object-contain"
|
||||||
|
style={{ filter: 'drop-shadow(0 4px 6px rgba(0,0,0,0.1))' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="relative p-4 sm:p-5 bg-gradient-to-br from-[#d4af37] to-[#c9a227] rounded-full shadow-lg shadow-[#d4af37]/30">
|
||||||
|
<Hotel className="w-12 h-12 sm:w-16 sm:h-16 lg:w-20 lg:h-20 text-[#0f0f0f] relative z-10" />
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-[#f5d76e] to-[#d4af37] rounded-full blur-xl opacity-50"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tagline */}
|
||||||
|
{settings.company_tagline && (
|
||||||
|
<p className="text-xs sm:text-sm text-[#d4af37] uppercase tracking-[2px] sm:tracking-[3px] mb-3 sm:mb-4 font-light">
|
||||||
|
{settings.company_tagline}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 404 Number */}
|
||||||
|
<div className="mb-6 sm:mb-8">
|
||||||
|
<h1 className="text-8xl sm:text-9xl lg:text-[12rem] font-serif font-bold text-transparent bg-clip-text bg-gradient-to-r from-[#d4af37] via-[#f5d76e] to-[#d4af37] leading-none tracking-tight">
|
||||||
|
404
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Icon */}
|
||||||
|
<div className="flex justify-center mb-6 sm:mb-8">
|
||||||
|
<div className="relative p-4 sm:p-5 bg-red-50 rounded-full border-2 border-red-200">
|
||||||
|
<AlertCircle className="w-8 h-8 sm:w-10 sm:h-10 text-red-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Message */}
|
||||||
|
<div className="mb-6 sm:mb-8">
|
||||||
|
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-serif font-semibold text-gray-900 mb-3 sm:mb-4 tracking-tight">
|
||||||
|
Page Not Found
|
||||||
|
</h2>
|
||||||
|
<p className="text-base sm:text-lg text-gray-600 font-light tracking-wide max-w-md mx-auto leading-relaxed">
|
||||||
|
We're sorry, but the page you're looking for doesn't exist or has been moved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Additional Info */}
|
||||||
|
<div className="mb-8 sm:mb-10">
|
||||||
|
<p className="text-sm sm:text-base text-gray-500 font-light">
|
||||||
|
The page you requested could not be found on our server.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Home Button */}
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="inline-flex items-center justify-center gap-2 px-8 py-3.5 sm:px-10 sm:py-4 bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-[#0f0f0f] font-semibold text-sm sm:text-base rounded-lg shadow-lg shadow-[#d4af37]/30 hover:from-[#f5d76e] hover:to-[#d4af37] hover:shadow-xl hover:shadow-[#d4af37]/40 transition-all duration-300 ease-out hover:-translate-y-0.5 active:translate-y-0 relative overflow-hidden group"
|
||||||
|
>
|
||||||
|
<span className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/20 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700"></span>
|
||||||
|
<Home className="w-5 h-5 sm:w-6 sm:h-6 relative z-10" />
|
||||||
|
<span className="relative z-10 tracking-wide">Back to Homepage</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Decorative Elements */}
|
||||||
|
<div className="mt-12 sm:mt-16 flex justify-center gap-2">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-[#d4af37]/30"></div>
|
||||||
|
<div className="w-2 h-2 rounded-full bg-[#d4af37]/50"></div>
|
||||||
|
<div className="w-2 h-2 rounded-full bg-[#d4af37]/30"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotFoundPage;
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ const DashboardPage: React.FC = () => {
|
|||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
await logout();
|
await logout();
|
||||||
navigate('/login');
|
navigate('/');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Logout error:', error);
|
console.error('Logout error:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,385 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import {
|
|
||||||
Mail,
|
|
||||||
ArrowLeft,
|
|
||||||
Send,
|
|
||||||
Loader2,
|
|
||||||
CheckCircle,
|
|
||||||
Hotel,
|
|
||||||
Home,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import useAuthStore from '../../store/useAuthStore';
|
|
||||||
import {
|
|
||||||
forgotPasswordSchema,
|
|
||||||
ForgotPasswordFormData,
|
|
||||||
} from '../../utils/validationSchemas';
|
|
||||||
import { useCompanySettings } from '../../contexts/CompanySettingsContext';
|
|
||||||
|
|
||||||
const ForgotPasswordPage: React.FC = () => {
|
|
||||||
const { forgotPassword, isLoading, error, clearError } =
|
|
||||||
useAuthStore();
|
|
||||||
const { settings } = useCompanySettings();
|
|
||||||
|
|
||||||
const [isSuccess, setIsSuccess] = useState(false);
|
|
||||||
const [submittedEmail, setSubmittedEmail] = useState('');
|
|
||||||
|
|
||||||
|
|
||||||
const supportEmail = settings.company_email || 'support@hotel.com';
|
|
||||||
const supportPhone = settings.company_phone || '1900-xxxx';
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const companyName = settings.company_name || 'Luxury Hotel';
|
|
||||||
document.title = `Forgot Password - ${companyName}`;
|
|
||||||
}, [settings.company_name]);
|
|
||||||
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<ForgotPasswordFormData>({
|
|
||||||
resolver: yupResolver(forgotPasswordSchema),
|
|
||||||
defaultValues: {
|
|
||||||
email: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const onSubmit = async (data: ForgotPasswordFormData) => {
|
|
||||||
try {
|
|
||||||
clearError();
|
|
||||||
setSubmittedEmail(data.email);
|
|
||||||
await forgotPassword({ email: data.email });
|
|
||||||
|
|
||||||
|
|
||||||
setIsSuccess(true);
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
console.error('Forgot password error:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="min-h-screen bg-gradient-to-br
|
|
||||||
from-blue-50 to-indigo-100 flex items-center
|
|
||||||
justify-center py-4 sm:py-8 lg:py-12 px-3 sm:px-4 lg:px-8"
|
|
||||||
>
|
|
||||||
<div className="max-w-md w-full space-y-4 sm:space-y-6 lg:space-y-8">
|
|
||||||
{}
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="flex justify-center mb-3 sm:mb-4">
|
|
||||||
{settings.company_logo_url ? (
|
|
||||||
<img
|
|
||||||
src={settings.company_logo_url.startsWith('http')
|
|
||||||
? settings.company_logo_url
|
|
||||||
: `${import.meta.env.VITE_API_URL || 'http://localhost:8000'}${settings.company_logo_url}`}
|
|
||||||
alt={settings.company_name || 'Logo'}
|
|
||||||
className="h-14 sm:h-16 lg:h-20 w-auto max-w-[150px] sm:max-w-[180px] lg:max-w-[200px] object-contain"
|
|
||||||
style={{ filter: 'drop-shadow(0 4px 6px rgba(0,0,0,0.1))' }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="p-2.5 sm:p-3 bg-blue-600 rounded-full">
|
|
||||||
<Hotel className="w-8 h-8 sm:w-10 sm:h-10 lg:w-12 lg:h-12 text-white" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{settings.company_tagline && (
|
|
||||||
<p className="text-[10px] sm:text-xs text-blue-600 uppercase tracking-[1.5px] sm:tracking-[2px] mb-1 sm:mb-2 font-light px-2">
|
|
||||||
{settings.company_tagline}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<h2 className="text-2xl sm:text-3xl font-bold text-gray-900 px-2">
|
|
||||||
Forgot Password?
|
|
||||||
</h2>
|
|
||||||
<p className="mt-1 sm:mt-2 text-xs sm:text-sm text-gray-600 px-4">
|
|
||||||
Enter your email to receive a password reset link for {settings.company_name || 'Luxury Hotel'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="bg-white rounded-lg shadow-xl p-4 sm:p-6 lg:p-8">
|
|
||||||
{isSuccess ? (
|
|
||||||
|
|
||||||
<div className="text-center space-y-4 sm:space-y-5 lg:space-y-6">
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<div
|
|
||||||
className="w-12 h-12 sm:w-14 sm:h-14 lg:w-16 lg:h-16 bg-green-100
|
|
||||||
rounded-full flex items-center
|
|
||||||
justify-center"
|
|
||||||
>
|
|
||||||
<CheckCircle
|
|
||||||
className="w-6 h-6 sm:w-8 sm:h-8 lg:w-10 lg:h-10 text-green-600"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1.5 sm:space-y-2">
|
|
||||||
<h3
|
|
||||||
className="text-lg sm:text-xl font-semibold
|
|
||||||
text-gray-900 px-2"
|
|
||||||
>
|
|
||||||
Email Sent!
|
|
||||||
</h3>
|
|
||||||
<p className="text-xs sm:text-sm text-gray-600 px-2">
|
|
||||||
We have sent a password reset link to
|
|
||||||
</p>
|
|
||||||
<p className="text-xs sm:text-sm font-medium text-blue-600 break-all px-4">
|
|
||||||
{submittedEmail}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="bg-blue-50 border border-blue-200
|
|
||||||
rounded-lg p-3 sm:p-4 text-left"
|
|
||||||
>
|
|
||||||
<p className="text-xs sm:text-sm text-gray-700">
|
|
||||||
<strong>Note:</strong>
|
|
||||||
</p>
|
|
||||||
<ul
|
|
||||||
className="mt-2 space-y-1 text-xs sm:text-sm
|
|
||||||
text-gray-600 list-disc list-inside"
|
|
||||||
>
|
|
||||||
<li>Link is valid for 1 hour</li>
|
|
||||||
<li>Check your Spam/Junk folder</li>
|
|
||||||
<li>
|
|
||||||
If you don't receive the email, please try again
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2.5 sm:space-y-3">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setIsSuccess(false);
|
|
||||||
clearError();
|
|
||||||
}}
|
|
||||||
className="w-full flex items-center
|
|
||||||
justify-center py-2.5 sm:py-3 px-4 border
|
|
||||||
border-gray-300 rounded-lg
|
|
||||||
text-xs sm:text-sm font-medium text-gray-700
|
|
||||||
bg-white hover:bg-gray-50
|
|
||||||
focus:outline-none focus:ring-2
|
|
||||||
focus:ring-offset-2
|
|
||||||
focus:ring-blue-500
|
|
||||||
transition-colors"
|
|
||||||
>
|
|
||||||
<Mail className="-ml-1 mr-2 h-4 w-4 sm:h-5 sm:w-5" />
|
|
||||||
Resend Email
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
to="/login"
|
|
||||||
className="w-full flex items-center
|
|
||||||
justify-center py-2.5 sm:py-3 px-4 border
|
|
||||||
border-transparent rounded-lg
|
|
||||||
text-xs sm:text-sm font-medium text-white
|
|
||||||
bg-blue-600 hover:bg-blue-700
|
|
||||||
focus:outline-none focus:ring-2
|
|
||||||
focus:ring-offset-2
|
|
||||||
focus:ring-blue-500
|
|
||||||
transition-colors"
|
|
||||||
>
|
|
||||||
<ArrowLeft
|
|
||||||
className="-ml-1 mr-2 h-4 w-4 sm:h-5 sm:w-5"
|
|
||||||
/>
|
|
||||||
Back to Login
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="mt-3 sm:mt-4 pt-3 sm:pt-4 border-t border-gray-200">
|
|
||||||
<Link
|
|
||||||
to="/"
|
|
||||||
className="w-full inline-flex items-center justify-center gap-2 px-3 sm:px-4 py-2 sm:py-2.5 rounded-lg bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-[#0f0f0f] font-semibold text-xs sm:text-sm shadow-lg shadow-[#d4af37]/30 hover:from-[#f5d76e] hover:to-[#d4af37] hover:shadow-xl hover:shadow-[#d4af37]/40 transition-all duration-300 ease-out hover:-translate-y-0.5 active:translate-y-0 relative overflow-hidden group"
|
|
||||||
>
|
|
||||||
<span className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/20 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700"></span>
|
|
||||||
<Home className="w-3.5 h-3.5 sm:w-4 sm:h-4 relative z-10" />
|
|
||||||
<span className="relative z-10 tracking-wide">Back to Homepage</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
className="space-y-4 sm:space-y-5 lg:space-y-6"
|
|
||||||
>
|
|
||||||
{}
|
|
||||||
{error && (
|
|
||||||
<div
|
|
||||||
className="bg-red-50 border
|
|
||||||
border-red-200 text-red-700
|
|
||||||
px-3 sm:px-4 py-2.5 sm:py-3 rounded-lg text-xs sm:text-sm"
|
|
||||||
>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="email"
|
|
||||||
className="block text-xs sm:text-sm font-medium
|
|
||||||
text-gray-700 mb-1.5 sm:mb-2"
|
|
||||||
>
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div
|
|
||||||
className="absolute inset-y-0 left-0
|
|
||||||
pl-3 flex items-center
|
|
||||||
pointer-events-none"
|
|
||||||
>
|
|
||||||
<Mail
|
|
||||||
className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
{...register('email')}
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
autoComplete="email"
|
|
||||||
autoFocus
|
|
||||||
className={`block w-full pl-9 sm:pl-10 pr-3
|
|
||||||
py-2.5 sm:py-3 border rounded-lg
|
|
||||||
focus:outline-none focus:ring-2
|
|
||||||
transition-colors text-sm sm:text-base
|
|
||||||
${
|
|
||||||
errors.email
|
|
||||||
? 'border-red-300 ' +
|
|
||||||
'focus:ring-red-500'
|
|
||||||
: 'border-gray-300 ' +
|
|
||||||
'focus:ring-blue-500'
|
|
||||||
}`}
|
|
||||||
placeholder="email@example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.email && (
|
|
||||||
<p className="mt-1 text-xs sm:text-sm text-red-600">
|
|
||||||
{errors.email.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading}
|
|
||||||
className="w-full flex items-center
|
|
||||||
justify-center py-2.5 sm:py-3 px-4 border
|
|
||||||
border-transparent rounded-lg
|
|
||||||
shadow-sm text-xs sm:text-sm font-medium
|
|
||||||
text-white bg-blue-600
|
|
||||||
hover:bg-blue-700 focus:outline-none
|
|
||||||
focus:ring-2 focus:ring-offset-2
|
|
||||||
focus:ring-blue-500
|
|
||||||
disabled:opacity-50
|
|
||||||
disabled:cursor-not-allowed
|
|
||||||
transition-colors"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<Loader2
|
|
||||||
className="animate-spin -ml-1
|
|
||||||
mr-2 h-4 w-4 sm:h-5 sm:w-5"
|
|
||||||
/>
|
|
||||||
Processing...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Send className="-ml-1 mr-2 h-4 w-4 sm:h-5 sm:w-5" />
|
|
||||||
Send Reset Link
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="text-center">
|
|
||||||
<Link
|
|
||||||
to="/login"
|
|
||||||
className="inline-flex items-center
|
|
||||||
text-xs sm:text-sm font-medium text-blue-600
|
|
||||||
hover:text-blue-500 transition-colors"
|
|
||||||
>
|
|
||||||
<ArrowLeft
|
|
||||||
className="mr-1 h-3.5 w-3.5 sm:h-4 sm:w-4"
|
|
||||||
/>
|
|
||||||
Back to Login
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="mt-3 sm:mt-4 pt-3 sm:pt-4 border-t border-gray-200">
|
|
||||||
<Link
|
|
||||||
to="/"
|
|
||||||
className="w-full inline-flex items-center justify-center gap-2 px-3 sm:px-4 py-2 sm:py-2.5 rounded-lg bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-[#0f0f0f] font-semibold text-xs sm:text-sm shadow-lg shadow-[#d4af37]/30 hover:from-[#f5d76e] hover:to-[#d4af37] hover:shadow-xl hover:shadow-[#d4af37]/40 transition-all duration-300 ease-out hover:-translate-y-0.5 active:translate-y-0 relative overflow-hidden group"
|
|
||||||
>
|
|
||||||
<span className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/20 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700"></span>
|
|
||||||
<Home className="w-3.5 h-3.5 sm:w-4 sm:h-4 relative z-10" />
|
|
||||||
<span className="relative z-10 tracking-wide">Back to Homepage</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
{!isSuccess && (
|
|
||||||
<div className="text-center text-xs sm:text-sm text-gray-500 px-2">
|
|
||||||
<p>
|
|
||||||
Don't have an account?{' '}
|
|
||||||
<Link
|
|
||||||
to="/register"
|
|
||||||
className="font-medium text-blue-600
|
|
||||||
hover:underline"
|
|
||||||
>
|
|
||||||
Register now
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div
|
|
||||||
className="bg-white rounded-lg shadow-sm
|
|
||||||
border border-gray-200 p-3 sm:p-4"
|
|
||||||
>
|
|
||||||
<h3
|
|
||||||
className="text-xs sm:text-sm font-semibold text-gray-900
|
|
||||||
mb-1.5 sm:mb-2"
|
|
||||||
>
|
|
||||||
Need Help?
|
|
||||||
</h3>
|
|
||||||
<p className="text-[10px] sm:text-xs text-gray-600 leading-relaxed">
|
|
||||||
If you're having trouble resetting your password,
|
|
||||||
please contact our support team via email{' '}
|
|
||||||
<a
|
|
||||||
href={`mailto:${supportEmail}`}
|
|
||||||
className="text-blue-600 hover:underline break-all"
|
|
||||||
>
|
|
||||||
{supportEmail}
|
|
||||||
</a>
|
|
||||||
{supportPhone && (
|
|
||||||
<>
|
|
||||||
{' '}or hotline{' '}
|
|
||||||
<a
|
|
||||||
href={`tel:${supportPhone.replace(/\s+/g, '').replace(/[()]/g, '')}`}
|
|
||||||
className="text-blue-600 hover:underline"
|
|
||||||
>
|
|
||||||
{supportPhone}
|
|
||||||
</a>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ForgotPasswordPage;
|
|
||||||
@@ -1,540 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
|
||||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
|
||||||
import {
|
|
||||||
Eye,
|
|
||||||
EyeOff,
|
|
||||||
LogIn,
|
|
||||||
Loader2,
|
|
||||||
Mail,
|
|
||||||
Lock,
|
|
||||||
Hotel,
|
|
||||||
Home,
|
|
||||||
Shield,
|
|
||||||
ArrowLeft
|
|
||||||
} from 'lucide-react';
|
|
||||||
import useAuthStore from '../../store/useAuthStore';
|
|
||||||
import {
|
|
||||||
loginSchema,
|
|
||||||
LoginFormData
|
|
||||||
} from '../../utils/validationSchemas';
|
|
||||||
import { useCompanySettings } from '../../contexts/CompanySettingsContext';
|
|
||||||
import * as yup from 'yup';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import Recaptcha from '../../components/common/Recaptcha';
|
|
||||||
import { recaptchaService } from '../../services/api/systemSettingsService';
|
|
||||||
|
|
||||||
const mfaTokenSchema = yup.object().shape({
|
|
||||||
mfaToken: yup
|
|
||||||
.string()
|
|
||||||
.required('MFA token is required')
|
|
||||||
.min(6, 'MFA token must be 6 digits')
|
|
||||||
.max(8, 'MFA token must be 6-8 characters')
|
|
||||||
.matches(/^\d+$|^[A-Z0-9]{8}$/, 'Invalid token format'),
|
|
||||||
});
|
|
||||||
|
|
||||||
type MFATokenFormData = yup.InferType<typeof mfaTokenSchema>;
|
|
||||||
|
|
||||||
const LoginPage: React.FC = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
const { login, verifyMFA, isLoading, error, clearError, requiresMFA, clearMFA, userInfo, isAuthenticated } =
|
|
||||||
useAuthStore();
|
|
||||||
const { settings } = useCompanySettings();
|
|
||||||
|
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
|
||||||
const [recaptchaToken, setRecaptchaToken] = useState<string | null>(null);
|
|
||||||
|
|
||||||
|
|
||||||
const {
|
|
||||||
register: registerMFA,
|
|
||||||
handleSubmit: handleSubmitMFA,
|
|
||||||
formState: { errors: mfaErrors },
|
|
||||||
} = useForm<MFATokenFormData>({
|
|
||||||
resolver: yupResolver(mfaTokenSchema),
|
|
||||||
defaultValues: {
|
|
||||||
mfaToken: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isLoading && isAuthenticated && !requiresMFA && userInfo) {
|
|
||||||
if (userInfo.role === 'admin') {
|
|
||||||
navigate('/admin/dashboard', { replace: true });
|
|
||||||
} else if (userInfo.role === 'staff') {
|
|
||||||
navigate('/staff/dashboard', { replace: true });
|
|
||||||
} else {
|
|
||||||
navigate('/', { replace: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [isLoading, isAuthenticated, requiresMFA, userInfo, navigate]);
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const companyName = settings.company_name || 'Luxury Hotel';
|
|
||||||
document.title = requiresMFA ? `Verify Identity - ${companyName}` : `Login - ${companyName}`;
|
|
||||||
}, [settings.company_name, requiresMFA]);
|
|
||||||
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<LoginFormData>({
|
|
||||||
resolver: yupResolver(loginSchema),
|
|
||||||
defaultValues: {
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
rememberMe: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const onSubmit = async (data: LoginFormData) => {
|
|
||||||
try {
|
|
||||||
clearError();
|
|
||||||
|
|
||||||
|
|
||||||
if (recaptchaToken) {
|
|
||||||
try {
|
|
||||||
const verifyResponse = await recaptchaService.verifyRecaptcha(recaptchaToken);
|
|
||||||
if (verifyResponse.status === 'error' || !verifyResponse.data.verified) {
|
|
||||||
toast.error('reCAPTCHA verification failed. Please try again.');
|
|
||||||
setRecaptchaToken(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('reCAPTCHA verification failed. Please try again.');
|
|
||||||
setRecaptchaToken(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await login({
|
|
||||||
email: data.email,
|
|
||||||
password: data.password,
|
|
||||||
rememberMe: data.rememberMe,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
if (!requiresMFA) {
|
|
||||||
|
|
||||||
const getRedirectPath = () => {
|
|
||||||
const from = location.state?.from?.pathname;
|
|
||||||
if (from) return from;
|
|
||||||
|
|
||||||
const currentUserInfo = useAuthStore.getState().userInfo;
|
|
||||||
if (currentUserInfo?.role === 'admin') {
|
|
||||||
return '/admin/dashboard';
|
|
||||||
} else if (currentUserInfo?.role === 'staff') {
|
|
||||||
return '/staff/dashboard';
|
|
||||||
}
|
|
||||||
return '/dashboard';
|
|
||||||
};
|
|
||||||
|
|
||||||
navigate(getRedirectPath(), { replace: true });
|
|
||||||
}
|
|
||||||
setRecaptchaToken(null);
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
console.error('Login error:', error);
|
|
||||||
setRecaptchaToken(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const onSubmitMFA = async (data: MFATokenFormData) => {
|
|
||||||
try {
|
|
||||||
clearError();
|
|
||||||
await verifyMFA(data.mfaToken);
|
|
||||||
|
|
||||||
|
|
||||||
const getRedirectPath = () => {
|
|
||||||
const from = location.state?.from?.pathname;
|
|
||||||
if (from) return from;
|
|
||||||
|
|
||||||
const currentUserInfo = useAuthStore.getState().userInfo;
|
|
||||||
if (currentUserInfo?.role === 'admin') {
|
|
||||||
return '/admin/dashboard';
|
|
||||||
} else if (currentUserInfo?.role === 'staff') {
|
|
||||||
return '/staff/dashboard';
|
|
||||||
}
|
|
||||||
return '/dashboard';
|
|
||||||
};
|
|
||||||
|
|
||||||
navigate(getRedirectPath(), { replace: true });
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
console.error('MFA verification error:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const handleBackToLogin = () => {
|
|
||||||
clearMFA();
|
|
||||||
clearError();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br
|
|
||||||
from-gray-50 via-gray-100 to-gray-50
|
|
||||||
flex items-center justify-center py-4 sm:py-8 lg:py-12 px-3 sm:px-4 lg:px-8 relative overflow-hidden"
|
|
||||||
>
|
|
||||||
{}
|
|
||||||
<div className="absolute inset-0 opacity-5" style={{
|
|
||||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9 0h2v20H9zM25 0h2v20h-2zM41 0h2v20h-2zM57 0h2v20h-2zM0 9h20v2H0zM0 25h20v2H0zM0 41h20v2H0zM0 57h20v2H0zM40 9h20v2H40zM40 25h20v2H40zM40 41h20v2H40zM40 57h20v2H40zM9 40h2v20H9zM25 40h2v20h-2zM41 40h2v20h-2zM57 40h2v20h-2z' fill='%23d4af37' opacity='0.05'/%3E%3C/svg%3E")`
|
|
||||||
}}></div>
|
|
||||||
|
|
||||||
<div className="max-w-md w-full space-y-4 sm:space-y-6 lg:space-y-8 relative z-10">
|
|
||||||
{}
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="flex justify-center mb-3 sm:mb-4">
|
|
||||||
{settings.company_logo_url ? (
|
|
||||||
<img
|
|
||||||
src={settings.company_logo_url.startsWith('http')
|
|
||||||
? settings.company_logo_url
|
|
||||||
: `${import.meta.env.VITE_API_URL || 'http://localhost:8000'}${settings.company_logo_url}`}
|
|
||||||
alt={settings.company_name || 'Logo'}
|
|
||||||
className="h-14 sm:h-16 lg:h-20 w-auto max-w-[150px] sm:max-w-[180px] lg:max-w-[200px] object-contain"
|
|
||||||
style={{ filter: 'drop-shadow(0 4px 6px rgba(0,0,0,0.1))' }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="relative p-3 sm:p-4 bg-gradient-to-br from-[#d4af37] to-[#c9a227] rounded-full shadow-lg shadow-[#d4af37]/30">
|
|
||||||
<Hotel className="w-8 h-8 sm:w-10 sm:h-10 lg:w-12 lg:h-12 text-[#0f0f0f] relative z-10" />
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-[#f5d76e] to-[#d4af37] rounded-full blur-xl opacity-50"></div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{settings.company_tagline && (
|
|
||||||
<p className="text-[10px] sm:text-xs text-[#d4af37] uppercase tracking-[1.5px] sm:tracking-[2px] mb-1 sm:mb-2 font-light px-2">
|
|
||||||
{settings.company_tagline}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<h2 className="text-2xl sm:text-3xl font-serif font-semibold text-gray-900 tracking-tight px-2">
|
|
||||||
{requiresMFA ? 'Verify Your Identity' : 'Welcome Back'}
|
|
||||||
</h2>
|
|
||||||
<p className="mt-1 sm:mt-2 text-xs sm:text-sm text-gray-600 font-light tracking-wide px-4">
|
|
||||||
{requiresMFA
|
|
||||||
? 'Enter the 6-digit code from your authenticator app'
|
|
||||||
: `Sign in to ${settings.company_name || 'Luxury Hotel'}`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{requiresMFA ? (
|
|
||||||
|
|
||||||
<div className="luxury-glass rounded-sm p-4 sm:p-6 lg:p-8 border border-[#d4af37]/20 shadow-2xl">
|
|
||||||
<form onSubmit={handleSubmitMFA(onSubmitMFA)}
|
|
||||||
className="space-y-4 sm:space-y-5 lg:space-y-6"
|
|
||||||
>
|
|
||||||
{}
|
|
||||||
{error && (
|
|
||||||
<div className="bg-red-50/80 backdrop-blur-sm border border-red-200
|
|
||||||
text-red-700 px-4 py-3 rounded-sm
|
|
||||||
text-sm font-light"
|
|
||||||
>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="mfaToken"
|
|
||||||
className="block text-xs sm:text-sm font-medium
|
|
||||||
text-gray-700 mb-1.5 sm:mb-2 tracking-wide"
|
|
||||||
>
|
|
||||||
Authentication Code
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-y-0 left-0
|
|
||||||
pl-3 flex items-center pointer-events-none"
|
|
||||||
>
|
|
||||||
<Shield className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
{...registerMFA('mfaToken')}
|
|
||||||
id="mfaToken"
|
|
||||||
type="text"
|
|
||||||
autoComplete="one-time-code"
|
|
||||||
maxLength={8}
|
|
||||||
className={`luxury-input pl-9 sm:pl-10 py-2.5 sm:py-3 text-sm sm:text-base text-center tracking-widest ${
|
|
||||||
mfaErrors.mfaToken
|
|
||||||
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
placeholder="000000"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{mfaErrors.mfaToken && (
|
|
||||||
<p className="mt-1 text-xs sm:text-sm text-red-600 font-light">
|
|
||||||
{mfaErrors.mfaToken.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<p className="mt-1.5 text-xs text-gray-500 font-light">
|
|
||||||
Enter the 6-digit code from your authenticator app or an 8-character backup code
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading}
|
|
||||||
className="btn-luxury-primary w-full flex items-center
|
|
||||||
justify-center py-2.5 sm:py-3 px-4 text-xs sm:text-sm relative"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="animate-spin -ml-1
|
|
||||||
mr-2 h-4 w-4 sm:h-5 sm:w-5 relative z-10"
|
|
||||||
/>
|
|
||||||
<span className="relative z-10">Verifying...</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Shield className="-ml-1 mr-2 h-4 w-4 sm:h-5 sm:w-5 relative z-10" />
|
|
||||||
<span className="relative z-10">Verify</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="text-center">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleBackToLogin}
|
|
||||||
className="inline-flex items-center
|
|
||||||
text-xs sm:text-sm font-medium text-gray-600
|
|
||||||
hover:text-gray-900 transition-colors"
|
|
||||||
>
|
|
||||||
<ArrowLeft
|
|
||||||
className="mr-1 h-3.5 w-3.5 sm:h-4 sm:w-4"
|
|
||||||
/>
|
|
||||||
Back to Login
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
|
|
||||||
<>
|
|
||||||
<div className="luxury-glass rounded-sm p-4 sm:p-6 lg:p-8 border border-[#d4af37]/20 shadow-2xl">
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}
|
|
||||||
className="space-y-4 sm:space-y-5 lg:space-y-6"
|
|
||||||
>
|
|
||||||
{}
|
|
||||||
{error && (
|
|
||||||
<div className="bg-red-50/80 backdrop-blur-sm border border-red-200
|
|
||||||
text-red-700 px-4 py-3 rounded-sm
|
|
||||||
text-sm font-light"
|
|
||||||
>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="email"
|
|
||||||
className="block text-xs sm:text-sm font-medium
|
|
||||||
text-gray-700 mb-1.5 sm:mb-2 tracking-wide"
|
|
||||||
>
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-y-0 left-0
|
|
||||||
pl-3 flex items-center pointer-events-none"
|
|
||||||
>
|
|
||||||
<Mail className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
{...register('email')}
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
autoComplete="email"
|
|
||||||
className={`luxury-input pl-9 sm:pl-10 py-2.5 sm:py-3 text-sm sm:text-base ${
|
|
||||||
errors.email
|
|
||||||
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
placeholder="email@example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.email && (
|
|
||||||
<p className="mt-1 text-sm text-red-600 font-light">
|
|
||||||
{errors.email.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="password"
|
|
||||||
className="block text-xs sm:text-sm font-medium
|
|
||||||
text-gray-700 mb-1.5 sm:mb-2 tracking-wide"
|
|
||||||
>
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-y-0 left-0
|
|
||||||
pl-3 flex items-center pointer-events-none"
|
|
||||||
>
|
|
||||||
<Lock className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
{...register('password')}
|
|
||||||
id="password"
|
|
||||||
type={showPassword ? 'text' : 'password'}
|
|
||||||
autoComplete="current-password"
|
|
||||||
className={`luxury-input pl-9 sm:pl-10 pr-9 sm:pr-10 py-2.5 sm:py-3 text-sm sm:text-base ${
|
|
||||||
errors.password
|
|
||||||
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
|
||||||
className="absolute inset-y-0 right-0
|
|
||||||
pr-3 flex items-center transition-colors
|
|
||||||
hover:text-[#d4af37]"
|
|
||||||
>
|
|
||||||
{showPassword ? (
|
|
||||||
<EyeOff className="h-4 w-4 sm:h-5 sm:w-5
|
|
||||||
text-gray-400"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Eye className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{errors.password && (
|
|
||||||
<p className="mt-1 text-sm text-red-600 font-light">
|
|
||||||
{errors.password.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 sm:gap-0">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<input
|
|
||||||
{...register('rememberMe')}
|
|
||||||
id="rememberMe"
|
|
||||||
type="checkbox"
|
|
||||||
className="h-4 w-4 text-[#d4af37]
|
|
||||||
focus:ring-[#d4af37]/50 border-gray-300
|
|
||||||
rounded-sm cursor-pointer accent-[#d4af37]"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="rememberMe"
|
|
||||||
className="ml-2 block text-xs sm:text-sm
|
|
||||||
text-gray-700 cursor-pointer font-light tracking-wide"
|
|
||||||
>
|
|
||||||
Remember me
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
to="/forgot-password"
|
|
||||||
className="text-xs sm:text-sm font-medium
|
|
||||||
text-[#d4af37] hover:text-[#c9a227]
|
|
||||||
transition-colors tracking-wide"
|
|
||||||
>
|
|
||||||
Forgot password?
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<Recaptcha
|
|
||||||
onChange={(token) => setRecaptchaToken(token)}
|
|
||||||
onError={(error) => {
|
|
||||||
console.error('reCAPTCHA error:', error);
|
|
||||||
setRecaptchaToken(null);
|
|
||||||
}}
|
|
||||||
theme="light"
|
|
||||||
size="normal"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading}
|
|
||||||
className="btn-luxury-primary w-full flex items-center
|
|
||||||
justify-center py-2.5 sm:py-3 px-4 text-xs sm:text-sm relative"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="animate-spin -ml-1
|
|
||||||
mr-2 h-4 w-4 sm:h-5 sm:w-5 relative z-10"
|
|
||||||
/>
|
|
||||||
<span className="relative z-10">Processing...</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<LogIn className="-ml-1 mr-2 h-4 w-4 sm:h-5 sm:w-5 relative z-10" />
|
|
||||||
<span className="relative z-10">Sign In</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="mt-3 sm:mt-4 pt-3 sm:pt-4 border-t border-gray-200">
|
|
||||||
<Link
|
|
||||||
to="/"
|
|
||||||
className="w-full inline-flex items-center justify-center gap-2 px-3 sm:px-4 py-2 sm:py-2.5 rounded-lg bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-[#0f0f0f] font-semibold text-xs sm:text-sm shadow-lg shadow-[#d4af37]/30 hover:from-[#f5d76e] hover:to-[#d4af37] hover:shadow-xl hover:shadow-[#d4af37]/40 transition-all duration-300 ease-out hover:-translate-y-0.5 active:translate-y-0 relative overflow-hidden group"
|
|
||||||
>
|
|
||||||
<span className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/20 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700"></span>
|
|
||||||
<Home className="w-3.5 h-3.5 sm:w-4 sm:h-4 relative z-10" />
|
|
||||||
<span className="relative z-10 tracking-wide">Back to Homepage</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="mt-4 sm:mt-6 text-center">
|
|
||||||
<p className="text-xs sm:text-sm text-gray-600 font-light tracking-wide">
|
|
||||||
Don't have an account?{' '}
|
|
||||||
<Link
|
|
||||||
to="/register"
|
|
||||||
className="font-medium text-[#d4af37]
|
|
||||||
hover:text-[#c9a227] transition-colors"
|
|
||||||
>
|
|
||||||
Register now
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="text-center text-xs sm:text-sm text-gray-500 font-light tracking-wide px-2">
|
|
||||||
<p>
|
|
||||||
By logging in, you agree to our{' '}
|
|
||||||
<Link
|
|
||||||
to="/terms"
|
|
||||||
className="text-[#d4af37] hover:underline"
|
|
||||||
>
|
|
||||||
Terms of Service
|
|
||||||
</Link>{' '}
|
|
||||||
and{' '}
|
|
||||||
<Link
|
|
||||||
to="/privacy"
|
|
||||||
className="text-[#d4af37] hover:underline"
|
|
||||||
>
|
|
||||||
Privacy Policy
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LoginPage;
|
|
||||||
@@ -1,571 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
|
||||||
import {
|
|
||||||
Eye,
|
|
||||||
EyeOff,
|
|
||||||
UserPlus,
|
|
||||||
Loader2,
|
|
||||||
Mail,
|
|
||||||
Lock,
|
|
||||||
User,
|
|
||||||
Phone,
|
|
||||||
Hotel,
|
|
||||||
CheckCircle2,
|
|
||||||
XCircle,
|
|
||||||
Home,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import useAuthStore from '../../store/useAuthStore';
|
|
||||||
import {
|
|
||||||
registerSchema,
|
|
||||||
RegisterFormData,
|
|
||||||
} from '../../utils/validationSchemas';
|
|
||||||
import { useCompanySettings } from '../../contexts/CompanySettingsContext';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import Recaptcha from '../../components/common/Recaptcha';
|
|
||||||
import { recaptchaService } from '../../services/api/systemSettingsService';
|
|
||||||
|
|
||||||
const RegisterPage: React.FC = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { register: registerUser, isLoading, error, clearError } =
|
|
||||||
useAuthStore();
|
|
||||||
const { settings } = useCompanySettings();
|
|
||||||
|
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
|
||||||
const [showConfirmPassword, setShowConfirmPassword] =
|
|
||||||
useState(false);
|
|
||||||
const [recaptchaToken, setRecaptchaToken] = useState<string | null>(null);
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const companyName = settings.company_name || 'Luxury Hotel';
|
|
||||||
document.title = `Register - ${companyName}`;
|
|
||||||
}, [settings.company_name]);
|
|
||||||
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
watch,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<RegisterFormData>({
|
|
||||||
resolver: yupResolver(registerSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
confirmPassword: '',
|
|
||||||
phone: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const password = watch('password');
|
|
||||||
|
|
||||||
|
|
||||||
const getPasswordStrength = (pwd: string) => {
|
|
||||||
if (!pwd) return { strength: 0, label: '', color: '' };
|
|
||||||
|
|
||||||
let strength = 0;
|
|
||||||
if (pwd.length >= 8) strength++;
|
|
||||||
if (/[a-z]/.test(pwd)) strength++;
|
|
||||||
if (/[A-Z]/.test(pwd)) strength++;
|
|
||||||
if (/\d/.test(pwd)) strength++;
|
|
||||||
if (/[@$!%*?&]/.test(pwd)) strength++;
|
|
||||||
|
|
||||||
const labels = [
|
|
||||||
{ label: 'Very Weak', color: 'bg-red-500' },
|
|
||||||
{ label: 'Weak', color: 'bg-orange-500' },
|
|
||||||
{ label: 'Medium', color: 'bg-yellow-500' },
|
|
||||||
{ label: 'Strong', color: 'bg-blue-500' },
|
|
||||||
{ label: 'Very Strong', color: 'bg-green-500' },
|
|
||||||
];
|
|
||||||
|
|
||||||
return { strength, ...labels[strength] };
|
|
||||||
};
|
|
||||||
|
|
||||||
const passwordStrength = getPasswordStrength(password || '');
|
|
||||||
|
|
||||||
|
|
||||||
const onSubmit = async (data: RegisterFormData) => {
|
|
||||||
try {
|
|
||||||
clearError();
|
|
||||||
|
|
||||||
|
|
||||||
if (recaptchaToken) {
|
|
||||||
try {
|
|
||||||
const verifyResponse = await recaptchaService.verifyRecaptcha(recaptchaToken);
|
|
||||||
if (verifyResponse.status === 'error' || !verifyResponse.data.verified) {
|
|
||||||
toast.error('reCAPTCHA verification failed. Please try again.');
|
|
||||||
setRecaptchaToken(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('reCAPTCHA verification failed. Please try again.');
|
|
||||||
setRecaptchaToken(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await registerUser({
|
|
||||||
name: data.name,
|
|
||||||
email: data.email,
|
|
||||||
password: data.password,
|
|
||||||
phone: data.phone,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
navigate('/login', { replace: true });
|
|
||||||
setRecaptchaToken(null);
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
console.error('Register error:', error);
|
|
||||||
setRecaptchaToken(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="min-h-screen bg-gradient-to-br
|
|
||||||
from-gray-50 via-gray-100 to-gray-50 flex items-center
|
|
||||||
justify-center py-4 sm:py-8 lg:py-12 px-3 sm:px-4 lg:px-8 relative overflow-hidden"
|
|
||||||
>
|
|
||||||
{}
|
|
||||||
<div className="absolute inset-0 opacity-5" style={{
|
|
||||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9 0h2v20H9zM25 0h2v20h-2zM41 0h2v20h-2zM57 0h2v20h-2zM0 9h20v2H0zM0 25h20v2H0zM0 41h20v2H0zM0 57h20v2H0zM40 9h20v2H40zM40 25h20v2H40zM40 41h20v2H40zM40 57h20v2H40zM9 40h2v20H9zM25 40h2v20h-2zM41 40h2v20h-2zM57 40h2v20h-2z' fill='%23d4af37' opacity='0.05'/%3E%3C/svg%3E")`
|
|
||||||
}}></div>
|
|
||||||
|
|
||||||
<div className="max-w-md w-full space-y-4 sm:space-y-6 lg:space-y-8 relative z-10">
|
|
||||||
{}
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="flex justify-center mb-3 sm:mb-4">
|
|
||||||
{settings.company_logo_url ? (
|
|
||||||
<img
|
|
||||||
src={settings.company_logo_url.startsWith('http')
|
|
||||||
? settings.company_logo_url
|
|
||||||
: `${import.meta.env.VITE_API_URL || 'http://localhost:8000'}${settings.company_logo_url}`}
|
|
||||||
alt={settings.company_name || 'Logo'}
|
|
||||||
className="h-14 sm:h-16 lg:h-20 w-auto max-w-[150px] sm:max-w-[180px] lg:max-w-[200px] object-contain"
|
|
||||||
style={{ filter: 'drop-shadow(0 4px 6px rgba(0,0,0,0.1))' }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="relative p-3 sm:p-4 bg-gradient-to-br from-[#d4af37] to-[#c9a227] rounded-full shadow-lg shadow-[#d4af37]/30">
|
|
||||||
<Hotel className="w-8 h-8 sm:w-10 sm:h-10 lg:w-12 lg:h-12 text-[#0f0f0f] relative z-10" />
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-[#f5d76e] to-[#d4af37] rounded-full blur-xl opacity-50"></div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{settings.company_tagline && (
|
|
||||||
<p className="text-[10px] sm:text-xs text-[#d4af37] uppercase tracking-[1.5px] sm:tracking-[2px] mb-1 sm:mb-2 font-light px-2">
|
|
||||||
{settings.company_tagline}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<h2 className="text-2xl sm:text-3xl font-serif font-semibold text-gray-900 tracking-tight px-2">
|
|
||||||
Create Account
|
|
||||||
</h2>
|
|
||||||
<p className="mt-1 sm:mt-2 text-xs sm:text-sm text-gray-600 font-light tracking-wide px-4">
|
|
||||||
Join {settings.company_name || 'Luxury Hotel'} for exclusive benefits
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="luxury-glass rounded-sm p-4 sm:p-6 lg:p-8 border border-[#d4af37]/20 shadow-2xl">
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
className="space-y-4 sm:space-y-5"
|
|
||||||
>
|
|
||||||
{}
|
|
||||||
{error && (
|
|
||||||
<div
|
|
||||||
className="bg-red-50/80 backdrop-blur-sm border border-red-200
|
|
||||||
text-red-700 px-4 py-3 rounded-sm
|
|
||||||
text-sm font-light"
|
|
||||||
>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="name"
|
|
||||||
className="block text-xs sm:text-sm font-medium
|
|
||||||
text-gray-700 mb-1.5 sm:mb-2 tracking-wide"
|
|
||||||
>
|
|
||||||
Full Name
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div
|
|
||||||
className="absolute inset-y-0 left-0
|
|
||||||
pl-3 flex items-center
|
|
||||||
pointer-events-none"
|
|
||||||
>
|
|
||||||
<User className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
{...register('name')}
|
|
||||||
id="name"
|
|
||||||
type="text"
|
|
||||||
autoComplete="name"
|
|
||||||
className={`luxury-input pl-9 sm:pl-10 py-2.5 sm:py-3 text-sm sm:text-base ${
|
|
||||||
errors.name
|
|
||||||
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
placeholder="John Doe"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.name && (
|
|
||||||
<p className="mt-1 text-xs sm:text-sm text-red-600 font-light">
|
|
||||||
{errors.name.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="email"
|
|
||||||
className="block text-xs sm:text-sm font-medium
|
|
||||||
text-gray-700 mb-1.5 sm:mb-2 tracking-wide"
|
|
||||||
>
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div
|
|
||||||
className="absolute inset-y-0 left-0
|
|
||||||
pl-3 flex items-center
|
|
||||||
pointer-events-none"
|
|
||||||
>
|
|
||||||
<Mail className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
{...register('email')}
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
autoComplete="email"
|
|
||||||
className={`luxury-input pl-9 sm:pl-10 py-2.5 sm:py-3 text-sm sm:text-base ${
|
|
||||||
errors.email
|
|
||||||
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
placeholder="email@example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.email && (
|
|
||||||
<p className="mt-1 text-xs sm:text-sm text-red-600 font-light">
|
|
||||||
{errors.email.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="phone"
|
|
||||||
className="block text-xs sm:text-sm font-medium
|
|
||||||
text-gray-700 mb-1.5 sm:mb-2 tracking-wide"
|
|
||||||
>
|
|
||||||
Phone Number (Optional)
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div
|
|
||||||
className="absolute inset-y-0 left-0
|
|
||||||
pl-3 flex items-center
|
|
||||||
pointer-events-none"
|
|
||||||
>
|
|
||||||
<Phone className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
{...register('phone')}
|
|
||||||
id="phone"
|
|
||||||
type="tel"
|
|
||||||
autoComplete="tel"
|
|
||||||
className={`luxury-input pl-9 sm:pl-10 py-2.5 sm:py-3 text-sm sm:text-base ${
|
|
||||||
errors.phone
|
|
||||||
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
placeholder="0123456789"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.phone && (
|
|
||||||
<p className="mt-1 text-xs sm:text-sm text-red-600 font-light">
|
|
||||||
{errors.phone.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="password"
|
|
||||||
className="block text-xs sm:text-sm font-medium
|
|
||||||
text-gray-700 mb-1.5 sm:mb-2 tracking-wide"
|
|
||||||
>
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div
|
|
||||||
className="absolute inset-y-0 left-0
|
|
||||||
pl-3 flex items-center
|
|
||||||
pointer-events-none"
|
|
||||||
>
|
|
||||||
<Lock className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
{...register('password')}
|
|
||||||
id="password"
|
|
||||||
type={showPassword ? 'text' : 'password'}
|
|
||||||
autoComplete="new-password"
|
|
||||||
className={`luxury-input pl-9 sm:pl-10 pr-9 sm:pr-10 py-2.5 sm:py-3 text-sm sm:text-base ${
|
|
||||||
errors.password
|
|
||||||
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
|
||||||
className="absolute inset-y-0 right-0
|
|
||||||
pr-3 flex items-center transition-colors
|
|
||||||
hover:text-[#d4af37]"
|
|
||||||
>
|
|
||||||
{showPassword ? (
|
|
||||||
<EyeOff
|
|
||||||
className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Eye
|
|
||||||
className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{errors.password && (
|
|
||||||
<p className="mt-1 text-xs sm:text-sm text-red-600 font-light">
|
|
||||||
{errors.password.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{}
|
|
||||||
{password && password.length > 0 && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="flex-1 h-2 bg-gray-200
|
|
||||||
rounded-full overflow-hidden"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`h-full transition-all
|
|
||||||
duration-300 ${
|
|
||||||
passwordStrength.strength >= 4
|
|
||||||
? 'bg-[#d4af37]'
|
|
||||||
: passwordStrength.strength >= 3
|
|
||||||
? 'bg-yellow-500'
|
|
||||||
: 'bg-red-500'
|
|
||||||
}`}
|
|
||||||
style={{
|
|
||||||
width: `${
|
|
||||||
(passwordStrength.strength / 5) * 100
|
|
||||||
}%`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-[10px] sm:text-xs font-medium
|
|
||||||
text-gray-600 tracking-wide"
|
|
||||||
>
|
|
||||||
{passwordStrength.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="mt-2 space-y-1">
|
|
||||||
<PasswordRequirement
|
|
||||||
met={password.length >= 8}
|
|
||||||
text="At least 8 characters"
|
|
||||||
/>
|
|
||||||
<PasswordRequirement
|
|
||||||
met={/[a-z]/.test(password)}
|
|
||||||
text="Lowercase letter (a-z)"
|
|
||||||
/>
|
|
||||||
<PasswordRequirement
|
|
||||||
met={/[A-Z]/.test(password)}
|
|
||||||
text="Uppercase letter (A-Z)"
|
|
||||||
/>
|
|
||||||
<PasswordRequirement
|
|
||||||
met={/\d/.test(password)}
|
|
||||||
text="Number (0-9)"
|
|
||||||
/>
|
|
||||||
<PasswordRequirement
|
|
||||||
met={/[@$!%*?&]/.test(password)}
|
|
||||||
text="Special character (@$!%*?&)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="confirmPassword"
|
|
||||||
className="block text-xs sm:text-sm font-medium
|
|
||||||
text-gray-700 mb-1.5 sm:mb-2 tracking-wide"
|
|
||||||
>
|
|
||||||
Confirm Password
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div
|
|
||||||
className="absolute inset-y-0 left-0
|
|
||||||
pl-3 flex items-center
|
|
||||||
pointer-events-none"
|
|
||||||
>
|
|
||||||
<Lock className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400" />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
{...register('confirmPassword')}
|
|
||||||
id="confirmPassword"
|
|
||||||
type={
|
|
||||||
showConfirmPassword ? 'text' : 'password'
|
|
||||||
}
|
|
||||||
autoComplete="new-password"
|
|
||||||
className={`luxury-input pl-9 sm:pl-10 pr-9 sm:pr-10 py-2.5 sm:py-3 text-sm sm:text-base ${
|
|
||||||
errors.confirmPassword
|
|
||||||
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
setShowConfirmPassword(!showConfirmPassword)
|
|
||||||
}
|
|
||||||
className="absolute inset-y-0 right-0
|
|
||||||
pr-3 flex items-center transition-colors
|
|
||||||
hover:text-[#d4af37]"
|
|
||||||
>
|
|
||||||
{showConfirmPassword ? (
|
|
||||||
<EyeOff
|
|
||||||
className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Eye
|
|
||||||
className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{errors.confirmPassword && (
|
|
||||||
<p className="mt-1 text-xs sm:text-sm text-red-600 font-light">
|
|
||||||
{errors.confirmPassword.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<Recaptcha
|
|
||||||
onChange={(token) => setRecaptchaToken(token)}
|
|
||||||
onError={(error) => {
|
|
||||||
console.error('reCAPTCHA error:', error);
|
|
||||||
setRecaptchaToken(null);
|
|
||||||
}}
|
|
||||||
theme="light"
|
|
||||||
size="normal"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading}
|
|
||||||
className="btn-luxury-primary w-full flex items-center
|
|
||||||
justify-center py-2.5 sm:py-3 px-4 text-xs sm:text-sm relative"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="animate-spin -ml-1
|
|
||||||
mr-2 h-4 w-4 sm:h-5 sm:w-5 relative z-10"
|
|
||||||
/>
|
|
||||||
<span className="relative z-10">Processing...</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<UserPlus className="-ml-1 mr-2 h-4 w-4 sm:h-5 sm:w-5 relative z-10" />
|
|
||||||
<span className="relative z-10">Register</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="mt-4 sm:mt-6 text-center">
|
|
||||||
<p className="text-xs sm:text-sm text-gray-600 font-light tracking-wide">
|
|
||||||
Already have an account?{' '}
|
|
||||||
<Link
|
|
||||||
to="/login"
|
|
||||||
className="font-medium text-[#d4af37]
|
|
||||||
hover:text-[#c9a227] transition-colors"
|
|
||||||
>
|
|
||||||
Login now
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="mt-3 sm:mt-4 pt-3 sm:pt-4 border-t border-gray-200">
|
|
||||||
<Link
|
|
||||||
to="/"
|
|
||||||
className="w-full inline-flex items-center justify-center gap-2 px-3 sm:px-4 py-2 sm:py-2.5 rounded-lg bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-[#0f0f0f] font-semibold text-xs sm:text-sm shadow-lg shadow-[#d4af37]/30 hover:from-[#f5d76e] hover:to-[#d4af37] hover:shadow-xl hover:shadow-[#d4af37]/40 transition-all duration-300 ease-out hover:-translate-y-0.5 active:translate-y-0 relative overflow-hidden group"
|
|
||||||
>
|
|
||||||
<span className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/20 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700"></span>
|
|
||||||
<Home className="w-3.5 h-3.5 sm:w-4 sm:h-4 relative z-10" />
|
|
||||||
<span className="relative z-10 tracking-wide">Back to Homepage</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="text-center text-xs sm:text-sm text-gray-500 font-light tracking-wide px-2">
|
|
||||||
<p>
|
|
||||||
By registering, you agree to our{' '}
|
|
||||||
<Link
|
|
||||||
to="/terms"
|
|
||||||
className="text-[#d4af37] hover:underline"
|
|
||||||
>
|
|
||||||
Terms of Service
|
|
||||||
</Link>{' '}
|
|
||||||
and{' '}
|
|
||||||
<Link
|
|
||||||
to="/privacy"
|
|
||||||
className="text-[#d4af37] hover:underline"
|
|
||||||
>
|
|
||||||
Privacy Policy
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const PasswordRequirement: React.FC<{
|
|
||||||
met: boolean;
|
|
||||||
text: string;
|
|
||||||
}> = ({ met, text }) => (
|
|
||||||
<div className="flex items-center gap-1.5 sm:gap-2 text-[10px] sm:text-xs font-light">
|
|
||||||
{met ? (
|
|
||||||
<CheckCircle2 className="h-3.5 w-3.5 sm:h-4 sm:w-4 text-[#d4af37] flex-shrink-0" />
|
|
||||||
) : (
|
|
||||||
<XCircle className="h-3.5 w-3.5 sm:h-4 sm:w-4 text-gray-300 flex-shrink-0" />
|
|
||||||
)}
|
|
||||||
<span className={met ? 'text-[#c9a227] font-medium' : 'text-gray-500'}>
|
|
||||||
{text}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default RegisterPage;
|
|
||||||
@@ -1,579 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
|
||||||
import { useNavigate, useParams, Link } from 'react-router-dom';
|
|
||||||
import {
|
|
||||||
Eye,
|
|
||||||
EyeOff,
|
|
||||||
Lock,
|
|
||||||
Loader2,
|
|
||||||
CheckCircle2,
|
|
||||||
XCircle,
|
|
||||||
AlertCircle,
|
|
||||||
KeyRound,
|
|
||||||
Hotel,
|
|
||||||
Home,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import useAuthStore from '../../store/useAuthStore';
|
|
||||||
import {
|
|
||||||
resetPasswordSchema,
|
|
||||||
ResetPasswordFormData,
|
|
||||||
} from '../../utils/validationSchemas';
|
|
||||||
import { useCompanySettings } from '../../contexts/CompanySettingsContext';
|
|
||||||
|
|
||||||
const ResetPasswordPage: React.FC = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { token } = useParams<{ token: string }>();
|
|
||||||
const { resetPassword, isLoading, error, clearError } =
|
|
||||||
useAuthStore();
|
|
||||||
const { settings } = useCompanySettings();
|
|
||||||
|
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
|
||||||
const [showConfirmPassword, setShowConfirmPassword] =
|
|
||||||
useState(false);
|
|
||||||
const [isSuccess, setIsSuccess] = useState(false);
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const companyName = settings.company_name || 'Luxury Hotel';
|
|
||||||
document.title = `Reset Password - ${companyName}`;
|
|
||||||
}, [settings.company_name]);
|
|
||||||
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
watch,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<ResetPasswordFormData>({
|
|
||||||
resolver: yupResolver(resetPasswordSchema),
|
|
||||||
defaultValues: {
|
|
||||||
password: '',
|
|
||||||
confirmPassword: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const password = watch('password');
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!token) {
|
|
||||||
navigate('/forgot-password', { replace: true });
|
|
||||||
}
|
|
||||||
}, [token, navigate]);
|
|
||||||
|
|
||||||
|
|
||||||
const getPasswordStrength = (pwd: string) => {
|
|
||||||
if (!pwd) return { strength: 0, label: '', color: '' };
|
|
||||||
|
|
||||||
let strength = 0;
|
|
||||||
if (pwd.length >= 8) strength++;
|
|
||||||
if (/[a-z]/.test(pwd)) strength++;
|
|
||||||
if (/[A-Z]/.test(pwd)) strength++;
|
|
||||||
if (/\d/.test(pwd)) strength++;
|
|
||||||
if (/[@$!%*?&]/.test(pwd)) strength++;
|
|
||||||
|
|
||||||
const labels = [
|
|
||||||
{ label: 'Very Weak', color: 'bg-red-500' },
|
|
||||||
{ label: 'Weak', color: 'bg-orange-500' },
|
|
||||||
{ label: 'Medium', color: 'bg-yellow-500' },
|
|
||||||
{ label: 'Strong', color: 'bg-blue-500' },
|
|
||||||
{ label: 'Very Strong', color: 'bg-green-500' },
|
|
||||||
];
|
|
||||||
|
|
||||||
return { strength, ...labels[strength] };
|
|
||||||
};
|
|
||||||
|
|
||||||
const passwordStrength = getPasswordStrength(password || '');
|
|
||||||
|
|
||||||
|
|
||||||
const onSubmit = async (data: ResetPasswordFormData) => {
|
|
||||||
if (!token) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
clearError();
|
|
||||||
await resetPassword({
|
|
||||||
token,
|
|
||||||
password: data.password,
|
|
||||||
confirmPassword: data.confirmPassword,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
setIsSuccess(true);
|
|
||||||
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
navigate('/login', { replace: true });
|
|
||||||
}, 3000);
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
console.error('Reset password error:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const isTokenError =
|
|
||||||
error?.includes('token') || error?.includes('expired');
|
|
||||||
|
|
||||||
|
|
||||||
const isReuseError =
|
|
||||||
error?.toLowerCase().includes('must be different') ||
|
|
||||||
error?.toLowerCase().includes('different from old');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="min-h-screen bg-gradient-to-br
|
|
||||||
from-indigo-50 to-purple-100 flex items-center
|
|
||||||
justify-center py-4 sm:py-8 lg:py-12 px-3 sm:px-4 lg:px-8"
|
|
||||||
>
|
|
||||||
<div className="max-w-md w-full space-y-4 sm:space-y-6 lg:space-y-8">
|
|
||||||
{}
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="flex justify-center mb-3 sm:mb-4">
|
|
||||||
{settings.company_logo_url ? (
|
|
||||||
<img
|
|
||||||
src={settings.company_logo_url.startsWith('http')
|
|
||||||
? settings.company_logo_url
|
|
||||||
: `${import.meta.env.VITE_API_URL || 'http://localhost:8000'}${settings.company_logo_url}`}
|
|
||||||
alt={settings.company_name || 'Logo'}
|
|
||||||
className="h-14 sm:h-16 lg:h-20 w-auto max-w-[150px] sm:max-w-[180px] lg:max-w-[200px] object-contain"
|
|
||||||
style={{ filter: 'drop-shadow(0 4px 6px rgba(0,0,0,0.1))' }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="p-2.5 sm:p-3 bg-indigo-600 rounded-full">
|
|
||||||
<Hotel className="w-8 h-8 sm:w-10 sm:h-10 lg:w-12 lg:h-12 text-white" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{settings.company_tagline && (
|
|
||||||
<p className="text-[10px] sm:text-xs text-indigo-600 uppercase tracking-[1.5px] sm:tracking-[2px] mb-1 sm:mb-2 font-light px-2">
|
|
||||||
{settings.company_tagline}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<h2 className="text-2xl sm:text-3xl font-bold text-gray-900 px-2">
|
|
||||||
{isSuccess ? 'Complete!' : 'Reset Password'}
|
|
||||||
</h2>
|
|
||||||
<p className="mt-1 sm:mt-2 text-xs sm:text-sm text-gray-600 px-4">
|
|
||||||
{isSuccess
|
|
||||||
? 'Password has been reset successfully'
|
|
||||||
: `Enter a new password for your ${settings.company_name || 'Luxury Hotel'} account`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="bg-white rounded-lg shadow-xl p-4 sm:p-6 lg:p-8">
|
|
||||||
{isSuccess ? (
|
|
||||||
|
|
||||||
<div className="text-center space-y-4 sm:space-y-5 lg:space-y-6">
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<div
|
|
||||||
className="w-12 h-12 sm:w-14 sm:h-14 lg:w-16 lg:h-16 bg-green-100
|
|
||||||
rounded-full flex items-center
|
|
||||||
justify-center"
|
|
||||||
>
|
|
||||||
<CheckCircle2
|
|
||||||
className="w-6 h-6 sm:w-8 sm:h-8 lg:w-10 lg:h-10 text-green-600"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1.5 sm:space-y-2">
|
|
||||||
<h3
|
|
||||||
className="text-lg sm:text-xl font-semibold
|
|
||||||
text-gray-900 px-2"
|
|
||||||
>
|
|
||||||
Password reset successful!
|
|
||||||
</h3>
|
|
||||||
<p className="text-xs sm:text-sm text-gray-600 px-2">
|
|
||||||
Your password has been updated.
|
|
||||||
</p>
|
|
||||||
<p className="text-xs sm:text-sm text-gray-600 px-2">
|
|
||||||
You can now login with your new password.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="bg-blue-50 border border-blue-200
|
|
||||||
rounded-lg p-3 sm:p-4"
|
|
||||||
>
|
|
||||||
<p className="text-xs sm:text-sm text-gray-700">
|
|
||||||
Redirecting to login page...
|
|
||||||
</p>
|
|
||||||
<div className="mt-2 flex justify-center">
|
|
||||||
<Loader2
|
|
||||||
className="animate-spin h-4 w-4 sm:h-5 sm:w-5
|
|
||||||
text-blue-600"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
to="/login"
|
|
||||||
className="inline-flex items-center
|
|
||||||
justify-center w-full py-2.5 sm:py-3 px-4
|
|
||||||
border border-transparent rounded-lg
|
|
||||||
text-xs sm:text-sm font-medium text-white
|
|
||||||
bg-indigo-600 hover:bg-indigo-700
|
|
||||||
focus:outline-none focus:ring-2
|
|
||||||
focus:ring-offset-2
|
|
||||||
focus:ring-indigo-500
|
|
||||||
transition-colors"
|
|
||||||
>
|
|
||||||
<KeyRound className="-ml-1 mr-2 h-4 w-4 sm:h-5 sm:w-5" />
|
|
||||||
Login Now
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="mt-3 sm:mt-4 pt-3 sm:pt-4 border-t border-gray-200">
|
|
||||||
<Link
|
|
||||||
to="/"
|
|
||||||
className="w-full inline-flex items-center justify-center gap-2 px-3 sm:px-4 py-2 sm:py-2.5 rounded-lg bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-[#0f0f0f] font-semibold text-xs sm:text-sm shadow-lg shadow-[#d4af37]/30 hover:from-[#f5d76e] hover:to-[#d4af37] hover:shadow-xl hover:shadow-[#d4af37]/40 transition-all duration-300 ease-out hover:-translate-y-0.5 active:translate-y-0 relative overflow-hidden group"
|
|
||||||
>
|
|
||||||
<span className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/20 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700"></span>
|
|
||||||
<Home className="w-3.5 h-3.5 sm:w-4 sm:h-4 relative z-10" />
|
|
||||||
<span className="relative z-10 tracking-wide">Back to Homepage</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
className="space-y-4 sm:space-y-5"
|
|
||||||
>
|
|
||||||
{}
|
|
||||||
{error && (
|
|
||||||
<div
|
|
||||||
className={`border px-3 sm:px-4 py-2.5 sm:py-3 rounded-lg
|
|
||||||
text-xs sm:text-sm flex items-start gap-2
|
|
||||||
${
|
|
||||||
isTokenError
|
|
||||||
? 'bg-yellow-50 border-yellow-200 ' +
|
|
||||||
'text-yellow-800'
|
|
||||||
: 'bg-red-50 border-red-200 ' +
|
|
||||||
'text-red-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<AlertCircle className="h-4 w-4 sm:h-5 sm:w-5 flex-shrink-0 mt-0.5" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="font-medium">
|
|
||||||
{isReuseError
|
|
||||||
? 'New password must be different from old password'
|
|
||||||
: error}
|
|
||||||
</p>
|
|
||||||
{isTokenError && (
|
|
||||||
<Link
|
|
||||||
to="/forgot-password"
|
|
||||||
className="mt-2 inline-block text-xs sm:text-sm
|
|
||||||
font-medium underline
|
|
||||||
hover:text-yellow-900"
|
|
||||||
>
|
|
||||||
Request new link
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="password"
|
|
||||||
className="block text-xs sm:text-sm font-medium
|
|
||||||
text-gray-700 mb-1.5 sm:mb-2"
|
|
||||||
>
|
|
||||||
New Password
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div
|
|
||||||
className="absolute inset-y-0 left-0
|
|
||||||
pl-3 flex items-center
|
|
||||||
pointer-events-none"
|
|
||||||
>
|
|
||||||
<Lock
|
|
||||||
className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
{...register('password')}
|
|
||||||
id="password"
|
|
||||||
type={showPassword ? 'text' : 'password'}
|
|
||||||
autoComplete="new-password"
|
|
||||||
autoFocus
|
|
||||||
className={`block w-full pl-9 sm:pl-10 pr-9 sm:pr-10
|
|
||||||
py-2.5 sm:py-3 border rounded-lg
|
|
||||||
focus:outline-none focus:ring-2
|
|
||||||
transition-colors text-sm sm:text-base
|
|
||||||
${
|
|
||||||
errors.password
|
|
||||||
? 'border-red-300 ' +
|
|
||||||
'focus:ring-red-500'
|
|
||||||
: 'border-gray-300 ' +
|
|
||||||
'focus:ring-indigo-500'
|
|
||||||
}`}
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
setShowPassword(!showPassword)
|
|
||||||
}
|
|
||||||
className="absolute inset-y-0 right-0
|
|
||||||
pr-3 flex items-center"
|
|
||||||
>
|
|
||||||
{showPassword ? (
|
|
||||||
<EyeOff
|
|
||||||
className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400
|
|
||||||
hover:text-gray-600"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Eye
|
|
||||||
className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400
|
|
||||||
hover:text-gray-600"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{errors.password && (
|
|
||||||
<p className="mt-1 text-xs sm:text-sm text-red-600">
|
|
||||||
{errors.password.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{}
|
|
||||||
{password && password.length > 0 && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
className="flex-1 h-2 bg-gray-200
|
|
||||||
rounded-full overflow-hidden"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`h-full transition-all
|
|
||||||
duration-300
|
|
||||||
${passwordStrength.color}`}
|
|
||||||
style={{
|
|
||||||
width: `${
|
|
||||||
(passwordStrength.strength / 5) *
|
|
||||||
100
|
|
||||||
}%`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className="text-[10px] sm:text-xs font-medium
|
|
||||||
text-gray-600"
|
|
||||||
>
|
|
||||||
{passwordStrength.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="mt-2 space-y-1">
|
|
||||||
<PasswordRequirement
|
|
||||||
met={password.length >= 8}
|
|
||||||
text="At least 8 characters"
|
|
||||||
/>
|
|
||||||
<PasswordRequirement
|
|
||||||
met={/[a-z]/.test(password)}
|
|
||||||
text="Lowercase letter (a-z)"
|
|
||||||
/>
|
|
||||||
<PasswordRequirement
|
|
||||||
met={/[A-Z]/.test(password)}
|
|
||||||
text="Uppercase letter (A-Z)"
|
|
||||||
/>
|
|
||||||
<PasswordRequirement
|
|
||||||
met={/\d/.test(password)}
|
|
||||||
text="Number (0-9)"
|
|
||||||
/>
|
|
||||||
<PasswordRequirement
|
|
||||||
met={/[@$!%*?&]/.test(password)}
|
|
||||||
text="Special character (@$!%*?&)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="confirmPassword"
|
|
||||||
className="block text-xs sm:text-sm font-medium
|
|
||||||
text-gray-700 mb-1.5 sm:mb-2"
|
|
||||||
>
|
|
||||||
Confirm Password
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div
|
|
||||||
className="absolute inset-y-0 left-0
|
|
||||||
pl-3 flex items-center
|
|
||||||
pointer-events-none"
|
|
||||||
>
|
|
||||||
<Lock
|
|
||||||
className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
{...register('confirmPassword')}
|
|
||||||
id="confirmPassword"
|
|
||||||
type={
|
|
||||||
showConfirmPassword ? 'text' : 'password'
|
|
||||||
}
|
|
||||||
autoComplete="new-password"
|
|
||||||
className={`block w-full pl-9 sm:pl-10 pr-9 sm:pr-10
|
|
||||||
py-2.5 sm:py-3 border rounded-lg
|
|
||||||
focus:outline-none focus:ring-2
|
|
||||||
transition-colors text-sm sm:text-base
|
|
||||||
${
|
|
||||||
errors.confirmPassword
|
|
||||||
? 'border-red-300 ' +
|
|
||||||
'focus:ring-red-500'
|
|
||||||
: 'border-gray-300 ' +
|
|
||||||
'focus:ring-indigo-500'
|
|
||||||
}`}
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
setShowConfirmPassword(
|
|
||||||
!showConfirmPassword
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="absolute inset-y-0 right-0
|
|
||||||
pr-3 flex items-center"
|
|
||||||
>
|
|
||||||
{showConfirmPassword ? (
|
|
||||||
<EyeOff
|
|
||||||
className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400
|
|
||||||
hover:text-gray-600"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Eye
|
|
||||||
className="h-4 w-4 sm:h-5 sm:w-5 text-gray-400
|
|
||||||
hover:text-gray-600"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{errors.confirmPassword && (
|
|
||||||
<p className="mt-1 text-xs sm:text-sm text-red-600">
|
|
||||||
{errors.confirmPassword.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading}
|
|
||||||
className="w-full flex items-center
|
|
||||||
justify-center py-2.5 sm:py-3 px-4 border
|
|
||||||
border-transparent rounded-lg
|
|
||||||
shadow-sm text-xs sm:text-sm font-medium
|
|
||||||
text-white bg-indigo-600
|
|
||||||
hover:bg-indigo-700
|
|
||||||
focus:outline-none focus:ring-2
|
|
||||||
focus:ring-offset-2
|
|
||||||
focus:ring-indigo-500
|
|
||||||
disabled:opacity-50
|
|
||||||
disabled:cursor-not-allowed
|
|
||||||
transition-colors"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<Loader2
|
|
||||||
className="animate-spin -ml-1 mr-2
|
|
||||||
h-4 w-4 sm:h-5 sm:w-5"
|
|
||||||
/>
|
|
||||||
Processing...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<KeyRound
|
|
||||||
className="-ml-1 mr-2 h-4 w-4 sm:h-5 sm:w-5"
|
|
||||||
/>
|
|
||||||
Reset Password
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="text-center">
|
|
||||||
<Link
|
|
||||||
to="/login"
|
|
||||||
className="text-xs sm:text-sm font-medium
|
|
||||||
text-indigo-600 hover:text-indigo-500
|
|
||||||
transition-colors"
|
|
||||||
>
|
|
||||||
Back to Login
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="mt-3 sm:mt-4 pt-3 sm:pt-4 border-t border-gray-200">
|
|
||||||
<Link
|
|
||||||
to="/"
|
|
||||||
className="w-full inline-flex items-center justify-center gap-2 px-3 sm:px-4 py-2 sm:py-2.5 rounded-lg bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-[#0f0f0f] font-semibold text-xs sm:text-sm shadow-lg shadow-[#d4af37]/30 hover:from-[#f5d76e] hover:to-[#d4af37] hover:shadow-xl hover:shadow-[#d4af37]/40 transition-all duration-300 ease-out hover:-translate-y-0.5 active:translate-y-0 relative overflow-hidden group"
|
|
||||||
>
|
|
||||||
<span className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/20 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700"></span>
|
|
||||||
<Home className="w-3.5 h-3.5 sm:w-4 sm:h-4 relative z-10" />
|
|
||||||
<span className="relative z-10 tracking-wide">Back to Homepage</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
{!isSuccess && (
|
|
||||||
<div
|
|
||||||
className="bg-white rounded-lg shadow-sm
|
|
||||||
border border-gray-200 p-3 sm:p-4"
|
|
||||||
>
|
|
||||||
<h3
|
|
||||||
className="text-xs sm:text-sm font-semibold
|
|
||||||
text-gray-900 mb-1.5 sm:mb-2 flex items-center
|
|
||||||
gap-2"
|
|
||||||
>
|
|
||||||
<Lock className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
|
|
||||||
Security
|
|
||||||
</h3>
|
|
||||||
<ul
|
|
||||||
className="text-[10px] sm:text-xs text-gray-600 space-y-1
|
|
||||||
list-disc list-inside"
|
|
||||||
>
|
|
||||||
<li>Reset link is valid for 1 hour only</li>
|
|
||||||
<li>Password is securely encrypted</li>
|
|
||||||
<li>
|
|
||||||
If the link expires, please request a new link
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const PasswordRequirement: React.FC<{
|
|
||||||
met: boolean;
|
|
||||||
text: string;
|
|
||||||
}> = ({ met, text }) => (
|
|
||||||
<div className="flex items-center gap-1.5 sm:gap-2 text-[10px] sm:text-xs">
|
|
||||||
{met ? (
|
|
||||||
<CheckCircle2 className="h-3.5 w-3.5 sm:h-4 sm:w-4 text-green-500 flex-shrink-0" />
|
|
||||||
) : (
|
|
||||||
<XCircle className="h-3.5 w-3.5 sm:h-4 sm:w-4 text-gray-300 flex-shrink-0" />
|
|
||||||
)}
|
|
||||||
<span className={met ? 'text-green-600' : 'text-gray-500'}>
|
|
||||||
{text}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default ResetPasswordPage;
|
|
||||||
@@ -6,9 +6,11 @@ import { RoomCard, RoomCardSkeleton } from
|
|||||||
import useFavoritesStore from
|
import useFavoritesStore from
|
||||||
'../../store/useFavoritesStore';
|
'../../store/useFavoritesStore';
|
||||||
import useAuthStore from '../../store/useAuthStore';
|
import useAuthStore from '../../store/useAuthStore';
|
||||||
|
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||||
|
|
||||||
const FavoritesPage: React.FC = () => {
|
const FavoritesPage: React.FC = () => {
|
||||||
const { isAuthenticated } = useAuthStore();
|
const { isAuthenticated } = useAuthStore();
|
||||||
|
const { openModal } = useAuthModal();
|
||||||
const {
|
const {
|
||||||
favorites,
|
favorites,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -44,15 +46,15 @@ const FavoritesPage: React.FC = () => {
|
|||||||
<p className="text-gray-600 mb-4">
|
<p className="text-gray-600 mb-4">
|
||||||
You need to login to view your favorites list
|
You need to login to view your favorites list
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<button
|
||||||
to="/login"
|
onClick={() => openModal('login')}
|
||||||
className="inline-block px-6 py-3
|
className="inline-block px-6 py-3
|
||||||
bg-indigo-600 text-white rounded-lg
|
bg-indigo-600 text-white rounded-lg
|
||||||
hover:bg-indigo-700 transition-colors
|
hover:bg-indigo-700 transition-colors
|
||||||
font-semibold"
|
font-semibold"
|
||||||
>
|
>
|
||||||
Login
|
Login
|
||||||
</Link>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user