494 lines
17 KiB
TypeScript
494 lines
17 KiB
TypeScript
import { useState, useRef, useEffect } from 'react'
|
|
import { BrowserRouter, Routes, Route, Link, useLocation } from 'react-router-dom'
|
|
import { Box, Container, Typography, Button } from '@mui/material'
|
|
import LogoutIcon from '@mui/icons-material/Logout'
|
|
import LockIcon from '@mui/icons-material/Lock'
|
|
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'
|
|
import ChatPage from './pages/ChatPage'
|
|
import MapPage from './pages/MapPage'
|
|
import BrowsePage from './pages/BrowsePage'
|
|
import StatsPage from './pages/StatsPage'
|
|
import OntologyPage from './pages/OntologyPage'
|
|
import RulesPage from './pages/RulesPage'
|
|
import LoginPage from './pages/LoginPage'
|
|
import ChangePasswordDialog from './components/ChangePasswordDialog'
|
|
import { AuthProvider, useAuth } from './context/AuthContext'
|
|
|
|
// NA Color palette
|
|
const naColors = {
|
|
primary: '#007bc7',
|
|
red: '#d52b1e',
|
|
orange: '#e17000',
|
|
green: '#39870c',
|
|
cream: '#f7f5f3',
|
|
darkBlue: '#154273',
|
|
}
|
|
|
|
// Navigation items
|
|
const navItems = [
|
|
{ label: 'Chat', path: '/' },
|
|
{ label: 'Kaart', path: '/map' },
|
|
{ label: 'Verkennen', path: '/browse' },
|
|
{ label: 'Statistieken', path: '/stats' },
|
|
{ label: 'Ontologie', path: '/ontology' },
|
|
{ label: 'Regels', path: '/rules' },
|
|
]
|
|
|
|
function NavBar() {
|
|
const location = useLocation()
|
|
const { logout, user } = useAuth()
|
|
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false)
|
|
|
|
return (
|
|
<>
|
|
<Box sx={{ bgcolor: naColors.primary }}>
|
|
<Container maxWidth="lg">
|
|
<Box sx={{ display: 'flex', gap: 0, justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<Box sx={{ display: 'flex', gap: 0 }}>
|
|
{navItems.map((item) => {
|
|
const isActive = location.pathname === item.path ||
|
|
(item.path === '/' && location.pathname === '')
|
|
|
|
return (
|
|
<Link key={item.path} to={item.path} style={{ textDecoration: 'none' }}>
|
|
<Box
|
|
sx={{
|
|
px: 3,
|
|
py: 1.5,
|
|
color: '#fff',
|
|
fontWeight: 600,
|
|
fontSize: '1rem',
|
|
bgcolor: isActive ? 'rgba(255,255,255,0.15)' : 'transparent',
|
|
transition: 'background-color 0.2s',
|
|
'&:hover': {
|
|
bgcolor: 'rgba(255,255,255,0.1)',
|
|
},
|
|
}}
|
|
>
|
|
{item.label}
|
|
</Box>
|
|
</Link>
|
|
)
|
|
})}
|
|
</Box>
|
|
|
|
{/* User info and actions */}
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
{user && (
|
|
<Typography sx={{ color: '#fff', fontSize: '0.875rem', opacity: 0.9, mr: 1 }}>
|
|
{user.email}
|
|
</Typography>
|
|
)}
|
|
<Button
|
|
onClick={() => setPasswordDialogOpen(true)}
|
|
startIcon={<LockIcon />}
|
|
sx={{
|
|
color: '#fff',
|
|
fontSize: '0.875rem',
|
|
textTransform: 'none',
|
|
opacity: 0.9,
|
|
'&:hover': {
|
|
opacity: 1,
|
|
bgcolor: 'rgba(255,255,255,0.1)',
|
|
},
|
|
}}
|
|
>
|
|
Wachtwoord
|
|
</Button>
|
|
<Button
|
|
onClick={logout}
|
|
startIcon={<LogoutIcon />}
|
|
sx={{
|
|
color: '#fff',
|
|
fontSize: '0.875rem',
|
|
textTransform: 'none',
|
|
opacity: 0.9,
|
|
'&:hover': {
|
|
opacity: 1,
|
|
bgcolor: 'rgba(255,255,255,0.1)',
|
|
},
|
|
}}
|
|
>
|
|
Uitloggen
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
</Container>
|
|
</Box>
|
|
|
|
<ChangePasswordDialog
|
|
open={passwordDialogOpen}
|
|
onClose={() => setPasswordDialogOpen(false)}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
function AppContent() {
|
|
const { isAuthenticated, isLoading } = useAuth()
|
|
const [footerOpen, setFooterOpen] = useState(false)
|
|
const footerRef = useRef<HTMLDivElement>(null)
|
|
const [footerHeight, setFooterHeight] = useState(0)
|
|
|
|
// Header collapse state
|
|
const [isCollapsed, setIsCollapsed] = useState(false)
|
|
|
|
// Helper to set collapsed state
|
|
const setCollapsedWithTransition = (collapsed: boolean) => {
|
|
if (collapsed === isCollapsed) return
|
|
setIsCollapsed(collapsed)
|
|
}
|
|
|
|
// Measure footer height when it opens
|
|
useEffect(() => {
|
|
if (footerOpen && footerRef.current) {
|
|
setFooterHeight(footerRef.current.offsetHeight)
|
|
}
|
|
}, [footerOpen])
|
|
|
|
// Scroll/wheel event listeners for header collapse
|
|
useEffect(() => {
|
|
let lastScrollY = 0
|
|
let ticking = false
|
|
|
|
// Very low threshold - collapse almost immediately on scroll down
|
|
const WHEEL_THRESHOLD = 15
|
|
const SCROLL_UP_THRESHOLD = -30 // Need to scroll up a bit to restore
|
|
|
|
const handleWheel = (e: WheelEvent) => {
|
|
// Don't collapse when interacting with footer toggle
|
|
if (e.target instanceof Element && e.target.closest('.footer-toggle-btn')) return
|
|
|
|
if (e.deltaY > WHEEL_THRESHOLD) {
|
|
// Scrolling down - collapse header
|
|
setCollapsedWithTransition(true)
|
|
} else if (e.deltaY < SCROLL_UP_THRESHOLD) {
|
|
// Scrolling up significantly - restore header
|
|
setCollapsedWithTransition(false)
|
|
}
|
|
}
|
|
|
|
// Also handle touch scrolling on mobile
|
|
const handleTouchMove = () => {
|
|
if (ticking) return
|
|
ticking = true
|
|
|
|
requestAnimationFrame(() => {
|
|
const currentScrollY = window.scrollY
|
|
if (currentScrollY > lastScrollY + 10) {
|
|
setCollapsedWithTransition(true)
|
|
} else if (currentScrollY < lastScrollY - 30) {
|
|
setCollapsedWithTransition(false)
|
|
}
|
|
lastScrollY = currentScrollY
|
|
ticking = false
|
|
})
|
|
}
|
|
|
|
document.addEventListener('wheel', handleWheel, { passive: true })
|
|
document.addEventListener('touchmove', handleTouchMove, { passive: true })
|
|
|
|
return () => {
|
|
document.removeEventListener('wheel', handleWheel)
|
|
document.removeEventListener('touchmove', handleTouchMove)
|
|
}
|
|
}, [isCollapsed])
|
|
|
|
// Show loading state while checking auth
|
|
if (isLoading) {
|
|
return (
|
|
<Box
|
|
sx={{
|
|
minHeight: '100vh',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
bgcolor: naColors.cream,
|
|
}}
|
|
>
|
|
<Box sx={{ textAlign: 'center' }}>
|
|
<img
|
|
src="/de-aa-logo.svg"
|
|
alt="de Aa"
|
|
style={{ height: 64, width: 64, marginBottom: 16 }}
|
|
/>
|
|
<Typography sx={{ color: naColors.primary }}>Laden...</Typography>
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
// Show login if not authenticated
|
|
if (!isAuthenticated) {
|
|
return <LoginPage />
|
|
}
|
|
|
|
// Show main app if authenticated
|
|
return (
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: '100vh', bgcolor: naColors.cream }}>
|
|
|
|
{/* Small floating logo - appears when header is collapsed */}
|
|
<button
|
|
className={`header-logo-small ${isCollapsed ? 'visible' : ''}`}
|
|
onClick={() => setIsCollapsed(false)}
|
|
aria-label="Toon header"
|
|
>
|
|
<img
|
|
src="/de-aa-logo.svg"
|
|
alt="de Aa"
|
|
className="header-logo-small-img"
|
|
/>
|
|
</button>
|
|
|
|
{/* Collapsible Header Section */}
|
|
<div className={`header-collapsible ${isCollapsed ? 'header-collapsed' : ''}`}>
|
|
{/* Top utility bar */}
|
|
<Box sx={{
|
|
bgcolor: '#fff',
|
|
borderBottom: '1px solid #e0e0e0',
|
|
py: 0.5,
|
|
}}>
|
|
<Container maxWidth="lg">
|
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 3 }}>
|
|
<Typography
|
|
component="a"
|
|
href="https://www.nationaalarchief.nl/over-het-na"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
sx={{
|
|
fontSize: '0.875rem',
|
|
color: naColors.primary,
|
|
textDecoration: 'none',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 0.5,
|
|
'&:hover': { textDecoration: 'underline' }
|
|
}}
|
|
>
|
|
Over het NA
|
|
</Typography>
|
|
<Typography
|
|
component="a"
|
|
href="https://www.nationaalarchief.nl/contact"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
sx={{
|
|
fontSize: '0.875rem',
|
|
color: naColors.primary,
|
|
textDecoration: 'none',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 0.5,
|
|
'&:hover': { textDecoration: 'underline' }
|
|
}}
|
|
>
|
|
Contact
|
|
</Typography>
|
|
<Typography
|
|
component="a"
|
|
href="https://www.nationaalarchief.nl/onderzoeken/zoeken"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
sx={{
|
|
fontSize: '0.875rem',
|
|
color: naColors.primary,
|
|
textDecoration: 'none',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 0.5,
|
|
'&:hover': { textDecoration: 'underline' }
|
|
}}
|
|
>
|
|
Zoeken in NA
|
|
</Typography>
|
|
</Box>
|
|
</Container>
|
|
</Box>
|
|
|
|
{/* Main Header with Logo */}
|
|
<Box sx={{ bgcolor: '#fff', py: 2, borderBottom: '1px solid #e0e0e0' }}>
|
|
<Container maxWidth="lg">
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
{/* Site Title with de Aa logo */}
|
|
<Link to="/" style={{ textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
<img
|
|
src="/de-aa-logo.svg"
|
|
alt="de Aa"
|
|
style={{ height: 48, width: 48, objectFit: 'contain' }}
|
|
/>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
|
<Typography
|
|
variant="h4"
|
|
sx={{
|
|
fontFamily: 'Georgia, serif',
|
|
fontStyle: 'italic',
|
|
fontWeight: 400,
|
|
color: naColors.primary,
|
|
lineHeight: 1.1,
|
|
}}
|
|
>
|
|
de Aa
|
|
</Typography>
|
|
<Typography
|
|
sx={{
|
|
fontFamily: 'Georgia, serif',
|
|
fontStyle: 'italic',
|
|
fontWeight: 400,
|
|
fontSize: '0.75rem',
|
|
color: naColors.primary,
|
|
opacity: 0.6,
|
|
}}
|
|
>
|
|
Archiefassistent
|
|
</Typography>
|
|
</Box>
|
|
</Link>
|
|
|
|
{/* NA Logo */}
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
<Typography sx={{ fontSize: '0.75rem', color: '#666' }}>
|
|
Een dienst van het
|
|
</Typography>
|
|
<a
|
|
href="https://www.nationaalarchief.nl"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
style={{ display: 'flex', alignItems: 'center' }}
|
|
>
|
|
<img
|
|
src="/na-logo.svg"
|
|
alt="Nationaal Archief"
|
|
style={{ height: 40 }}
|
|
/>
|
|
</a>
|
|
</Box>
|
|
</Box>
|
|
</Container>
|
|
</Box>
|
|
|
|
{/* Navigation Bar */}
|
|
<NavBar />
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<Box component="main" sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
|
<Routes>
|
|
<Route path="/" element={<ChatPage />} />
|
|
<Route path="/map" element={<MapPage />} />
|
|
<Route path="/browse" element={<BrowsePage />} />
|
|
<Route path="/stats" element={<StatsPage />} />
|
|
<Route path="/ontology" element={<OntologyPage />} />
|
|
<Route path="/rules" element={<RulesPage />} />
|
|
<Route path="*" element={<ChatPage />} />
|
|
</Routes>
|
|
</Box>
|
|
|
|
{/* Footer Toggle Button - Always visible at bottom center */}
|
|
<button
|
|
className={`footer-toggle-btn ${footerOpen ? 'footer-open' : ''}`}
|
|
onClick={() => setFooterOpen(!footerOpen)}
|
|
aria-label={footerOpen ? 'Verberg footer' : 'Toon footer'}
|
|
style={footerOpen ? { bottom: `${footerHeight}px` } : undefined}
|
|
>
|
|
<KeyboardArrowUpIcon />
|
|
</button>
|
|
|
|
{/* Toggle Footer - Hidden by default, shown on button click */}
|
|
<div
|
|
ref={footerRef}
|
|
className={`toggle-footer ${footerOpen ? 'footer-visible' : ''}`}
|
|
>
|
|
<Container maxWidth="lg" sx={{ py: 4 }}>
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: 4 }}>
|
|
{/* Column 1 */}
|
|
<Box>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 2 }}>
|
|
<img
|
|
src="/de-aa-logo.svg"
|
|
alt="de Aa"
|
|
style={{ height: 32, width: 32, objectFit: 'contain' }}
|
|
/>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
|
<Typography variant="h6" sx={{ fontFamily: 'Georgia, serif', fontStyle: 'italic', lineHeight: 1.1 }}>
|
|
de Aa
|
|
</Typography>
|
|
<Typography sx={{ fontFamily: 'Georgia, serif', fontStyle: 'italic', fontSize: '0.6rem', opacity: 0.6 }}>
|
|
Archiefassistent
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
<Typography variant="body2" sx={{ opacity: 0.8, maxWidth: 300 }}>
|
|
Uw digitale helper voor archiefonderzoek en erfgoedvragen,
|
|
aangedreven door het Nationaal Archief.
|
|
</Typography>
|
|
</Box>
|
|
|
|
{/* Column 2 */}
|
|
<Box>
|
|
<Typography variant="subtitle1" sx={{ mb: 2, fontWeight: 600 }}>
|
|
Contact
|
|
</Typography>
|
|
<Typography variant="body2" sx={{ opacity: 0.8 }}>
|
|
Nationaal Archief<br />
|
|
Prins Willem-Alexanderhof 20<br />
|
|
2595 BE Den Haag<br />
|
|
<Box component="a" href="tel:+31703315400" sx={{ color: '#fff', opacity: 0.8 }}>
|
|
070 - 331 54 00
|
|
</Box>
|
|
</Typography>
|
|
</Box>
|
|
|
|
{/* Column 3 */}
|
|
<Box>
|
|
<Typography variant="subtitle1" sx={{ mb: 2, fontWeight: 600 }}>
|
|
Links
|
|
</Typography>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
|
{[
|
|
{ label: 'nationaalarchief.nl', url: 'https://www.nationaalarchief.nl' },
|
|
{ label: 'Archieven.nl', url: 'https://www.archieven.nl' },
|
|
{ label: 'Gahetna.nl', url: 'https://www.gahetna.nl' },
|
|
].map((link) => (
|
|
<Typography
|
|
key={link.label}
|
|
component="a"
|
|
href={link.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
sx={{
|
|
color: '#fff',
|
|
opacity: 0.8,
|
|
textDecoration: 'none',
|
|
'&:hover': { opacity: 1, textDecoration: 'underline' }
|
|
}}
|
|
>
|
|
{link.label}
|
|
</Typography>
|
|
))}
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Copyright */}
|
|
<Box sx={{ mt: 4, pt: 2, borderTop: '1px solid rgba(255,255,255,0.2)' }}>
|
|
<Typography variant="body2" sx={{ opacity: 0.6 }}>
|
|
© {new Date().getFullYear()} Nationaal Archief. Alle rechten voorbehouden.
|
|
</Typography>
|
|
</Box>
|
|
</Container>
|
|
</div>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
function App() {
|
|
return (
|
|
<AuthProvider>
|
|
<BrowserRouter>
|
|
<AppContent />
|
|
</BrowserRouter>
|
|
</AuthProvider>
|
|
)
|
|
}
|
|
|
|
export default App
|