|
import React, { useEffect, useState, lazy, Suspense } from 'react'; |
|
import { Routes, Route, useLocation } from 'react-router-dom'; |
|
import { useDispatch, useSelector } from 'react-redux'; |
|
import { getCurrentUser, checkCachedAuth, autoLogin } from './store/reducers/authSlice'; |
|
import cookieService from './services/cookieService'; |
|
import Login from './pages/Login.jsx'; |
|
import Register from './pages/Register.jsx'; |
|
import Dashboard from './pages/Dashboard.jsx'; |
|
import Sources from './pages/Sources.jsx'; |
|
import Accounts from './pages/Accounts.jsx'; |
|
import Posts from './pages/Posts.jsx'; |
|
import Schedule from './pages/Schedule.jsx'; |
|
import Home from './pages/Home.jsx'; |
|
import Header from './components/Header/Header.jsx'; |
|
import Sidebar from './components/Sidebar/Sidebar.jsx'; |
|
import LinkedInCallbackHandler from './components/LinkedInAccount/LinkedInCallbackHandler.jsx'; |
|
import './css/main.css'; |
|
|
|
|
|
const LazyFeatureCard = lazy(() => import('./components/FeatureCard')); |
|
const LazyTestimonialCard = lazy(() => import('./components/TestimonialCard')); |
|
|
|
|
|
class ErrorBoundary extends React.Component { |
|
constructor(props) { |
|
super(props); |
|
this.state = { hasError: false, error: null }; |
|
} |
|
|
|
static getDerivedStateFromError(error) { |
|
return { hasError: true, error }; |
|
} |
|
|
|
componentDidCatch(error, errorInfo) { |
|
console.error('Error caught by boundary:', error, errorInfo); |
|
} |
|
|
|
render() { |
|
if (this.state.hasError) { |
|
return ( |
|
<div style={{ padding: '20px', textAlign: 'center' }}> |
|
<h2>Something went wrong.</h2> |
|
<p>Please refresh the page or try again later.</p> |
|
<button onClick={() => window.location.reload()}> |
|
Refresh Page |
|
</button> |
|
</div> |
|
); |
|
} |
|
|
|
return this.props.children; |
|
} |
|
} |
|
|
|
function App() { |
|
const dispatch = useDispatch(); |
|
const { isAuthenticated, loading } = useSelector(state => state.auth); |
|
const location = useLocation(); |
|
|
|
const [isCheckingAuth, setIsCheckingAuth] = useState(true); |
|
|
|
|
|
const isAuthRoute = location.pathname === '/login' || location.pathname === '/register'; |
|
const isCallbackRoute = location.pathname === '/linkedin/callback'; |
|
const showSidebar = isAuthenticated && !isAuthRoute && !isCallbackRoute && location.pathname !== '/'; |
|
|
|
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); |
|
const [isMobile, setIsMobile] = useState(false); |
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); |
|
|
|
|
|
useEffect(() => { |
|
const checkMobile = () => { |
|
const mobile = window.innerWidth < 1024; |
|
setIsMobile(mobile); |
|
|
|
|
|
if (mobile && !isSidebarCollapsed) { |
|
setIsSidebarCollapsed(true); |
|
} |
|
}; |
|
|
|
checkMobile(); |
|
window.addEventListener('resize', checkMobile); |
|
return () => window.removeEventListener('resize', checkMobile); |
|
}, [isSidebarCollapsed]); |
|
|
|
|
|
useEffect(() => { |
|
if (isMobile) { |
|
|
|
document.body.classList.add('mobile-optimized-animation'); |
|
|
|
|
|
const mobileElements = document.querySelectorAll('.mobile-accelerate'); |
|
mobileElements.forEach(el => { |
|
el.classList.add('mobile-accelerated'); |
|
}); |
|
|
|
|
|
const touchElements = document.querySelectorAll('.touch-optimized'); |
|
touchElements.forEach(el => { |
|
el.classList.add('touch-optimized'); |
|
}); |
|
|
|
|
|
const preventZoom = (e) => { |
|
if (e.detail > 1) { |
|
e.preventDefault(); |
|
} |
|
}; |
|
|
|
document.addEventListener('dblclick', preventZoom, { passive: false }); |
|
|
|
return () => { |
|
document.removeEventListener('dblclick', preventZoom); |
|
document.body.classList.remove('mobile-optimized-animation'); |
|
}; |
|
} else { |
|
|
|
document.body.classList.remove('mobile-optimized-animation'); |
|
} |
|
}, [isMobile]); |
|
|
|
const toggleSidebar = () => { |
|
setIsSidebarCollapsed(!isSidebarCollapsed); |
|
}; |
|
|
|
const toggleMobileMenu = () => { |
|
setIsMobileMenuOpen(!isMobileMenuOpen); |
|
}; |
|
|
|
|
|
useEffect(() => { |
|
const handleClickOutside = (event) => { |
|
|
|
if (isMobileMenuOpen && |
|
!event.target.closest('#mobile-menu') && |
|
!event.target.closest('.mobile-menu-button')) { |
|
setIsMobileMenuOpen(false); |
|
} |
|
}; |
|
|
|
document.addEventListener('mousedown', handleClickOutside); |
|
return () => { |
|
document.removeEventListener('mousedown', handleClickOutside); |
|
}; |
|
}, [isMobileMenuOpen]); |
|
|
|
|
|
useEffect(() => { |
|
const handleGlobalKeyDown = (e) => { |
|
|
|
if (e.key === 'Escape' && isMobileMenuOpen) { |
|
setIsMobileMenuOpen(false); |
|
} |
|
}; |
|
|
|
document.addEventListener('keydown', handleGlobalKeyDown); |
|
return () => document.removeEventListener('keydown', handleGlobalKeyDown); |
|
}, [isMobileMenuOpen]); |
|
|
|
|
|
useEffect(() => { |
|
const initializeAuth = async () => { |
|
try { |
|
setIsCheckingAuth(true); |
|
|
|
|
|
const cachedResult = await dispatch(checkCachedAuth()); |
|
|
|
|
|
const token = localStorage.getItem('token'); |
|
const cookieAuth = await cookieService.getAuthTokens(); |
|
|
|
if (!cachedResult.payload?.success && (token || cookieAuth?.accessToken)) { |
|
try { |
|
await dispatch(autoLogin()); |
|
} catch (error) { |
|
console.log('Auto login failed, clearing tokens'); |
|
localStorage.removeItem('token'); |
|
await cookieService.clearAuthTokens(); |
|
} |
|
} |
|
} catch (error) { |
|
console.error('Auth initialization failed:', error); |
|
localStorage.removeItem('token'); |
|
await cookieService.clearAuthTokens(); |
|
} finally { |
|
setIsCheckingAuth(false); |
|
} |
|
}; |
|
|
|
|
|
if (isCheckingAuth) { |
|
initializeAuth(); |
|
} |
|
}, []); |
|
|
|
|
|
if (isCheckingAuth) { |
|
return ( |
|
<div className="loading"> |
|
<div className="auth-loading"> |
|
<div className="spinner"></div> |
|
<p>Checking authentication...</p> |
|
</div> |
|
</div> |
|
); |
|
} |
|
|
|
return ( |
|
<ErrorBoundary> |
|
<div className="App min-h-screen bg-gray-50" role="application" aria-label="Lin Application"> |
|
{/* Skip to main content link for accessibility */} |
|
<a |
|
href="#main-content" |
|
className="skip-link sr-only" |
|
onClick={(e) => { |
|
e.preventDefault(); |
|
const mainContent = document.getElementById('main-content'); |
|
if (mainContent) { |
|
mainContent.focus(); |
|
} |
|
}} |
|
> |
|
Skip to main content |
|
</a> |
|
|
|
{/* Mobile menu overlay */} |
|
{isMobile && isMobileMenuOpen && ( |
|
<div |
|
className="fixed inset-0 bg-black bg-opacity-50 z-40 transition-opacity duration-300" |
|
onClick={() => setIsMobileMenuOpen(false)} |
|
aria-label="Close mobile menu" |
|
role="button" |
|
tabIndex={0} |
|
onKeyDown={(e) => { |
|
if (e.key === 'Enter' || e.key === ' ') { |
|
setIsMobileMenuOpen(false); |
|
} |
|
}} |
|
></div> |
|
)} |
|
|
|
{/* Full-width layout without header/sidebar for Home/Auth/Callback */} |
|
{(!isAuthenticated || isAuthRoute || isCallbackRoute || location.pathname === '/') ? ( |
|
<div className="content" id="main-content" tabIndex={-1}> |
|
<Routes> |
|
<Route path="/" element={ |
|
<Suspense fallback={<div className="mobile-loading-optimized">Loading...</div>}> |
|
<Home /> |
|
</Suspense> |
|
} /> |
|
<Route path="/login" element={<Login />} /> |
|
<Route path="/register" element={<Register />} /> |
|
<Route path="/linkedin/callback" element={<LinkedInCallbackHandler />} /> |
|
</Routes> |
|
</div> |
|
) : ( |
|
<> |
|
{/* App layout with header + sidebar for authenticated app pages */} |
|
<Header |
|
onMenuToggle={toggleMobileMenu} |
|
isMenuOpen={isMobileMenuOpen} |
|
isMobile={isMobile} |
|
/> |
|
|
|
{/* Mobile sidebar overlay */} |
|
{isMobile && !isSidebarCollapsed && ( |
|
<div |
|
className="fixed inset-0 bg-black bg-opacity-50 z-40 transition-opacity duration-300" |
|
onClick={() => setIsSidebarCollapsed(true)} |
|
aria-label="Close sidebar" |
|
role="button" |
|
tabIndex={0} |
|
onKeyDown={(e) => { |
|
if (e.key === 'Enter' || e.key === ' ') { |
|
setIsSidebarCollapsed(true); |
|
} |
|
}} |
|
></div> |
|
)} |
|
|
|
<div className={`main-container transition-all duration-300 ease-in-out ${isSidebarCollapsed ? 'sidebar-collapsed' : ''}`} role="main"> |
|
<Sidebar |
|
isCollapsed={isSidebarCollapsed} |
|
toggleSidebar={toggleSidebar} |
|
isMobile={isMobile} |
|
/> |
|
<div className="content flex-1 transition-all duration-300 mobile-render-optimized p-4 sm:p-6 overflow-y-auto" id="main-content" tabIndex={-1}> |
|
<Suspense fallback={<div className="mobile-loading-optimized">Loading...</div>}> |
|
<Routes> |
|
<Route path="/dashboard" element={<Dashboard />} /> |
|
<Route path="/sources" element={<Sources />} /> |
|
<Route path="/accounts" element={<Accounts />} /> |
|
<Route path="/posts" element={<Posts />} /> |
|
<Route path="/schedule" element={<Schedule />} /> |
|
</Routes> |
|
</Suspense> |
|
</div> |
|
</div> |
|
</> |
|
)} |
|
</div> |
|
</ErrorBoundary> |
|
); |
|
} |
|
|
|
export default App; |
|
|