| 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 ForgotPassword from './pages/ForgotPassword.jsx'; |
| import ResetPassword from './pages/ResetPassword.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(); |
| } |
| }, []); |
|
|
| |
| |
| const isProtectedRoute = !isAuthRoute && !isCallbackRoute && location.pathname !== '/'; |
| const showAuthChecking = isCheckingAuth && isProtectedRoute; |
|
|
| 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 */} |
| {(showAuthChecking || !isAuthenticated || isAuthRoute || isCallbackRoute || location.pathname === '/') ? ( |
| <div className="content" id="main-content" tabIndex={-1}> |
| {showAuthChecking ? ( |
| // Subtle auth checking indicator for protected routes |
| <div className="flex items-center justify-center min-h-screen"> |
| <div className="text-center"> |
| <div className="inline-block animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary-500 mb-2"></div> |
| <p className="text-gray-600 text-sm">Checking authentication...</p> |
| </div> |
| </div> |
| ) : ( |
| <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="/forgot-password" element={<ForgotPassword />} /> |
| <Route path="/reset-password" element={<ResetPassword />} /> |
| <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; |
|
|