linguabot's picture
Upload src/components/Layout.tsx with huggingface_hub
d0d95fe verified
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<number>(0);
// Handle page transitions
useEffect(() => {
// Only trigger transition if path actually changed
if (location.pathname !== previousPathRef.current) {
setIsTransitioning(true);
previousPathRef.current = location.pathname;
// Reset week selection to Week 1 when navigating between pages
const previousPath = previousPathRef.current;
if (location.pathname === '/tutorial-tasks' &&
previousPath &&
!previousPath.includes('/tutorial-tasks')) {
// Use URL parameter to force Week 1
window.history.replaceState(null, '', '/tutorial-tasks?week=1');
localStorage.setItem('selectedTutorialWeek', '1');
} else if (location.pathname === '/weekly-practice' &&
previousPath &&
!previousPath.includes('/weekly-practice')) {
// Use URL parameter to force Week 1
window.history.replaceState(null, '', '/weekly-practice?week=1');
localStorage.setItem('selectedWeeklyPracticeWeek', '1');
}
// Determine transition duration based on destination page
let transitionDuration = 800; // Default duration
// Longer duration for heavy pages
if (location.pathname === '/tutorial-tasks' || location.pathname === '/weekly-practice') {
transitionDuration = 1200; // Longer for content-heavy pages
}
// Special case: Weekly Practice → Tutorial Tasks (add 500ms delay)
if (location.pathname === '/tutorial-tasks' &&
previousPath &&
previousPath.includes('/weekly-practice')) {
// Check if navigating to Week 2 (use localStorage since URL might not be updated yet)
const tutorialWeek = localStorage.getItem('selectedTutorialWeek');
if (tutorialWeek === '2') {
transitionDuration = 2500; // Extended duration for Week 2 (2000 + 500)
} else {
transitionDuration = 1700; // Standard duration for Week 1 (1200 + 500)
}
}
// End transition after content is loaded (wait for DOM updates)
const timer = setTimeout(() => {
setIsTransitioning(false);
}, transitionDuration);
return () => clearTimeout(timer);
}
}, [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]);
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 (
<div className="min-h-screen bg-gray-50">
{/* Navigation */}
<nav className="bg-white shadow-sm border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex">
<div className="flex-shrink-0 flex items-center">
<Link to="/dashboard" className="text-xl font-bold text-indigo-600">
Transcreation
</Link>
</div>
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
{navigation.map((item) => {
const isActive = location.pathname === item.href;
return (
<Link
key={item.name}
to={item.href}
className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium transition-all duration-200 ease-in-out ${
isActive
? 'border-indigo-500 text-gray-900'
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'
}`}
>
<item.icon className="h-4 w-4 mr-1" />
{item.name}
{item.name === 'Feedback' && user?.role === 'admin' && unreadCount > 0 && (
<span className="ml-2 inline-flex items-center justify-center min-w-[16px] h-4 px-1 rounded-full bg-red-600 text-white text-[10px] leading-none">{unreadCount > 99 ? '99+' : unreadCount}</span>
)}
</Link>
);
})}
</div>
</div>
<div className="flex items-center">
{user && (
<div className="flex items-center space-x-4">
<span className="text-sm text-gray-700">
Welcome, {(user as any).displayName || user.name}
</span>
<button
onClick={handleLogout}
className="text-gray-500 hover:text-gray-700 flex items-center"
>
<ArrowRightOnRectangleIcon className="h-4 w-4 mr-1" />
Logout
</button>
</div>
)}
</div>
</div>
</div>
</nav>
{/* Mobile Navigation */}
<div className="sm:hidden">
<div className="pt-2 pb-3 space-y-1">
{navigation.map((item) => {
const isActive = location.pathname === item.href;
return (
<Link
key={item.name}
to={item.href}
className={`block pl-3 pr-4 py-2 border-l-4 text-base font-medium transition-all duration-200 ease-in-out ${
isActive
? 'bg-indigo-50 border-indigo-500 text-indigo-700'
: 'border-transparent text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800'
}`}
>
<div className="flex items-center">
<item.icon className="h-4 w-4 mr-2" />
{item.name}
</div>
</Link>
);
})}
</div>
</div>
{/* Main Content */}
<main>
{!isTransitioning && children}
</main>
{/* Transition Loading Indicator */}
{isTransitioning && (
<div className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50">
<div className="bg-white rounded-lg shadow-lg p-4 flex items-center space-x-3">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-indigo-600"></div>
<span className="text-gray-700 font-medium">Loading...</span>
</div>
</div>
)}
<HitokotoBar />
</div>
);
};
export default Layout;