import React, { useState, useEffect, useRef } from 'react'; import { Link, useLocation } from 'react-router-dom'; // We keep heroicons for fallback, but nav uses custom SVGs from /public/icons import { PowerIcon, HomeIcon, AcademicCapIcon, BookOpenIcon, HandThumbUpIcon, WrenchScrewdriverIcon, ChatBubbleLeftRightIcon, Cog6ToothIcon } from '@heroicons/react/24/outline'; import HitokotoBar from './HitokotoBar'; import { api } from '../services/api'; interface User { name: string; email: string; role: string; } const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => { const location = useLocation(); const [viewMode, setViewMode] = useState<'admin' | 'student' | 'auto'>(() => { try { const saved = localStorage.getItem('viewMode') as any; return saved === 'admin' || saved === 'student' ? saved : 'auto'; } catch { return 'auto'; } }); const [isTransitioning] = useState(false); const userData = localStorage.getItem('user'); const user: User | null = userData ? JSON.parse(userData) : null; const [unreadCount, setUnreadCount] = useState(0); // Lightweight online presence: send heartbeat periodically useEffect(() => { let timer: any; const sendHeartbeat = async () => { try { if (!user?.email) return; const token = localStorage.getItem('token') || ''; const base = (((api.defaults as any)?.baseURL as string) || '').replace(/\/$/, ''); await fetch(`${base}/api/auth/online/heartbeat`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'user-role': user.role || 'visitor', 'user-info': userData || '' }, body: JSON.stringify({ email: user.email, path: location.pathname }) }); } catch {} }; sendHeartbeat(); timer = setInterval(sendHeartbeat, 60000); return () => { if (timer) clearInterval(timer); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [user?.email]); // Send a heartbeat on route changes for fresher session tracking useEffect(() => { const run = async () => { try { if (!user?.email) return; const token = localStorage.getItem('token') || ''; const base = (((api.defaults as any)?.baseURL as string) || '').replace(/\/$/, ''); await fetch(`${base}/api/auth/online/heartbeat`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'user-role': user.role || 'visitor', 'user-info': userData || '' }, body: JSON.stringify({ email: user.email, path: location.pathname }) }); } catch {} }; run(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [location.pathname]); // React to external view mode changes (from Manage toggle) useEffect(() => { const handler = (e: any) => { const mode = e?.detail; if (mode === 'admin' || mode === 'student') { setViewMode(mode); } }; window.addEventListener('view-mode-change', handler as any); return () => window.removeEventListener('view-mode-change', handler as any); }, []); // Admin unread message badge (non-invasive) useEffect(() => { let timer: any; const run = async () => { try { if (user?.role !== 'admin') return; const token = localStorage.getItem('token') || ''; const base = (((api.defaults as any)?.baseURL as string) || '').replace(/\/$/, ''); const resp = await fetch(`${base}/api/messages/unread-count`, { headers: { 'Authorization': `Bearer ${token}`, 'user-role': 'admin', 'user-info': userData || '' } }); if (resp.ok) { const data = await resp.json(); if (typeof data?.count === 'number') setUnreadCount(data.count); } } catch {} }; run(); if (user?.role === 'admin') { timer = setInterval(run, 60000); } return () => { if (timer) clearInterval(timer); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [user?.role]); // Admin unread message badge (non-invasive) useEffect(() => { let timer: any; const run = async () => { try { if (user?.role !== 'admin') return; const token = localStorage.getItem('token') || ''; const base = (((api.defaults as any)?.baseURL as string) || '').replace(/\/$/, ''); const resp = await fetch(`${base}/api/messages/unread-count`, { headers: { 'Authorization': `Bearer ${token}`, 'user-role': 'admin', 'user-info': userData || '' } }); if (resp.ok) { const data = await resp.json(); if (typeof data?.count === 'number') setUnreadCount(data.count); } } catch {} }; run(); if (user?.role === 'admin') { timer = setInterval(run, 60000); } return () => { if (timer) clearInterval(timer); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [user?.role]); const handleLogout = () => { try { localStorage.removeItem('token'); localStorage.removeItem('user'); } catch {} window.location.href = '/login'; }; // heroicons already imported at top let navigation = [ { name: 'Home', href: '/dashboard', icon: HomeIcon }, { name: 'Tutorial Tasks', href: '/tutorial-tasks', icon: AcademicCapIcon }, { name: 'Weekly Practice', href: '/weekly-practice', icon: BookOpenIcon }, { name: 'Votes', href: '/votes', icon: HandThumbUpIcon }, { name: 'Toolkit', href: '/toolkit', icon: WrenchScrewdriverIcon }, { name: 'Slides', href: '/slides', icon: BookOpenIcon }, { name: 'Feedback', href: '/feedback', icon: ChatBubbleLeftRightIcon }, ]; // Effective role based on viewMode const effectiveRole = (() => { if (viewMode === 'auto') return user?.role; return viewMode === 'student' ? 'student' : 'admin'; })(); // Hide Slides for visitors if (!user || effectiveRole === 'visitor') { navigation = navigation.filter(item => item.name !== 'Slides'); } // Add Manage link for admin users (always keep in nav) if (user?.role === 'admin') { navigation.push({ name: 'Manage', href: '/manage', icon: Cog6ToothIcon }); } const iconSrcFor = (name: string): string => { switch (name) { case 'Home': return '/icons/home.svg'; case 'Tutorial Tasks': return '/icons/tutorial tasks.svg'; case 'Weekly Practice': return '/icons/weekly practice.svg'; case 'Votes': return '/icons/votes.svg'; case 'Toolkit': return '/icons/toolkit.svg'; case 'Slides': return '/icons/slides.svg'; case 'Feedback': return '/icons/feedback.svg'; case 'Manage': return '/icons/manage.svg'; default: return '/icons/home.svg'; } }; return (
{/* Top Bar */}
logo TransHub
{/* Shell: Sidebar + Content */}
{/* Sidebar */} {/* Main Content */}
{!isTransitioning && children}
{/* Transition Loading Indicator */} {isTransitioning && (
Loading...
)} {/* */}
); }; export default Layout;