/**
* Main Application Component with Routing and Authentication
*
* © 2025 Netwerk Digitaal Erfgoed & TextPast. All rights reserved.
*/
import React, { Suspense, useEffect, useState, useCallback } from 'react';
import {
createBrowserRouter,
RouterProvider,
Navigate,
} from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';
import { LanguageProvider } from './contexts/LanguageContext';
import { UIStateProvider } from './contexts/UIStateContext';
import { ProtectedRoute } from './components/ProtectedRoute';
import { Layout } from './components/layout/Layout';
import { preloadInstitutions } from './hooks/useGeoApiInstitutions';
import {
lazyWithRetry,
RouterErrorBoundary,
LazyLoadErrorBoundary
} from './components/common/LazyLoadError';
import { startVersionCheck } from './lib/version-check';
import { RefreshCw } from 'lucide-react';
// Eagerly loaded pages (small, frequently accessed)
import { LoginPage } from './pages/LoginPage';
import { Settings } from './pages/Settings';
import ProjectPlanPage from './pages/ProjectPlanPage';
// Lazy loaded pages with automatic retry on chunk load failures
// These use lazyWithRetry() to handle deployment-related chunk errors gracefully
// Map page - imports maplibre-gl (~1MB)
const NDEMapPage = lazyWithRetry(() => import('./pages/NDEMapPageMapLibre'));
// Visualize page - imports mermaid, d3, elkjs (~1.5MB combined)
const Visualize = lazyWithRetry(() => import('./pages/Visualize').then(m => ({ default: m.Visualize })));
// LinkML viewer - large schema parsing
const LinkMLViewerPage = lazyWithRetry(() => import('./pages/LinkMLViewerPage'));
// Ontology viewer - imports visualization libraries
const OntologyViewerPage = lazyWithRetry(() => import('./pages/OntologyViewerPage'));
// Data mappings page - custodian data mappings explorer
const DataMapPage = lazyWithRetry(() => import('./pages/DataMapPage'));
// Query builder - medium complexity
const QueryBuilderPage = lazyWithRetry(() => import('./pages/QueryBuilderPage'));
// Database page
const Database = lazyWithRetry(() => import('./pages/Database').then(m => ({ default: m.Database })));
// Stats page
const NDEStatsPage = lazyWithRetry(() => import('./pages/NDEStatsPage'));
// Conversation page
const ConversationPage = lazyWithRetry(() => import('./pages/ConversationPage'));
// Institution browser page
const InstitutionBrowserPage = lazyWithRetry(() => import('./pages/InstitutionBrowserPage'));
import './App.css';
// Loading fallback component for lazy-loaded pages
const PageLoader = () => (
);
// Wrap lazy components with Suspense and error boundary for chunk load errors
const withSuspense = (Component: React.LazyExoticComponent) => (
}>
);
// Create router configuration with protected and public routes
// Documentation routes (linkml, datamap, ontology, browse) are PUBLIC for embedding
// Other pages require authentication via ProtectedRoute
const router = createBrowserRouter([
{
path: '/login',
element: ,
errorElement: ,
},
// ========================================================================
// PUBLIC ROUTES - No authentication required
// These can be embedded in iframes from other domains (e.g., archief.support)
// ========================================================================
{
path: '/linkml',
element: ,
errorElement: ,
children: [
{
index: true,
element: withSuspense(LinkMLViewerPage),
},
],
},
{
path: '/datamap',
element: ,
errorElement: ,
children: [
{
index: true,
element: withSuspense(DataMapPage),
},
],
},
{
path: '/ontology',
element: ,
errorElement: ,
children: [
{
index: true,
element: withSuspense(OntologyViewerPage),
},
],
},
{
path: '/browse',
element: ,
errorElement: ,
children: [
{
index: true,
element: withSuspense(InstitutionBrowserPage),
},
],
},
// ========================================================================
// PROTECTED ROUTES - Authentication required
// ========================================================================
{
path: '/',
element: (
),
errorElement: ,
children: [
{
// Home page redirects to LinkML viewer
index: true,
element: ,
},
{
path: 'visualize',
element: withSuspense(Visualize),
},
{
path: 'database',
element: withSuspense(Database),
},
{
path: 'settings',
element: ,
},
{
// Roadmap page (formerly Home/Project Plan)
path: 'roadmap',
element: ,
},
{
path: 'query-builder',
element: withSuspense(QueryBuilderPage),
},
{
// Redirect old UML viewer route to unified visualize page
path: 'uml-viewer',
element: ,
},
{
path: 'map',
element: withSuspense(NDEMapPage),
},
{
path: 'stats',
element: withSuspense(NDEStatsPage),
},
{
path: 'conversation',
element: withSuspense(ConversationPage),
},
],
},
]);
function App() {
const [showUpdateBanner, setShowUpdateBanner] = useState(false);
// Preload institution data on app initialization
// This starts the fetch early so data is ready when user navigates to Map or Browse
useEffect(() => {
// Small delay to let the app initialize first
const timer = setTimeout(() => {
preloadInstitutions();
}, 500);
return () => clearTimeout(timer);
}, []);
// Check for new versions periodically
useEffect(() => {
const cleanup = startVersionCheck(() => {
setShowUpdateBanner(true);
});
return cleanup;
}, []);
const handleRefresh = useCallback(() => {
// Clear caches and reload
if ('caches' in window) {
caches.keys().then(names => {
names.forEach(name => caches.delete(name));
});
}
window.location.reload();
}, []);
return (
{/* New version available banner */}
{showUpdateBanner && (
A new version is available.
)}
);
}
export default App;