import React, { useState, useEffect, useRef } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { HomeIcon, AcademicCapIcon, BookOpenIcon, HandThumbUpIcon, WrenchScrewdriverIcon, UserIcon, ArrowRightOnRectangleIcon } 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 [isTransitioning, setIsTransitioning] = useState(false); const previousPathRef = useRef(location.pathname); 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]); // 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 = () => { localStorage.removeItem('token'); localStorage.removeItem('user'); window.location.href = '/'; }; 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: UserIcon }, ]; // Hide Slides for visitors if (!user || user.role === 'visitor') { navigation = navigation.filter(item => item.name !== 'Slides'); } // Add Manage link for admin users if (user?.role === 'admin') { navigation.push({ name: 'Manage', href: '/manage', icon: UserIcon }); } return (
{/* Navigation */} {/* Mobile Navigation */}
{navigation.map((item) => { const isActive = location.pathname === item.href; return (
{item.name}
); })}
{/* Main Content */}
{!isTransitioning && children}
{/* Transition Loading Indicator */} {isTransitioning && (
Loading...
)}
); }; export default Layout;