/** * 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 = () => (

Loading...

); // 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;