Add interactive GeoIntel storytelling and portfolio assets
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Jens
2026-07-28 08:28:09 +02:00
parent fe60d97a47
commit 61e2505940
15 changed files with 532 additions and 259 deletions
+141 -258
View File
@@ -1,314 +1,197 @@
# GeoIntel Belgium and the Belgian North Sea # GeoIntel
GeoIntel is a map-first GeoAI Workbench for Belgium and the Belgian North Sea. <p align="center">
It combines governed official-source coverage, raster/vector processing, <img src="frontend/public/geointel-icon.png" width="92" alt="GeoIntel logo">
historical comparison, computer vision, QA/QC and geospatial exports. </p>
Mol and the Kempen remain deep regression and model-validation references. The <p align="center"><strong>Evidence-first GeoAI voor België en de Belgische Noordzee.</strong></p>
release scope is all of Belgium plus legally labelled Belgian maritime zones;
source coverage remains explicit per theme and jurisdiction.
GeoIntel is not a generic dashboard or chatbot. The core product is: GeoIntel is een kaartgerichte GeoAI-workbench waarin officiële databronnen,
ruimtelijke analyse, PyTorch-computervisie, QA/QC en export samenkomen. Het
platform bewaart niet alleen een resultaat, maar ook de bron, ruimtelijke
context, modelversie en het bewijs waarmee dat resultaat gecontroleerd kan
worden.
> data → processing → geospatial output → QA/QC → export > **Data → ruimtelijke verwerking → AI-resultaat → kwaliteitsbewijs → export**
## Current milestone ![GeoIntel landingspagina](docs/assets/portfolio/geointel-landing-hero.png)
**v1.0.0 - Belgium/North Sea release** ## Waarom GeoIntel?
The canonical release controls are: Veel geoportalen tonen lagen. GeoIntel ondersteunt een volledige,
controleerbare analyseketen:
- `docs/00-start/START_HERE.md` - selecteer een officiële grens of teken een eigen Area of Interest;
- `docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md` - combineer vector-, raster- en historische bronnen met expliciete dekking;
- `docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md` - voer GIS- en CUDA-versnelde beeldanalyse uit;
- `docs/RELEASE_RUNBOOK.md` - vergelijk kandidaatresultaten met referentiedata;
- `docs/KNOWN_LIMITATIONS.md` - inspecteer false positives, false negatives, overlap en provenance;
- `docs/DEFINITION_OF_DONE.md` - exporteer alleen wat als reproduceerbaar resultaat is vastgelegd.
Older milestone and sprint handoff files remain historical evidence. They do Mol en de Kempen blijven de gouden regressiegebieden. De productgrens is heel
not override the active national/maritime scope freeze or RC roadmap. België plus de juridisch benoemde Belgische maritieme zones.
## Core V1 vertical slice ## Product in beeld
The first implementation target is: ### Interactieve projectketen
1. Project + Area creation. De landingspagina vertaalt de technische keten naar vier interactieve
2. Dataset registration/upload and metadata extraction. schakels. De illustratie reageert op selectie, bronnen, analyse en bewijs en
3. Reference building layer loading. respecteert automatisch `prefers-reduced-motion`.
4. Predicted detection layer loading/import.
5. QA/QC matching against reference polygons.
6. Metrics and false positive/false negative outputs.
7. GeoJSON export.
8. Minimal map/workbench UI.
## Primary stack ![Interactieve GeoIntel-projectketen](docs/assets/portfolio/geointel-interactive-story.png)
- Frontend: React, TypeScript, MapLibre GL, Deck.gl, Tailwind. ### Kaart als werkruimte
- Backend: FastAPI, Python.
- Database: PostgreSQL + PostGIS.
- GIS processing: GeoPandas, Shapely, Rasterio, PyProj, GDAL.
- AI: PyTorch, Ultralytics YOLO, SAM-compatible architecture.
- Jobs: Redis + RQ.
- Storage: local filesystem first, MinIO-compatible later.
## Codex instructions De kaart blijft het primaire werkblad. Thema, broncontext, selectie en
resultaten zijn vanuit dezelfde ruimtelijke context bereikbaar.
Codex must start with: ![GeoIntel kaartwerkruimte](docs/assets/portfolio/geointel-workbench-map.png)
1. `docs/00-start/START_HERE.md` ### Kwaliteit vóór resultaat
2. `prompts/codex/M11_ARCHITECT_MASTER_PROMPT.md`
Then follow the build order in: QA/QC is een eerste-klas workflow. Bewaarde controles koppelen scores aan
objectbewijs, kandidaat- en referentielagen en technische provenance.
- `docs/build/BUILD_ORDER_DEPENDENCY_GRAPH.md` ![GeoIntel kwaliteitscontrole](docs/assets/portfolio/geointel-workbench-quality.png)
- `docs/build/CODEX_OPERATING_SYSTEM.md`
Before every implementation pass, run available preflight/smoke scripts where applicable. ## Kernmogelijkheden
## Repo principle | Domein | Mogelijkheden |
| --- | --- |
| Werkgebieden | Officiële grenzen, vrije AOI-selectie, clipping en buffering |
| Bronnen | GeoJSON, vector, raster, orthofoto, historische en regionale catalogi |
| GIS | CRS-normalisatie, spatial joins, rasterstatistiek, tile- en selectieanalyse |
| GeoAI | PyTorch, Ultralytics YOLO en SAM-compatibele abstrahering |
| Tijd | Immutable DatasetVersions en vergelijking van ruimtelijke snapshots |
| QA/QC | Precision, recall, F1, IoU, false-positive/-negative evidence en reviews |
| Export | Reproduceerbare GeoJSON- en artefactexports met provenance |
| Runtime | DockerMan-native Unraid-container met PostGIS, Redis, backend en frontend |
This is a documentation-driven engineering repo. The documentation is not decorative; it is the control system for autonomous implementation. ## Architectuur
## Fastest Day 1 command path ```mermaid
flowchart LR
```bash UI["React + MapLibre workbench"] --> API["FastAPI contracts"]
make readiness API --> GIS["GeoPandas / Shapely / Rasterio"]
API --> JOBS["Redis + RQ"]
JOBS --> AI["PyTorch / YOLO / SAM"]
GIS --> DB[("PostgreSQL + PostGIS")]
AI --> DB
DB --> QA["QA/QC + provenance"]
QA --> UI
QA --> EXPORT["GeoJSON / artifacts"]
``` ```
## Unraid / Tower deployment De publieke endpoints volgen [docs/API_CONTRACTS.md](docs/API_CONTRACTS.md).
PostGIS en de persistentieregels volgen
[docs/DATABASE_IMPLEMENTATION_PLAN.md](docs/DATABASE_IMPLEMENTATION_PLAN.md).
GeoIntel runs on Unraid as an all-in-one DockerMan-native container. The container embeds PostGIS, runs the FastAPI backend internally, and serves the frontend through nginx on one editable web port. ## Stack
Unraid template assets live in: - **Frontend:** React 18, TypeScript, MapLibre GL, Lucide en Vite
- **Backend:** FastAPI en Python
- **Spatial:** GeoPandas, Shapely, Rasterio, PyProj en GDAL
- **AI:** PyTorch, Ultralytics YOLO en SAM-compatibele segmentatie
- **Data:** PostgreSQL/PostGIS, lokale artefactopslag en immutable versions
- **Jobs:** Redis en RQ
- **Deployment:** één DockerMan-native Unraid-container
- `deploy/unraid/geointel.env.example` ## Lokaal starten
- `deploy/unraid/geointel-unraid-template.xml`
- `deploy/unraid/geointel-icon.svg`
- `deploy/unraid/geointel-icon.png`
- `docker-compose.unraid.yml`
Copy the Unraid env template to `.env` in the checkout and edit ports/paths there: Vereisten: Python 3.11+, Node 20.19+ of 22.12+, PostgreSQL/PostGIS en Redis.
```bash
cd /mnt/user/appdata/geointel
cp deploy/unraid/geointel.env.example .env
nano .env
docker build -f deploy/unraid/Dockerfile.all-in-one -t geointel-all-in-one:latest .
bash deploy/unraid/run-dockerman-container.sh
```
Common editable values:
```env
GEOINTEL_FRONTEND_PORT=1202
GEOINTEL_STORAGE_PATH=/mnt/user/appdata/geointel/storage
GEOINTEL_POSTGIS_DATA_PATH=/mnt/user/appdata/geointel/postgres-data
```
### Guest demonstration access
Guest access is enabled by default whenever the operator login gate is active.
No additional guest toggle is required for a new authenticated installation:
```env
GEOINTEL_AUTH_ENABLED=true
GEOINTEL_AUTH_USERNAME=operator
GEOINTEL_AUTH_PASSWORD_HASH=pbkdf2_sha256$...
GEOINTEL_AUTH_SESSION_SECRET=<independent-random-secret-of-at-least-32-characters>
GEOINTEL_GUEST_ACCESS_ENABLED=true
GEOINTEL_GUEST_DISPLAY_NAME=Gast
GEOINTEL_GUEST_SESSION_TTL_SECONDS=7200
```
The login page offers **Als gast verkennen**. A guest receives a short-lived,
read-only session scoped to the seeded demo project and sees only the map and
existing quality evidence. Set `GEOINTEL_GUEST_ACCESS_ENABLED=false` to hide and
disable this route. This is not multi-user authorization or tenant isolation;
use a separate demo instance when the installation contains private or
operational datasets.
The backend and PostGIS ports are intentionally not exposed to the LAN in the all-in-one runtime. See `deploy/unraid/README.md` for full setup, port-change and cleanup notes.
On Tower/Unraid, `scripts/deploy_tower.ps1` and `scripts/deploy_tower.sh` validate the Compose reference but build with plain `docker build`, then automatically install the editable DockerMan template as `/boot/config/plugins/dockerMan/templates-user/my-geointel.xml`, install the PNG icon as `/boot/config/plugins/dockerMan/images/geointel-icon.png`, remove any old Compose-owned `geointel` container and start the final container with DockerMan labels.
## Sprint 2 quick start
- Update dependencies:
```bash ```bash
python -m pip install -e backend/.[dev] python -m pip install -e backend/.[dev]
cd frontend && npm install cd frontend
npm install
npm run start
``` ```
- Run full readiness checks (with no scope expansion): Voor de volledige lokale stack:
```bash ```bash
docker compose up --build
```
Open daarna `http://localhost:1202`.
### Gastdemo
Met `GEOINTEL_GUEST_ACCESS_ENABLED=true` biedt de toegangspagina een
kortlevende, alleen-lezen demosessie. Die toont uitsluitend het ingestelde
demoproject en bewaard kwaliteitsbewijs; dit is geen multi-user- of
tenantisolatie.
## NVIDIA/CUDA
Productie-AI gebruikt de NVIDIA GPU van de server. De runtime faalt gesloten
wanneer CUDA vereist maar niet beschikbaar is; ontbrekende modelconfiguratie
wordt als `not_configured` getoond en nooit als geslaagde inferentie.
Controleer GPU-zichtbaarheid in de container met:
```bash
docker exec geointel nvidia-smi
```
## Kwaliteitscontrole
```bash
# Backend
python -m compileall backend/app python -m compileall backend/app
cd backend && python -m pytest cd backend && python -m pytest
cd ../frontend && npm run typecheck && npm run build
bash scripts/run_readiness_check.sh
```
- Raster workflow validation command (backend only): # Frontend
cd frontend
```bash
bash scripts/smoke_backend_import.sh
cd backend && python -c "from app.main import app; print(app.title)"
```
If `rasterio` is not installed, raster metadata endpoints return `RASTER_PROCESSING_UNAVAILABLE` and the frontend displays the
state as failed until the dependency is added.
## Sprint 4 raster foundation
- Raster operations now support:
- raster metadata extraction,
- raster preview generation,
- raster clip by area (with provenance on derived datasets),
- raster tile generation with manifest output.
- Raster services are dependency-aware:
- if `rasterio` is unavailable, endpoints return `RASTER_PROCESSING_UNAVAILABLE`.
- if preview dependencies (`numpy`, `pillow`) are unavailable, preview generation is unavailable with a clear error.
- Enable raster stack explicitly when needed:
```bash
cd backend && python -m pip install -e .[dev,raster]
```
## Sprint 5 raster analytics hardening
- Added raster band statistics (min/max/mean/std, nodata ratio/count, valid pixel count, dtype, optional histograms).
- Added raster reproject workflow with CRS validation and provenance persistence.
- Extended tile manifest expectations (`tile_set_id`, `tile_size`, `overlap`, `bounds`, `source_raster_id`, `tile_paths`, `tile_server`).
- Clarified raster operation availability in frontend/backend docs (`RASTER_PROCESSING_UNAVAILABLE` and invalid-CRS cases).
- Raster workflow command set (where available):
```bash
cd backend
python -m pip install -e .[dev,raster]
python -m pytest
cd ../frontend
npm run typecheck npm run typecheck
npm run test:unit
npm run build npm run build
``` ```
Then give Codex the prompt in: Een wijziging is pas afgerond wanneer de toepasselijke criteria uit
[docs/DEFINITION_OF_DONE.md](docs/DEFINITION_OF_DONE.md) aantoonbaar zijn
afgevinkt.
- `prompts/codex/final/DAY_1_MASTER_PROMPT.md` ## Tower / Unraid deploy
De canonieke checkout op Tower staat in `/mnt/user/appdata/geointel`. Gebruik
de releasewrapper; de productiecontainer wordt niet met een alternatieve
Compose-checkout vervangen.
## M13 Codex optimization ```powershell
powershell -ExecutionPolicy Bypass -File scripts/deploy_tower.ps1 `
For the first serious Codex build run, use: -RemoteHost root@<tower-ip> `
-RemotePath /mnt/user/appdata/geointel `
- `prompts/codex/m13/DAY_1_OPTIMIZED_MASTER_PROMPT.md` -RemoteBranch main `
-FrontendUrl http://<tower-ip>:1202
Codex should also use the relevant reusable skill under `skills/` for each implementation pass. Validate the optimization assets with:
```bash
make m13
``` ```
The full readiness path remains: Zie [deploy/unraid/README.md](deploy/unraid/README.md) en
[docs/RELEASE_RUNBOOK.md](docs/RELEASE_RUNBOOK.md) voor configuratie,
healthchecks en rollback.
```bash ## Documentatiekaart
make readiness
```
- [Start hier](docs/00-start/START_HERE.md)
- [Belgium/North Sea scope freeze](docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md)
- [API-contracten](docs/API_CONTRACTS.md)
- [Database-implementatieplan](docs/DATABASE_IMPLEMENTATION_PLAN.md)
- [Bekende beperkingen](docs/KNOWN_LIMITATIONS.md)
- [Release-runbook](docs/RELEASE_RUNBOOK.md)
## M14 Build Launch ## Portfolio-assets
For the first serious implementation run, use: De map [`docs/assets/portfolio`](docs/assets/portfolio) bevat presentatieklare
screenshots van de landingspagina, interactieve illustratie, kaartwerkruimte,
kwaliteitscontrole en mobiele weergave. De twee originele campagnebeelden
staan in [`frontend/public/portfolio`](frontend/public/portfolio) en worden
ook door de applicatie gebruikt.
- `docs/40-build-launch/SPRINT_1_SCOPE_FREEZE.md` ![GeoIntel mobiele landingspagina](docs/assets/portfolio/geointel-landing-mobile.png)
- `docs/40-build-launch/BUILD_SUCCESS_DEFINITION.md`
- `docs/40-build-launch/CODEX_STOP_RULES.md`
- `prompts/codex/m14/CODEX_FIRST_DAY_MASTER_PROMPT.md`
Validate launch assets with: ## Status
```bash Actieve mijlpaal: **v1.0.0 — Belgium and Belgian North Sea**.
make m14
```
Full readiness remains: GeoIntel is een project van Jens / ITWorx.tech.
```bash
make readiness
```
## Sprint 1 execution (Sprint 1 only)
From a clean machine:
```bash
cd backend && python -m pip install -e .[dev]
cd ..
make backend-install
make frontend-install
make readiness
```
Copy `.env.example` to `.env` only when you want local overrides. Docker Compose has safe defaults for the local PostGIS/backend/frontend stack and does not require a root `.env` file to exist.
With Docker Compose, open the workbench at `http://localhost:1202`.
The Docker frontend is served by nginx and proxies `/api` and `/health` to the backend container, so browser clients should use the frontend URL only, for example `http://192.168.10.150:1202` on a LAN host.
Runtime containers include healthchecks for PostGIS, backend and frontend. After
startup, inspect them with:
```bash
docker compose ps
```
Verify the browser-facing API proxy after rebuilding Docker images:
```bash
bash scripts/verify_browser_runtime.sh http://localhost:1202 http://localhost:8000/health
```
Verify the Docker GIS runtime after rebuilding the backend image:
```bash
bash scripts/verify_gis_runtime.sh http://localhost:1202
```
On the LAN host use the published browser URL, for example:
```bash
bash scripts/verify_gis_runtime.sh http://192.168.10.150:1202
```
Load the explicit offline demo workflow:
```bash
curl -X POST http://192.168.10.150:1202/api/v1/demo/workflow
```
If `/api/v1/projects` returns frontend HTML instead of a JSON envelope, rebuild
and restart the frontend container.
Useful direct verification commands:
```bash
python -m compileall backend/app
cd backend && python -c "from app.main import app; print(app.title)"
python -m pytest
cd ../frontend && npm run typecheck
cd ../frontend && npm run build
docker compose config
bash scripts/run_readiness_check.sh
```
If `make` or `docker` are unavailable in your shell, run the equivalent script entrypoints directly:
```bash
bash scripts/backend_install.sh
bash scripts/backend_test.sh
bash scripts/frontend_install.sh
bash scripts/frontend_typecheck.sh
bash scripts/frontend_build.sh
bash scripts/run_readiness_check.sh
```
+25
View File
@@ -11679,3 +11679,28 @@ Deployment evidence:
- Repeated the browser-rendered landing, guest-session, map and quality checks - Repeated the browser-rendered landing, guest-session, map and quality checks
against `http://192.168.10.150:1202`; the inspected live flow produced no against `http://192.168.10.150:1202`; the inspected live flow produced no
browser-console errors. browser-console errors.
## 2026-07-28 - Interactive presentation and portfolio pass
- Added a keyboard-operable four-stage project-chain illustration to the
public landing page. Selection, source governance, PyTorch analysis and QA
evidence each change the explanatory state and map treatment.
- Added restrained route, scan, evidence and map-depth motion. Every animation
and transition is disabled under `prefers-reduced-motion`.
- Generated two original, text-free GeoIntel campaign images: a Belgian
land/sea GeoAI hero and a building-detection/QA orthophoto composition. The
assets are stored in `frontend/public/portfolio` for reuse.
- Captured repository-owned portfolio screenshots for the landing hero,
interactive project story, map workbench, quality workbench and mobile
landing experience in `docs/assets/portfolio`.
- Replaced the legacy README with a product-led overview, visual tour,
architecture diagram, governed AI/CUDA behavior, setup, verification,
deployment and documentation map.
- Browser checks confirmed the tab interaction, the PyTorch stage content and
a 390 px layout without horizontal overflow.
Verified in this pass:
- `npm run typecheck`
- `npm run test:unit -- --maxWorkers=1 --reporter=verbose` (`45 passed`)
- `npm run build` (`1892 modules transformed`, production build passed)
+9
View File
@@ -980,3 +980,12 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Make guest authentication independent of optional browser scrolling support. - [x] Make guest authentication independent of optional browser scrolling support.
- [x] Restore the explicit frontend dependency exclusion required by the Unraid build-context gate. - [x] Restore the explicit frontend dependency exclusion required by the Unraid build-context gate.
- [x] Push the verified release and redeploy the Dockerman-native all-in-one runtime from `/mnt/user/appdata/geointel`. - [x] Push the verified release and redeploy the Dockerman-native all-in-one runtime from `/mnt/user/appdata/geointel`.
# Sprint 230 - Interactive storytelling and portfolio presentation
- [x] Add an accessible interactive project-chain illustration to the public landing page.
- [x] Add restrained map, scan, route and evidence motion with a complete reduced-motion fallback.
- [x] Produce original GeoIntel hero and building-QA campaign imagery for project and portfolio use.
- [x] Capture portfolio-ready landing, story, map, quality and mobile screenshots.
- [x] Rewrite the repository README around product value, architecture, operation and visual evidence.
- [ ] Deploy and visually verify the presentation release on the Tower runtime.
Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

@@ -22,6 +22,7 @@ import { formatAuthError } from '../../lib/authError'
import '../../styles/landing.css' import '../../styles/landing.css'
import { GeoIntelMark } from '../brand/GeoIntelBrand' import { GeoIntelMark } from '../brand/GeoIntelBrand'
import { ItWorxSignature } from '../brand/ItWorxSignature' import { ItWorxSignature } from '../brand/ItWorxSignature'
import { LandingProjectStory } from './LandingProjectStory'
interface LandingPageProps { interface LandingPageProps {
onAuthenticated: (session: AuthSession) => void onAuthenticated: (session: AuthSession) => void
@@ -338,6 +339,8 @@ export function LandingPage({
</div> </div>
</section> </section>
<LandingProjectStory />
<section className="landing-workflow" id="werkproces" aria-labelledby="workflow-title"> <section className="landing-workflow" id="werkproces" aria-labelledby="workflow-title">
<div className="landing-workflow-visual"> <div className="landing-workflow-visual">
<div className="landing-workflow-map-image" aria-hidden="true" /> <div className="landing-workflow-map-image" aria-hidden="true" />
@@ -0,0 +1,28 @@
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it } from 'vitest'
import { LandingProjectStory } from './LandingProjectStory'
afterEach(cleanup)
describe('LandingProjectStory', () => {
it('moves through the evidence-first project chain', () => {
render(<LandingProjectStory />)
expect(screen.getByRole('tabpanel').textContent).toContain('Teken één ruimtelijke vraag')
fireEvent.click(screen.getByRole('tab', { name: '03 Analyseer' }))
expect(screen.getByRole('tab', { name: '03 Analyseer' }).getAttribute('aria-selected')).toBe('true')
expect(screen.getByRole('tabpanel').textContent).toContain('Laat GIS en PyTorch samenwerken')
expect(screen.getByRole('tabpanel').textContent).toContain('186')
})
it('supports arrow-key navigation between stages', () => {
render(<LandingProjectStory />)
fireEvent.keyDown(screen.getByRole('tab', { name: '01 Selecteer' }), { key: 'ArrowRight' })
expect(screen.getByRole('tab', { name: '02 Bronnen' }).getAttribute('aria-selected')).toBe('true')
expect(screen.getByRole('tabpanel').textContent).toContain('Hou herkomst en dekking zichtbaar')
})
})
@@ -0,0 +1,151 @@
import { useState } from 'react'
import { BadgeCheck, Database, Download, MapPinned, ScanSearch } from 'lucide-react'
import '../../styles/landing-project-story.css'
const stages = [
{
key: 'select',
number: '01',
label: 'Selecteer',
title: 'Teken één ruimtelijke vraag',
description: 'Een officiële grens of vrije AOI wordt de vaste context voor elke volgende stap.',
metric: '2,14 km²',
metricLabel: 'geselecteerd',
icon: MapPinned,
},
{
key: 'source',
number: '02',
label: 'Bronnen',
title: 'Hou herkomst en dekking zichtbaar',
description: 'Bronautoriteit, CRS, meetmoment en lokale dekking blijven aan ieder resultaat gekoppeld.',
metric: '7',
metricLabel: 'bronnen gecontroleerd',
icon: Database,
},
{
key: 'analyse',
number: '03',
label: 'Analyseer',
title: 'Laat GIS en PyTorch samenwerken',
description: 'Ruimtelijke selectie, beeldanalyse en regionale context komen samen zonder ontbrekende AI-configuratie te verbergen.',
metric: '186',
metricLabel: 'objecten gevonden',
icon: ScanSearch,
},
{
key: 'prove',
number: '04',
label: 'Onderbouw',
title: 'Controleer bewijs vóór export',
description: 'QA/QC, uitzonderingen en provenance maken het resultaat navolgbaar en verdedigbaar.',
metric: '0,91',
metricLabel: 'kwaliteitsscore',
icon: BadgeCheck,
},
] as const
export function LandingProjectStory(): JSX.Element {
const [activeIndex, setActiveIndex] = useState(0)
const activeStage = stages[activeIndex]
return (
<section className="landing-project-story" aria-labelledby="project-story-title">
<div className="landing-project-story-copy">
<p className="landing-workflow-eyebrow">Interactieve projectillustratie</p>
<h2 id="project-story-title">Van gebied naar resultaat, zonder de bewijsvoering kwijt te raken</h2>
<p className="landing-project-story-intro">
Verken de vier schakels van de GeoIntel-keten. Elke stap verandert de kaart en toont welke context behouden blijft.
</p>
<div className="landing-story-tabs" role="tablist" aria-label="GeoIntel-projectketen">
{stages.map(({ key, number, label, icon: Icon }, index) => (
<button
key={key}
id={`story-tab-${key}`}
type="button"
role="tab"
aria-selected={activeIndex === index}
aria-controls="story-stage-panel"
tabIndex={activeIndex === index ? 0 : -1}
onClick={() => setActiveIndex(index)}
onKeyDown={(event) => {
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return
event.preventDefault()
const direction = event.key === 'ArrowRight' ? 1 : -1
setActiveIndex((current) => (current + direction + stages.length) % stages.length)
}}
>
<span>{number}</span>
<Icon aria-hidden="true" />
<strong>{label}</strong>
</button>
))}
</div>
<div
className="landing-story-detail"
id="story-stage-panel"
role="tabpanel"
aria-live="polite"
aria-labelledby={`story-tab-${activeStage.key}`}
key={activeStage.key}
>
<div>
<small>Actieve schakel</small>
<h3>{activeStage.title}</h3>
<p>{activeStage.description}</p>
</div>
<span className="landing-story-metric">
<strong>{activeStage.metric}</strong>
<small>{activeStage.metricLabel}</small>
</span>
</div>
</div>
<div className={`landing-story-visual is-stage-${activeIndex + 1}`} aria-hidden="true">
<div className="landing-story-map" />
<svg viewBox="0 0 720 610" role="presentation">
<defs>
<linearGradient id="story-area" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stopColor="#8ee7d2" stopOpacity=".34" />
<stop offset="1" stopColor="#23a68e" stopOpacity=".1" />
</linearGradient>
<filter id="story-glow" x="-80%" y="-80%" width="260%" height="260%">
<feGaussianBlur stdDeviation="9" />
</filter>
</defs>
<g className="story-tile-grid">
<path d="M0 122H720M0 244H720M0 366H720M0 488H720" />
<path d="M144 0V610M288 0V610M432 0V610M576 0V610" />
</g>
<path className="story-aoi" d="M210 152 493 123 582 284 515 473 258 504 139 319Z" fill="url(#story-area)" />
<path className="story-route" d="M157 419C231 345 236 223 339 241s121 126 219 45" />
<g className="story-buildings">
<path d="m260 286 48-15 21 68-49 15Z" />
<path d="m340 207 69-11 10 57-67 13Z" />
<path d="m392 330 74-23 28 88-76 24Z" />
<path d="m220 376 52-18 19 55-51 19Z" />
</g>
<g className="story-evidence">
<circle cx="307" cy="314" r="8" />
<circle cx="402" cy="224" r="8" />
<circle cx="445" cy="365" r="8" />
<circle cx="255" cy="396" r="8" />
</g>
<g className="story-pulse">
<circle cx="445" cy="365" r="27" filter="url(#story-glow)" />
<circle cx="445" cy="365" r="12" />
</g>
<path className="story-scan" d="M142 219H581" />
</svg>
<div className="landing-story-status">
<span><i /> Live analyse</span>
<strong>{activeStage.label}</strong>
<small>{activeStage.metricLabel}</small>
</div>
<div className="landing-story-export"><Download /><span>Controleerbaar resultaat</span></div>
</div>
</section>
)
}
@@ -0,0 +1,174 @@
.landing-project-story {
display: grid;
grid-template-columns: minmax(0, .88fr) minmax(30rem, 1.12fr);
gap: clamp(2.5rem, 6vw, 6rem);
align-items: center;
width: min(92rem, 100%);
margin-inline: auto;
padding: clamp(5rem, 9vw, 8rem) clamp(1.25rem, 4vw, 4.5rem);
}
.landing-project-story-copy > h2 {
max-width: 18ch;
margin: .75rem 0 0;
font-family: "Manrope", sans-serif;
font-size: clamp(2.25rem, 4vw, 4rem);
letter-spacing: -.055em;
line-height: 1.04;
text-wrap: balance;
}
.landing-project-story-intro {
max-width: 60ch;
margin: 1.25rem 0 0;
color: var(--landing-muted);
font-size: .9rem;
line-height: 1.75;
}
.landing-story-tabs {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: .45rem;
margin-top: 2rem;
}
.landing-story-tabs button {
position: relative;
display: grid;
gap: .45rem;
justify-items: start;
min-width: 0;
border: 1px solid var(--landing-line);
border-radius: .8rem;
padding: .8rem;
background: rgba(255, 255, 255, .72);
color: var(--landing-muted);
cursor: pointer;
transition: transform .24s ease, border-color .24s ease, background .24s ease, color .24s ease;
}
.landing-story-tabs button::after {
position: absolute;
right: .8rem;
bottom: 0;
left: .8rem;
height: 2px;
border-radius: 999px;
background: var(--landing-primary);
content: '';
opacity: 0;
transform: scaleX(.2);
transition: opacity .24s ease, transform .24s ease;
}
.landing-story-tabs button:hover { transform: translateY(-2px); }
.landing-story-tabs button[aria-selected='true'] {
border-color: #83c8bb;
background: var(--landing-mint);
color: var(--landing-primary-strong);
}
.landing-story-tabs button[aria-selected='true']::after { opacity: 1; transform: scaleX(1); }
.landing-story-tabs button > span { font-size: .54rem; font-weight: 800; letter-spacing: .1em; }
.landing-story-tabs svg { width: 1rem; height: 1rem; }
.landing-story-tabs strong { overflow: hidden; max-width: 100%; font-size: .65rem; text-overflow: ellipsis; }
.landing-story-detail {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 1.5rem;
align-items: end;
margin-top: 1rem;
border-left: 3px solid var(--landing-primary);
border-radius: 0 .8rem .8rem 0;
padding: 1rem 1.1rem;
background: linear-gradient(90deg, rgba(222, 247, 241, .8), rgba(247, 252, 251, .55));
animation: landing-story-enter .42s ease both;
}
.landing-story-detail small { color: var(--landing-primary); font-size: .55rem; font-weight: 800; letter-spacing: .09em; text-transform: uppercase; }
.landing-story-detail h3 { margin: .25rem 0 0; font-family: "Manrope", sans-serif; font-size: 1rem; }
.landing-story-detail p { max-width: 54ch; margin: .35rem 0 0; color: var(--landing-muted); font-size: .69rem; line-height: 1.6; }
.landing-story-metric { display: grid; min-width: 6.5rem; text-align: right; }
.landing-story-metric strong { color: var(--landing-primary-strong); font-family: "Manrope", sans-serif; font-size: 1.75rem; line-height: 1; }
.landing-story-metric small { margin-top: .3rem; letter-spacing: .04em; }
.landing-story-visual {
position: relative;
isolation: isolate;
min-height: 38rem;
overflow: hidden;
border: 1px solid rgba(161, 215, 204, .6);
border-radius: 1.6rem;
background: #072f2c;
box-shadow: 0 35px 90px rgba(10, 67, 59, .2);
}
.landing-story-map {
position: absolute;
inset: 0;
background: linear-gradient(145deg, rgba(4, 35, 32, .2), rgba(2, 26, 24, .78)), url('/portfolio/geointel-building-qa.png') center / cover;
filter: saturate(.78) contrast(1.03);
transform: scale(1.035);
transition: transform 1.1s cubic-bezier(.22, 1, .36, 1), filter .5s ease;
}
.landing-story-visual:hover .landing-story-map { filter: saturate(.95) contrast(1.03); transform: scale(1.065); }
.landing-story-visual svg { position: absolute; z-index: 1; inset: 0; width: 100%; height: 100%; }
.story-tile-grid { fill: none; stroke: rgba(156, 229, 214, .12); stroke-width: 1; }
.story-aoi { stroke: #8ee7d2; stroke-width: 3; stroke-dasharray: 10 7; transition: opacity .4s ease, transform .55s ease; transform-origin: center; }
.story-route { fill: none; stroke: #8ee7d2; stroke-width: 4; stroke-dasharray: 9 12; stroke-linecap: round; animation: landing-story-route 12s linear infinite; }
.story-buildings { fill: rgba(102, 222, 197, .12); stroke: #a3f0df; stroke-width: 3; transition: opacity .35s ease; }
.story-evidence { fill: #f5c267; stroke: #fff4d4; stroke-width: 2; transition: opacity .35s ease; }
.story-pulse { fill: rgba(110, 236, 207, .68); stroke: #c8fff1; stroke-width: 2; animation: landing-story-pulse 2.8s ease-out infinite; transform-origin: 445px 365px; }
.story-scan { fill: none; stroke: rgba(190, 255, 240, .78); stroke-width: 2; filter: drop-shadow(0 0 7px #7be6d1); animation: landing-story-scan 5.5s ease-in-out infinite; }
.is-stage-1 .story-buildings, .is-stage-1 .story-evidence { opacity: .22; }
.is-stage-2 .story-evidence { opacity: .28; }
.is-stage-3 .story-aoi { transform: scale(.94); }
.is-stage-4 .story-buildings { opacity: .5; }
.landing-story-status, .landing-story-export {
position: absolute;
z-index: 2;
display: grid;
border: 1px solid rgba(196, 244, 234, .24);
border-radius: .9rem;
background: rgba(3, 39, 35, .78);
color: #fff;
box-shadow: 0 16px 40px rgba(0, 0, 0, .24);
backdrop-filter: blur(13px);
}
.landing-story-status { top: 1.25rem; left: 1.25rem; gap: .2rem; min-width: 10rem; padding: .8rem .95rem; }
.landing-story-status span { color: #8ee7d2; font-size: .52rem; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; }
.landing-story-status i { display: inline-block; width: .42rem; height: .42rem; margin-right: .4rem; border-radius: 50%; background: #77e2ca; box-shadow: 0 0 0 5px rgba(119, 226, 202, .11); }
.landing-story-status strong { font-family: "Manrope", sans-serif; font-size: .92rem; }
.landing-story-status small { color: rgba(255, 255, 255, .58); font-size: .59rem; }
.landing-story-export { right: 1.25rem; bottom: 1.25rem; grid-template-columns: auto 1fr; gap: .55rem; align-items: center; padding: .75rem .9rem; font-size: .62rem; }
.landing-story-export svg { position: static; width: 1rem; height: 1rem; color: #8ee7d2; }
@keyframes landing-story-enter { from { opacity: 0; transform: translateY(8px); } }
@keyframes landing-story-route { to { stroke-dashoffset: -84; } }
@keyframes landing-story-pulse { 50% { opacity: .45; transform: scale(1.45); } }
@keyframes landing-story-scan { 0%, 100% { opacity: .12; transform: translateY(-120px); } 50% { opacity: .8; transform: translateY(250px); } }
@media (max-width: 1000px) {
.landing-project-story { grid-template-columns: 1fr; }
.landing-project-story-copy > h2 { max-width: 21ch; }
.landing-story-visual { min-height: 34rem; }
}
@media (max-width: 640px) {
.landing-project-story { padding: 4rem 1rem; }
.landing-project-story-copy > h2 { font-size: 2.15rem; }
.landing-story-tabs { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.landing-story-detail { grid-template-columns: 1fr; }
.landing-story-metric { text-align: left; }
.landing-story-visual { min-height: 28rem; border-radius: 1.15rem; }
}
@media (prefers-reduced-motion: reduce) {
.landing-project-story *, .landing-project-story *::before, .landing-project-story *::after {
animation: none !important;
transition-duration: .01ms !important;
}
}
+1 -1
View File
@@ -234,7 +234,7 @@ body.landing-body {
inset: 0; inset: 0;
background: background:
linear-gradient(112deg, rgba(244, 250, 248, 1) 8%, rgba(241, 249, 247, 0.96) 45%, rgba(225, 241, 237, 0.77) 100%), linear-gradient(112deg, rgba(244, 250, 248, 1) 8%, rgba(241, 249, 247, 0.96) 45%, rgba(225, 241, 237, 0.77) 100%),
url('/landing-hero-belgium.webp') center / cover; url('/portfolio/geointel-belgium-north-sea-hero.png') center / cover;
content: ''; content: '';
} }