Lin / frontend /src /App.jsx
Zelyanoth's picture
fff
25f22bf
raw
history blame
10.8 kB
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';
// Lazy load components for better mobile performance
const LazyFeatureCard = lazy(() => import('./components/FeatureCard'));
const LazyTestimonialCard = lazy(() => import('./components/TestimonialCard'));
// Error Boundary Component
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);
// Auth pages should never render the Sidebar/Header
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);
// Detect mobile devices and set responsive behavior
useEffect(() => {
const checkMobile = () => {
const mobile = window.innerWidth < 1024; // Match sidebar breakpoint
setIsMobile(mobile);
// Auto-collapse sidebar on mobile
if (mobile && !isSidebarCollapsed) {
setIsSidebarCollapsed(true);
}
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, [isSidebarCollapsed]);
// Optimize performance for mobile devices
useEffect(() => {
if (isMobile) {
// Reduce animation complexity on mobile
document.body.classList.add('mobile-optimized-animation');
// Enable hardware acceleration for mobile elements
const mobileElements = document.querySelectorAll('.mobile-accelerate');
mobileElements.forEach(el => {
el.classList.add('mobile-accelerated');
});
// Optimize touch interactions
const touchElements = document.querySelectorAll('.touch-optimized');
touchElements.forEach(el => {
el.classList.add('touch-optimized');
});
// Prevent double-tap zoom on mobile
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 {
// Clean up mobile optimizations
document.body.classList.remove('mobile-optimized-animation');
}
}, [isMobile]);
const toggleSidebar = () => {
setIsSidebarCollapsed(!isSidebarCollapsed);
};
const toggleMobileMenu = () => {
setIsMobileMenuOpen(!isMobileMenuOpen);
};
// Close mobile menu when clicking outside
useEffect(() => {
const handleClickOutside = (event) => {
// Close mobile menu when clicking outside
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]);
// Handle keyboard navigation for mobile menu
useEffect(() => {
const handleGlobalKeyDown = (e) => {
// Close mobile menu with Escape
if (e.key === 'Escape' && isMobileMenuOpen) {
setIsMobileMenuOpen(false);
}
};
document.addEventListener('keydown', handleGlobalKeyDown);
return () => document.removeEventListener('keydown', handleGlobalKeyDown);
}, [isMobileMenuOpen]);
// Simplified authentication check - only run once on mount
useEffect(() => {
const initializeAuth = async () => {
try {
setIsCheckingAuth(true);
// Check for cached authentication first
const cachedResult = await dispatch(checkCachedAuth());
// If cached auth failed but we have a token, try auto login
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);
}
};
// Only run authentication check once on mount
if (isCheckingAuth) {
initializeAuth();
}
}, []); // Empty dependency array to run only once
// Show loading state only while checking auth on initial load
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;