glam/apps/archief-assistent/src/App.tsx

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 }}>
&copy; {new Date().getFullYear()} Nationaal Archief. Alle rechten voorbehouden.
</Typography>
</Box>
</Container>
</div>
</Box>
)
}
function App() {
return (
<AuthProvider>
<BrowserRouter>
<AppContent />
</BrowserRouter>
</AuthProvider>
)
}
export default App