# Frontend Architecture Design ## System Overview The Heritage Custodian Frontend follows a layered architecture pattern with clear separation of concerns, building upon proven patterns from the example_ld project while extending them for heritage institution data visualization needs. ## Architecture Layers ### 1. Presentation Layer ``` ┌─────────────────────────────────────────────┐ │ React Components │ ├─────────────────────────────────────────────┤ │ Pages │ Layouts │ Features │ Common │ ├─────────────────────────────────────────────┤ │ Component Library (UI Kit) │ └─────────────────────────────────────────────┘ ``` **Components Structure:** ``` src/ ├── components/ │ ├── common/ │ │ ├── Header.tsx │ │ ├── Footer.tsx │ │ ├── Navigation.tsx │ │ └── LoadingSpinner.tsx │ ├── rdf/ │ │ ├── RDFViewer.tsx │ │ ├── TripleTable.tsx │ │ ├── GraphExplorer.tsx │ │ └── FormatSelector.tsx │ ├── visualization/ │ │ ├── NetworkGraph.tsx │ │ ├── Timeline.tsx │ │ ├── GeoMap.tsx │ │ └── UMLDiagram.tsx │ ├── query/ │ │ ├── SPARQLEditor.tsx │ │ ├── QueryBuilder.tsx │ │ ├── ResultsView.tsx │ │ └── SavedQueries.tsx │ └── custodian/ │ ├── CustodianCard.tsx │ ├── CustodianDetail.tsx │ ├── CustodianList.tsx │ └── CustodianSearch.tsx ``` ### 2. State Management Layer ``` ┌─────────────────────────────────────────────┐ │ Zustand Stores │ ├─────────────────────────────────────────────┤ │ RDF Store │ Query Store │ UI Store │ ├─────────────────────────────────────────────┤ │ React Query (Data Fetching) │ └─────────────────────────────────────────────┘ ``` **Store Architecture:** ```typescript // stores/rdfStore.ts interface RDFStore { triples: Triple[]; graphs: Graph[]; currentFormat: RDFFormat; loadRDF: (url: string, format: RDFFormat) => Promise; parseRDF: (content: string, format: RDFFormat) => void; clearStore: () => void; } // stores/queryStore.ts interface QueryStore { queries: SavedQuery[]; currentQuery: string; results: QueryResult[]; isExecuting: boolean; executeQuery: (sparql: string) => Promise; saveQuery: (query: SavedQuery) => void; } // stores/uiStore.ts interface UIStore { theme: 'light' | 'dark'; sidebarOpen: boolean; activeView: ViewType; notifications: Notification[]; toggleSidebar: () => void; setActiveView: (view: ViewType) => void; } ``` ### 3. Data Processing Layer ``` ┌─────────────────────────────────────────────┐ │ Data Processors │ ├─────────────────────────────────────────────┤ │ RDF Parser │ SPARQL Engine │ Transformer │ ├─────────────────────────────────────────────┤ │ Data Validation & Sanitization │ └─────────────────────────────────────────────┘ ``` **Processing Modules:** ```typescript // lib/rdf/parser.ts export class RDFParser { async parse(content: string, format: RDFFormat): Promise async serialize(triples: Triple[], format: RDFFormat): Promise validateRDF(content: string): ValidationResult } // lib/sparql/engine.ts export class SPARQLEngine { constructor(store: N3.Store) execute(query: string): Promise prepare(query: string): PreparedQuery explain(query: string): QueryPlan } // lib/transform/custodian.ts export class CustodianTransformer { fromRDF(triples: Triple[]): CustodianObservation[] toRDF(custodian: CustodianObservation): Triple[] toGeoJSON(custodians: CustodianObservation[]): FeatureCollection toCSV(custodians: CustodianObservation[]): string } ``` ### 4. Visualization Layer ``` ┌─────────────────────────────────────────────┐ │ D3.js Visualizations │ ├─────────────────────────────────────────────┤ │ Force Graph │ Tree │ Timeline │ Geo Map │ ├─────────────────────────────────────────────┤ │ Visualization Utilities │ └─────────────────────────────────────────────┘ ``` **Visualization Services:** ```typescript // lib/viz/d3/uml.ts export class UMLVisualizer { renderClassDiagram(schema: LinkMLSchema): D3Selection renderRelationships(relations: Relationship[]): D3Selection animateTransition(from: Diagram, to: Diagram): void } // lib/viz/d3/network.ts export class NetworkVisualizer { renderForceGraph(nodes: Node[], edges: Edge[]): D3Selection applyLayout(layout: LayoutType): void enableInteraction(callbacks: InteractionCallbacks): void } // lib/viz/maps/geo.ts export class GeoVisualizer { renderMap(center: [number, number], zoom: number): LeafletMap addCustodianMarkers(custodians: CustodianObservation[]): void createChoropleth(data: GeoStats): void } ``` ### 5. API Communication Layer ``` ┌─────────────────────────────────────────────┐ │ API Clients │ ├─────────────────────────────────────────────┤ │ GraphQL │ REST │ WebSocket │ SPARQL │ ├─────────────────────────────────────────────┤ │ Request/Response Handling │ └─────────────────────────────────────────────┘ ``` **API Services:** ```typescript // lib/api/graphql.ts export class GraphQLClient { async query(query: string, variables?: Variables): Promise async mutation(mutation: string, variables?: Variables): Promise subscribe(subscription: string): Observable } // lib/api/sparql.ts export class SPARQLClient { constructor(endpoint: string) async query(sparql: string): Promise async update(sparql: string): Promise async construct(sparql: string): Promise } // lib/api/websocket.ts export class WebSocketClient { connect(url: string): void on(event: string, handler: EventHandler): void emit(event: string, data: any): void disconnect(): void } ``` ## Data Flow Architecture ### 1. RDF Loading Flow ``` User selects RDF file ↓ FileReader API reads content ↓ RDFParser validates and parses ↓ Triples stored in N3.Store ↓ UI components re-render ↓ Visualizations update ``` ### 2. SPARQL Query Flow ``` User writes SPARQL query ↓ Query syntax validation ↓ SPARQLEngine execution ↓ Results transformation ↓ Visualization selection ↓ D3.js rendering ``` ### 3. UML Diagram Rendering Flow ``` Load Mermaid/PlantUML file ↓ Parse diagram syntax ↓ Extract nodes and relationships ↓ Transform to D3.js data structure ↓ Apply force-directed layout ↓ Render interactive SVG ↓ Enable pan/zoom/click interactions ``` ## Component Communication ### Event-Driven Architecture ```typescript // Event bus for decoupled communication class EventBus { private events: Map = new Map(); on(event: string, handler: EventHandler): void { if (!this.events.has(event)) { this.events.set(event, []); } this.events.get(event)!.push(handler); } emit(event: string, data?: any): void { const handlers = this.events.get(event); handlers?.forEach(handler => handler(data)); } off(event: string, handler: EventHandler): void { const handlers = this.events.get(event); if (handlers) { const index = handlers.indexOf(handler); if (index > -1) { handlers.splice(index, 1); } } } } // Usage example eventBus.on('custodian:selected', (custodian) => { // Update detail view // Highlight on map // Show in timeline }); eventBus.emit('custodian:selected', selectedCustodian); ``` ### Props Drilling Prevention Using React Context for cross-cutting concerns: ```typescript // contexts/RDFContext.tsx export const RDFContext = createContext({ store: new N3.Store(), format: 'turtle', loading: false, }); // contexts/ThemeContext.tsx export const ThemeContext = createContext({ theme: 'light', toggleTheme: () => {}, }); // contexts/AuthContext.tsx export const AuthContext = createContext({ user: null, login: async () => {}, logout: () => {}, }); ``` ## Performance Optimization ### 1. Code Splitting ```typescript // Lazy load heavy components const NetworkGraph = lazy(() => import('./components/visualization/NetworkGraph')); const SPARQLEditor = lazy(() => import('./components/query/SPARQLEditor')); const UMLDiagram = lazy(() => import('./components/visualization/UMLDiagram')); // Route-based splitting const routes = [ { path: '/explore', element: }> }, { path: '/query', element: }> } ]; ``` ### 2. Virtual Scrolling ```typescript // For large lists of custodians import { useVirtualizer } from '@tanstack/react-virtual'; function CustodianList({ custodians }: { custodians: CustodianObservation[] }) { const parentRef = useRef(null); const virtualizer = useVirtualizer({ count: custodians.length, getScrollElement: () => parentRef.current, estimateSize: () => 120, // Estimated item height overscan: 5, }); return (
{virtualizer.getVirtualItems().map((virtualItem) => (
))}
); } ``` ### 3. Web Workers ```typescript // workers/rdfParser.worker.ts import { parse } from 'n3'; self.addEventListener('message', async (event) => { const { content, format } = event.data; try { const triples = await parseRDF(content, format); self.postMessage({ type: 'success', triples }); } catch (error) { self.postMessage({ type: 'error', error: error.message }); } }); // Usage in component const parseInWorker = (content: string, format: string) => { return new Promise((resolve, reject) => { const worker = new Worker('/workers/rdfParser.worker.js'); worker.onmessage = (event) => { if (event.data.type === 'success') { resolve(event.data.triples); } else { reject(new Error(event.data.error)); } worker.terminate(); }; worker.postMessage({ content, format }); }); }; ``` ## Security Considerations ### 1. Content Security Policy ```typescript // helmet configuration app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], // For D3.js styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'", "https://query.wikidata.org"], }, }, })); ``` ### 2. Input Sanitization ```typescript // Sanitize SPARQL queries function sanitizeSPARQL(query: string): string { // Remove comments query = query.replace(/#.*$/gm, ''); // Check for injection patterns const dangerous = [ /DROP\s+GRAPH/i, /CLEAR\s+GRAPH/i, /DELETE\s+WHERE/i, ]; for (const pattern of dangerous) { if (pattern.test(query)) { throw new Error('Potentially dangerous SPARQL query detected'); } } return query; } ``` ## Deployment Architecture ### Docker Configuration ```dockerfile # Multi-stage build FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/nginx.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] ``` ### Environment Configuration ```typescript // config/env.ts export const config = { api: { baseUrl: process.env.VITE_API_URL || 'http://localhost:3000', timeout: parseInt(process.env.VITE_API_TIMEOUT || '30000'), }, sparql: { endpoint: process.env.VITE_SPARQL_ENDPOINT || 'http://localhost:3030/dataset', }, maps: { tileUrl: process.env.VITE_MAP_TILES || 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', }, features: { enableWebSockets: process.env.VITE_ENABLE_WS === 'true', enableOffline: process.env.VITE_ENABLE_OFFLINE === 'true', }, }; ``` ## Testing Architecture ### 1. Unit Testing ```typescript // Component testing with React Testing Library describe('CustodianCard', () => { it('renders custodian information correctly', () => { const custodian = mockCustodianObservation(); render(); expect(screen.getByText(custodian.name)).toBeInTheDocument(); expect(screen.getByText(custodian.institution_type)).toBeInTheDocument(); }); }); ``` ### 2. Integration Testing ```typescript // API integration tests describe('SPARQL API', () => { it('executes queries and returns results', async () => { const query = 'SELECT * WHERE { ?s ?p ?o } LIMIT 10'; const results = await sparqlClient.query(query); expect(results).toHaveLength(10); expect(results[0]).toHaveProperty('s'); expect(results[0]).toHaveProperty('p'); expect(results[0]).toHaveProperty('o'); }); }); ``` ### 3. E2E Testing ```typescript // Playwright E2E tests test('complete user journey', async ({ page }) => { await page.goto('/'); // Load RDF file await page.click('[data-testid="load-rdf"]'); await page.setInputFiles('input[type="file"]', 'fixtures/custodian.ttl'); // Navigate to query page await page.click('a[href="/query"]'); // Execute SPARQL query await page.fill('[data-testid="sparql-editor"]', 'SELECT * WHERE { ?s a :CustodianObservation }'); await page.click('[data-testid="execute-query"]'); // Verify results await expect(page.locator('[data-testid="results-table"]')).toBeVisible(); await expect(page.locator('tr')).toHaveCount(11); // Header + 10 results }); ``` ## Related Documents - [02-design-patterns.md](02-design-patterns.md) - Frontend design patterns - [03-tdd-strategy.md](03-tdd-strategy.md) - Test-driven development approach - [04-example-ld-mapping.md](04-example-ld-mapping.md) - Reusable modules from example_ld - [05-d3-visualization.md](05-d3-visualization.md) - D3.js visualization strategy