feat(frontend): improve werkgebied display and database UI

- Fix polygon rendering with static paint properties instead of data-driven
- Add ensureSourceAndLayers() helper for reliable layer management
- Use setPaintProperty() for historical vs modern styling distinction
- Improve Database page layout with back buttons and cleaner navigation
- Add ResizableNestedTable component for DuckLake data display
- Optimize spacing and layout in Database.css
- Update schema manifest
This commit is contained in:
kempersc 2025-12-07 14:26:37 +01:00
parent f284e87d13
commit 4825f57951
5 changed files with 2619 additions and 202 deletions

View file

@ -1,5 +1,5 @@
{
"generated": "2025-12-06T23:25:26.568Z",
"generated": "2025-12-07T12:41:26.252Z",
"version": "1.0.0",
"categories": [
{

File diff suppressed because it is too large Load diff

View file

@ -209,34 +209,31 @@ export function useWerkgebiedMapLibre(map: maplibregl.Map | null): WerkgebiedHoo
});
}
// Add fill layer (below markers)
// Add fill layer (below markers) - use static values, we'll update them when showing
if (!map.getLayer(WERKGEBIED_FILL_LAYER_ID)) {
// Check if institutions layer exists to insert before it
const beforeLayer = map.getLayer('institutions-circles') ? 'institutions-circles' : undefined;
map.addLayer({
id: WERKGEBIED_FILL_LAYER_ID,
type: 'fill',
source: WERKGEBIED_SOURCE_ID,
paint: {
'fill-color': ['get', 'fillColor'],
'fill-opacity': ['get', 'fillOpacity'],
'fill-color': WERKGEBIED_FILL_COLOR,
'fill-opacity': WERKGEBIED_FILL_OPACITY,
},
}, beforeLayer);
});
}
// Add line layer for borders
if (!map.getLayer(WERKGEBIED_LINE_LAYER_ID)) {
const beforeLayer = map.getLayer('institutions-circles') ? 'institutions-circles' : undefined;
map.addLayer({
id: WERKGEBIED_LINE_LAYER_ID,
type: 'line',
source: WERKGEBIED_SOURCE_ID,
paint: {
'line-color': ['get', 'lineColor'],
'line-width': ['get', 'lineWidth'],
'line-color': WERKGEBIED_LINE_COLOR,
'line-width': WERKGEBIED_LINE_WIDTH,
'line-dasharray': [5, 5],
},
}, beforeLayer);
});
}
layersAddedRef.current = true;
@ -298,6 +295,54 @@ export function useWerkgebiedMapLibre(map: maplibregl.Map | null): WerkgebiedHoo
setHistoricalDescription(null);
}, [map]);
// Helper to ensure source and layers exist
const ensureSourceAndLayers = useCallback(() => {
if (!map) return false;
// Add source if missing
if (!map.getSource(WERKGEBIED_SOURCE_ID)) {
console.log('[useWerkgebiedMapLibre] Adding missing source');
map.addSource(WERKGEBIED_SOURCE_ID, {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [],
},
});
}
// Add fill layer if missing
if (!map.getLayer(WERKGEBIED_FILL_LAYER_ID)) {
console.log('[useWerkgebiedMapLibre] Adding missing fill layer');
map.addLayer({
id: WERKGEBIED_FILL_LAYER_ID,
type: 'fill',
source: WERKGEBIED_SOURCE_ID,
paint: {
'fill-color': WERKGEBIED_FILL_COLOR,
'fill-opacity': WERKGEBIED_FILL_OPACITY,
},
});
}
// Add line layer if missing
if (!map.getLayer(WERKGEBIED_LINE_LAYER_ID)) {
console.log('[useWerkgebiedMapLibre] Adding missing line layer');
map.addLayer({
id: WERKGEBIED_LINE_LAYER_ID,
type: 'line',
source: WERKGEBIED_SOURCE_ID,
paint: {
'line-color': WERKGEBIED_LINE_COLOR,
'line-width': WERKGEBIED_LINE_WIDTH,
'line-dasharray': [5, 5],
},
});
}
return true;
}, [map]);
// Helper to update map with GeoJSON features
const updateMapWithFeatures = useCallback((
features: GeoJSONFeature[] | HALCFeature[],
@ -306,26 +351,50 @@ export function useWerkgebiedMapLibre(map: maplibregl.Map | null): WerkgebiedHoo
) => {
if (!map) return;
// Ensure source and layers exist before updating
if (!ensureSourceAndLayers()) {
console.warn('[useWerkgebiedMapLibre] Failed to ensure source and layers');
return;
}
const { fitBounds = true, fitBoundsPadding = [50, 50], maxZoom = 12 } = options;
// Add style properties to features
const styledFeatures = features.map(f => ({
...f,
properties: {
...f.properties,
fillColor: isHistorical ? HISTORICAL_FILL_COLOR : WERKGEBIED_FILL_COLOR,
fillOpacity: isHistorical ? HISTORICAL_FILL_OPACITY : WERKGEBIED_FILL_OPACITY,
lineColor: isHistorical ? HISTORICAL_LINE_COLOR : WERKGEBIED_LINE_COLOR,
lineWidth: isHistorical ? HISTORICAL_LINE_WIDTH : WERKGEBIED_LINE_WIDTH,
},
}));
// Update paint properties based on historical vs modern
const fillColor = isHistorical ? HISTORICAL_FILL_COLOR : WERKGEBIED_FILL_COLOR;
const fillOpacity = isHistorical ? HISTORICAL_FILL_OPACITY : WERKGEBIED_FILL_OPACITY;
const lineColor = isHistorical ? HISTORICAL_LINE_COLOR : WERKGEBIED_LINE_COLOR;
const lineWidth = isHistorical ? HISTORICAL_LINE_WIDTH : WERKGEBIED_LINE_WIDTH;
// Update layer paint properties
if (map.getLayer(WERKGEBIED_FILL_LAYER_ID)) {
map.setPaintProperty(WERKGEBIED_FILL_LAYER_ID, 'fill-color', fillColor);
map.setPaintProperty(WERKGEBIED_FILL_LAYER_ID, 'fill-opacity', fillOpacity);
}
if (map.getLayer(WERKGEBIED_LINE_LAYER_ID)) {
map.setPaintProperty(WERKGEBIED_LINE_LAYER_ID, 'line-color', lineColor);
map.setPaintProperty(WERKGEBIED_LINE_LAYER_ID, 'line-width', lineWidth);
}
// Ensure werkgebied layers are below institutions layer
if (map.getLayer('institutions-circles')) {
if (map.getLayer(WERKGEBIED_FILL_LAYER_ID)) {
map.moveLayer(WERKGEBIED_FILL_LAYER_ID, 'institutions-circles');
}
if (map.getLayer(WERKGEBIED_LINE_LAYER_ID)) {
map.moveLayer(WERKGEBIED_LINE_LAYER_ID, 'institutions-circles');
}
}
// Update source data
const source = map.getSource(WERKGEBIED_SOURCE_ID) as maplibregl.GeoJSONSource | undefined;
if (source) {
source.setData({
type: 'FeatureCollection',
features: styledFeatures,
features: features,
} as GeoJSON.FeatureCollection);
console.log('[useWerkgebiedMapLibre] Updated source with', features.length, 'features');
} else {
console.warn('[useWerkgebiedMapLibre] Source still not found after ensure:', WERKGEBIED_SOURCE_ID);
}
// Fit map bounds to werkgebied - only if camera is zoomed out far
@ -356,7 +425,7 @@ export function useWerkgebiedMapLibre(map: maplibregl.Map | null): WerkgebiedHoo
maxZoom: maxZoom,
});
}
}, [map]);
}, [map, ensureSourceAndLayers]);
// Show werkgebied for an archive
const showWerkgebied = useCallback((archiveId: string, options: WerkgebiedOptions = {}) => {

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,6 @@
import { useState } from 'react';
import { useLanguage } from '../contexts/LanguageContext';
import { DuckDBPanel } from '../components/database/DuckDBPanel';
import { DuckLakePanel } from '../components/database/DuckLakePanel';
import { PostgreSQLPanel } from '../components/database/PostgreSQLPanel';
import { TypeDBPanel } from '../components/database/TypeDBPanel';
@ -52,7 +51,7 @@ const DATABASES: DatabaseInfo[] = [
nl: 'Lakehouse met tijdreizen, ACID-transacties en schema-evolutie',
en: 'Lakehouse with time travel, ACID transactions, and schema evolution',
},
icon: '🏔️',
icon: '🦆',
color: '#FFC107',
},
{
@ -105,7 +104,7 @@ const TEXT = {
loading: { nl: 'Laden...', en: 'Loading...' },
// Quick comparison
quickComparison: { nl: 'Snelle vergelijking', en: 'Quick Comparison' },
quickComparison: { nl: 'Gegevensbanken', en: 'Databases' },
database: { nl: 'Database', en: 'Database' },
status: { nl: 'Status', en: 'Status' },
dataCount: { nl: 'Data', en: 'Data' },
@ -152,71 +151,79 @@ export function Database() {
<p>{t('pageSubtitle')}</p>
</header>
{/* Database Type Tabs */}
<nav className="database-nav">
<button
className={`db-nav-button ${activeDatabase === 'all' ? 'active' : ''}`}
onClick={() => setActiveDatabase('all')}
>
<span className="db-icon">📊</span>
<span className="db-name">{t('allDatabases')}</span>
</button>
{DATABASES.map((db) => (
<button
key={db.id}
className={`db-nav-button ${activeDatabase === db.id ? 'active' : ''}`}
onClick={() => setActiveDatabase(db.id)}
style={{ '--db-color': db.color } as React.CSSProperties}
>
<span className="db-icon">{db.icon}</span>
<span className="db-name">{db.name}</span>
</button>
))}
</nav>
{/* Content based on active tab */}
<div className="database-content">
{activeDatabase === 'all' && <AllDatabasesView language={language} />}
{activeDatabase === 'duckdb' && <DuckLakePanel />}
{activeDatabase === 'postgres' && <PostgreSQLPanel />}
{activeDatabase === 'typedb' && <TypeDBPanel />}
{activeDatabase === 'oxigraph' && <OxigraphPanel />}
{activeDatabase === 'all' && <AllDatabasesView language={language} onSelectDatabase={setActiveDatabase} />}
{activeDatabase === 'duckdb' && (
<>
<button className="back-button" onClick={() => setActiveDatabase('all')}>
{language === 'nl' ? 'Terug naar overzicht' : 'Back to overview'}
</button>
<DuckLakePanel />
</>
)}
{activeDatabase === 'postgres' && (
<>
<button className="back-button" onClick={() => setActiveDatabase('all')}>
{language === 'nl' ? 'Terug naar overzicht' : 'Back to overview'}
</button>
<PostgreSQLPanel />
</>
)}
{activeDatabase === 'typedb' && (
<>
<button className="back-button" onClick={() => setActiveDatabase('all')}>
{language === 'nl' ? 'Terug naar overzicht' : 'Back to overview'}
</button>
<TypeDBPanel />
</>
)}
{activeDatabase === 'oxigraph' && (
<>
<button className="back-button" onClick={() => setActiveDatabase('all')}>
{language === 'nl' ? 'Terug naar overzicht' : 'Back to overview'}
</button>
<OxigraphPanel />
</>
)}
</div>
{/* Info Section */}
<section className="info-card">
<h2>{t('databaseTypes')}</h2>
<div className="db-types-grid">
<div className="db-type-card" style={{ borderLeftColor: '#FFC107' }}>
<span className="db-type-icon">🏔</span>
<div>
<strong>DuckLake</strong>
<p>{t('ducklakeDescription')}</p>
{/* Info Section - only show on overview */}
{activeDatabase === 'all' && (
<section className="info-card">
<h2>{t('databaseTypes')}</h2>
<div className="db-types-grid">
<div className="db-type-card" style={{ borderLeftColor: '#FFC107' }}>
<span className="db-type-icon">🦆</span>
<div>
<strong>DuckLake</strong>
<p>{t('ducklakeDescription')}</p>
</div>
</div>
<div className="db-type-card" style={{ borderLeftColor: '#336791' }}>
<span className="db-type-icon">🐘</span>
<div>
<strong>PostgreSQL</strong>
<p>{t('relationalDescription')}</p>
</div>
</div>
<div className="db-type-card" style={{ borderLeftColor: '#6B5CE7' }}>
<span className="db-type-icon">🧠</span>
<div>
<strong>TypeDB</strong>
<p>{t('graphDescription')}</p>
</div>
</div>
<div className="db-type-card" style={{ borderLeftColor: '#00A86B' }}>
<span className="db-type-icon">🔗</span>
<div>
<strong>Oxigraph</strong>
<p>{t('rdfDescription')}</p>
</div>
</div>
</div>
<div className="db-type-card" style={{ borderLeftColor: '#336791' }}>
<span className="db-type-icon">🐘</span>
<div>
<strong>PostgreSQL</strong>
<p>{t('relationalDescription')}</p>
</div>
</div>
<div className="db-type-card" style={{ borderLeftColor: '#6B5CE7' }}>
<span className="db-type-icon">🧠</span>
<div>
<strong>TypeDB</strong>
<p>{t('graphDescription')}</p>
</div>
</div>
<div className="db-type-card" style={{ borderLeftColor: '#00A86B' }}>
<span className="db-type-icon">🔗</span>
<div>
<strong>Oxigraph</strong>
<p>{t('rdfDescription')}</p>
</div>
</div>
</div>
</section>
</section>
)}
</div>
);
}
@ -224,7 +231,7 @@ export function Database() {
/**
* All Databases Overview View
*/
function AllDatabasesView({ language }: { language: 'nl' | 'en' }) {
function AllDatabasesView({ language, onSelectDatabase }: { language: 'nl' | 'en'; onSelectDatabase: (db: DatabaseType) => void }) {
const t = (key: keyof typeof TEXT) => TEXT[key][language];
// Initialize all database hooks
@ -295,7 +302,7 @@ function AllDatabasesView({ language }: { language: 'nl' | 'en' }) {
return (
<div className="all-databases-view">
{/* Quick Comparison Grid */}
{/* Database Cards Grid */}
<section className="comparison-section">
<h2>{t('quickComparison')}</h2>
<div className="comparison-grid">
@ -305,26 +312,11 @@ function AllDatabasesView({ language }: { language: 'nl' | 'en' }) {
db={db}
language={language}
stats={dbStats[db.id]}
onClick={() => onSelectDatabase(db.id)}
/>
))}
</div>
</section>
{/* Compact panels for each database */}
<div className="database-panels-grid">
<div className="panel-wrapper duckdb">
<DuckDBPanel compact />
</div>
<div className="panel-wrapper postgres">
<PostgreSQLPanel compact />
</div>
<div className="panel-wrapper typedb">
<TypeDBPanel compact />
</div>
<div className="panel-wrapper oxigraph">
<OxigraphPanel compact />
</div>
</div>
</div>
);
}
@ -336,10 +328,12 @@ function DatabaseStatusCard({
db,
language,
stats,
onClick,
}: {
db: DatabaseInfo;
language: 'nl' | 'en';
stats: DatabaseStats;
onClick: () => void;
}) {
// Format large numbers
const formatNumber = (n: number) => {
@ -350,8 +344,12 @@ function DatabaseStatusCard({
return (
<div
className="db-status-card"
className="db-status-card clickable"
style={{ '--db-color': db.color } as React.CSSProperties}
onClick={onClick}
role="button"
tabIndex={0}
onKeyDown={(e) => e.key === 'Enter' && onClick()}
>
<div className="db-status-header">
<span className="db-icon-large">{db.icon}</span>