glam/frontend/src/App.tsx
2025-12-30 03:43:31 +01:00

254 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 = () => (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600 dark:text-gray-400">Loading...</p>
</div>
</div>
);
// Wrap lazy components with Suspense and error boundary for chunk load errors
const withSuspense = (Component: React.LazyExoticComponent<React.ComponentType>) => (
<LazyLoadErrorBoundary>
<Suspense fallback={<PageLoader />}>
<Component />
</Suspense>
</LazyLoadErrorBoundary>
);
// 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: <LoginPage />,
errorElement: <RouterErrorBoundary />,
},
// ========================================================================
// PUBLIC ROUTES - No authentication required
// These can be embedded in iframes from other domains (e.g., archief.support)
// ========================================================================
{
path: '/linkml',
element: <Layout />,
errorElement: <RouterErrorBoundary />,
children: [
{
index: true,
element: withSuspense(LinkMLViewerPage),
},
],
},
{
path: '/datamap',
element: <Layout />,
errorElement: <RouterErrorBoundary />,
children: [
{
index: true,
element: withSuspense(DataMapPage),
},
],
},
{
path: '/ontology',
element: <Layout />,
errorElement: <RouterErrorBoundary />,
children: [
{
index: true,
element: withSuspense(OntologyViewerPage),
},
],
},
{
path: '/browse',
element: <Layout />,
errorElement: <RouterErrorBoundary />,
children: [
{
index: true,
element: withSuspense(InstitutionBrowserPage),
},
],
},
// ========================================================================
// PROTECTED ROUTES - Authentication required
// ========================================================================
{
path: '/',
element: (
<ProtectedRoute>
<Layout />
</ProtectedRoute>
),
errorElement: <RouterErrorBoundary />,
children: [
{
// Home page redirects to LinkML viewer
index: true,
element: <Navigate to="/linkml" replace />,
},
{
path: 'visualize',
element: withSuspense(Visualize),
},
{
path: 'database',
element: withSuspense(Database),
},
{
path: 'settings',
element: <Settings />,
},
{
// Roadmap page (formerly Home/Project Plan)
path: 'roadmap',
element: <ProjectPlanPage />,
},
{
path: 'query-builder',
element: withSuspense(QueryBuilderPage),
},
{
// Redirect old UML viewer route to unified visualize page
path: 'uml-viewer',
element: <Navigate to="/visualize" replace />,
},
{
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 (
<LanguageProvider>
<AuthProvider>
<UIStateProvider>
{/* New version available banner */}
{showUpdateBanner && (
<div className="fixed top-0 left-0 right-0 z-[9999] bg-blue-600 text-white px-4 py-2 text-center text-sm flex items-center justify-center gap-3">
<span>A new version is available.</span>
<button
onClick={handleRefresh}
className="inline-flex items-center gap-1.5 px-3 py-1 bg-white text-blue-600 rounded font-medium hover:bg-blue-50 transition-colors"
>
<RefreshCw className="w-3.5 h-3.5" />
Refresh
</button>
<button
onClick={() => setShowUpdateBanner(false)}
className="text-white/80 hover:text-white ml-2"
aria-label="Dismiss"
>
×
</button>
</div>
)}
<RouterProvider router={router} />
</UIStateProvider>
</AuthProvider>
</LanguageProvider>
);
}
export default App;