Files
geointel/frontend/src/components/auth/LandingPage.tsx
T
Jens c76a746cd7
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
Update
2026-07-27 23:28:43 +02:00

387 lines
16 KiB
TypeScript

import { useEffect, useRef, useState, type FormEvent } from 'react'
import {
ArrowRight,
BrainCircuit,
Check,
ChevronRight,
Database,
Eye,
EyeOff,
LockKeyhole,
LogIn,
MapPinned,
Menu,
MousePointer2,
ShieldCheck,
Sparkles,
UserRound,
X,
} from 'lucide-react'
import { login, loginAsGuest, type AuthSession } from '../../services/api/auth'
import { formatAuthError } from '../../lib/authError'
import '../../styles/landing.css'
import { GeoIntelMark } from '../brand/GeoIntelBrand'
import { ItWorxSignature } from '../brand/ItWorxSignature'
interface LandingPageProps {
onAuthenticated: (session: AuthSession) => void
serviceError?: string | null
guestAccessEnabled?: boolean
}
const capabilityItems = [
{
icon: MapPinned,
number: '01',
title: 'Eén kaartgerichte werkruimte',
description:
'Selecteer een gebied in België of de Belgische Noordzee en werk verder vanuit dezelfde ruimtelijke context.',
},
{
icon: Database,
number: '02',
title: 'Bronnen blijven herkenbaar',
description:
'Autoriteit, meetmoment, dekking, CRS en beperkingen blijven zichtbaar in plaats van achter één generieke kaartlaag te verdwijnen.',
},
{
icon: ShieldCheck,
number: '03',
title: 'Kwaliteit vóór resultaat',
description:
'Vergelijk referentie- en kandidaatgegevens, controleer bewijs en exporteer pas wanneer de context klopt.',
},
]
const workflowSteps = [
['Selecteer', 'Kies een officiële grens of teken zelf een gebied.'],
['Controleer', 'Bekijk dekking, bronkwaliteit en toepasselijke beperkingen.'],
['Analyseer', 'Meet toestand, evolutie of modelresultaten op de kaart.'],
['Onderbouw', 'Bewaar controleerbare resultaten en provenance.'],
]
export function LandingPage({
onAuthenticated,
serviceError = null,
guestAccessEnabled = false,
}: LandingPageProps): JSX.Element {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [pendingAction, setPendingAction] = useState<'operator' | 'guest' | null>(null)
const [authError, setAuthError] = useState<string | null>(null)
const [showPassword, setShowPassword] = useState(false)
const [menuOpen, setMenuOpen] = useState(false)
const usernameRef = useRef<HTMLInputElement | null>(null)
const accessPanelRef = useRef<HTMLElement | null>(null)
const busy = pendingAction !== null
useEffect(() => {
document.body.classList.add('landing-body')
return () => document.body.classList.remove('landing-body')
}, [])
const submitLogin = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
setPendingAction('operator')
setAuthError(null)
try {
const session = await login(username.trim(), password)
onAuthenticated(session)
} catch (error) {
setAuthError(formatAuthError(error, 'Aanmelden is momenteel niet gelukt. Controleer de verbinding en probeer opnieuw.'))
} finally {
setPendingAction(null)
}
}
const submitGuestLogin = async () => {
setMenuOpen(false)
setPendingAction('guest')
setAuthError(null)
accessPanelRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' })
try {
const session = await loginAsGuest()
onAuthenticated(session)
} catch (error) {
setAuthError(formatAuthError(error, 'De gastdemo kon niet worden voorbereid. Probeer het over enkele ogenblikken opnieuw.'))
} finally {
setPendingAction(null)
}
}
const focusLogin = () => {
setMenuOpen(false)
accessPanelRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' })
window.requestAnimationFrame(() => usernameRef.current?.focus())
}
return (
<div className="landing-page">
<a className="landing-skip-link" href="#login-panel">Ga naar toegang</a>
<header className="landing-header">
<a className="landing-brand" href="#top" aria-label="GeoIntel Atlas startpagina">
<GeoIntelMark className="landing-brand-mark" />
<span className="landing-brand-copy">
<strong>GeoIntel</strong>
<small>Atlas Workbench</small>
</span>
</a>
<button
className="landing-menu-toggle"
type="button"
aria-label={menuOpen ? 'Navigatie sluiten' : 'Navigatie openen'}
aria-expanded={menuOpen}
aria-controls="landing-navigation"
onClick={() => setMenuOpen((current) => !current)}
>
{menuOpen ? <X aria-hidden="true" /> : <Menu aria-hidden="true" />}
</button>
<nav
id="landing-navigation"
className={menuOpen ? 'landing-nav landing-nav-open' : 'landing-nav'}
aria-label="Landingspagina"
>
<a href="#mogelijkheden" onClick={() => setMenuOpen(false)}>Mogelijkheden</a>
<a href="#werkproces" onClick={() => setMenuOpen(false)}>Werkproces</a>
<a href="#kwaliteit" onClick={() => setMenuOpen(false)}>Kwaliteit</a>
</nav>
<div className="landing-header-actions">
{guestAccessEnabled ? (
<button className="landing-header-guest" type="button" onClick={submitGuestLogin} disabled={busy}>
Gastdemo
</button>
) : null}
<button className="landing-header-login" type="button" onClick={focusLogin} disabled={busy}>
Inloggen
</button>
</div>
</header>
<main id="top">
<section className="landing-hero" aria-labelledby="landing-title">
<div className="landing-hero-orbit landing-hero-orbit-one" aria-hidden="true" />
<div className="landing-hero-orbit landing-hero-orbit-two" aria-hidden="true" />
<div className="landing-hero-content">
<div className="landing-hero-copy">
<p className="landing-kicker">
<Sparkles aria-hidden="true" /> GeoAI-workbench voor België en de Noordzee
</p>
<h1 id="landing-title">
Van kaartlaag naar <span>aantoonbaar inzicht.</span>
</h1>
<p className="landing-lead">
GeoIntel brengt officiële bronnen, ruimtelijke analyse, AI-resultaten en kwaliteitscontrole samen in één professionele werkomgeving. Niet alleen zien wat er op de kaart staat, maar ook weten waarop het resultaat steunt.
</p>
<div className="landing-hero-actions">
{guestAccessEnabled ? (
<button className="landing-primary-action" type="button" onClick={submitGuestLogin} disabled={busy}>
<UserRound aria-hidden="true" />
{pendingAction === 'guest' ? 'Gastdemo voorbereiden…' : 'Verkennen als gast'}
</button>
) : (
<button className="landing-primary-action" type="button" onClick={focusLogin}>
<LogIn aria-hidden="true" /> Open de workbench
</button>
)}
<a className="landing-secondary-action" href="#mogelijkheden">
Bekijk mogelijkheden <ArrowRight aria-hidden="true" />
</a>
</div>
<div className="landing-proof-row" aria-label="Kernkwaliteiten">
<div><Check aria-hidden="true" /><span><strong>Officiële bronnen</strong><small>per rechtsgebied</small></span></div>
<div><Check aria-hidden="true" /><span><strong>Meetbare kwaliteit</strong><small>vóór export</small></span></div>
<div><Check aria-hidden="true" /><span><strong>Volledige provenance</strong><small>bij ieder resultaat</small></span></div>
</div>
</div>
<section
className="landing-access-card"
id="login-panel"
ref={accessPanelRef}
aria-labelledby="access-title"
>
<div className="landing-access-preview" aria-hidden="true">
<div className="landing-preview-toolbar">
<span><i /> Live werkcontext</span>
<span>België · Noordzee</span>
</div>
<div className="landing-preview-focus">
<span className="landing-preview-pin"><MapPinned /></span>
<div>
<small>Actieve analyse</small>
<strong>Gebouwen &amp; referentiedata</strong>
<span>Brondekking gecontroleerd</span>
</div>
</div>
<div className="landing-preview-metrics">
<span><strong>4</strong><small>rechtsgebieden</small></span>
<span><strong>QA</strong><small>bewijs zichtbaar</small></span>
<span><strong>GIS</strong><small>exporteerbaar</small></span>
</div>
</div>
<div className="landing-access-body">
<div className="landing-access-heading">
<span className="landing-login-icon" aria-hidden="true"><LockKeyhole /></span>
<div>
<p>Veilige toegang</p>
<h2 id="access-title">Open de GeoIntel-workbench</h2>
<span>Meld u aan als operator of start een beperkte demosessie.</span>
</div>
</div>
<form onSubmit={submitLogin} aria-busy={pendingAction === 'operator'}>
<label htmlFor="login-username">Gebruikersnaam</label>
<input
ref={usernameRef}
id="login-username"
name="username"
type="text"
autoComplete="username"
value={username}
onChange={(event) => setUsername(event.target.value)}
disabled={busy}
required
/>
<label htmlFor="login-password">Wachtwoord</label>
<div className="landing-password-field">
<input
id="login-password"
name="password"
type={showPassword ? 'text' : 'password'}
autoComplete="current-password"
value={password}
onChange={(event) => setPassword(event.target.value)}
disabled={busy}
required
/>
<button
type="button"
aria-label={showPassword ? 'Wachtwoord verbergen' : 'Wachtwoord tonen'}
aria-pressed={showPassword}
onClick={() => setShowPassword((current) => !current)}
disabled={busy}
>
{showPassword ? <EyeOff aria-hidden="true" /> : <Eye aria-hidden="true" />}
</button>
</div>
{authError || serviceError ? (
<p className="landing-login-error" role="alert">{authError ?? serviceError}</p>
) : null}
<button
className="landing-operator-submit"
type="submit"
disabled={busy || !username.trim() || !password}
>
<LogIn aria-hidden="true" />
{pendingAction === 'operator' ? 'Veilig aanmelden…' : 'Inloggen als operator'}
</button>
</form>
{guestAccessEnabled ? (
<div className="landing-access-divider"><span>of verken zonder account</span></div>
) : null}
{guestAccessEnabled ? (
<div className="landing-guest-access">
<span className="landing-guest-icon" aria-hidden="true"><UserRound /></span>
<div>
<strong>Gastmodus</strong>
<p>Open een tijdelijke, alleen-lezen demowerkruimte met voorbeelddata en een vooraf uitgevoerde kwaliteitscontrole.</p>
</div>
<button type="button" onClick={submitGuestLogin} disabled={busy}>
{pendingAction === 'guest' ? 'Demo voorbereiden…' : 'Als gast verkennen'}
<ChevronRight aria-hidden="true" />
</button>
</div>
) : null}
<p className="landing-session-note">
<ShieldCheck aria-hidden="true" /> HttpOnly-sessie · geen wachtwoordopslag in de browser
</p>
</div>
</section>
</div>
</section>
<section className="landing-capabilities" id="mogelijkheden" aria-labelledby="capabilities-title">
<div className="landing-section-heading">
<p>Gebouwd voor controleerbare analyse</p>
<h2 id="capabilities-title">Geen los dashboard, maar één samenhangende GIS-keten</h2>
<span>De interface volgt de manier waarop een onderbouwde ruimtelijke analyse werkelijk tot stand komt.</span>
</div>
<div className="landing-capability-grid">
{capabilityItems.map(({ icon: Icon, number, title, description }) => (
<article key={title} className="landing-capability">
<div className="landing-capability-topline">
<span className="landing-capability-icon" aria-hidden="true"><Icon /></span>
<small>{number}</small>
</div>
<h3>{title}</h3>
<p>{description}</p>
</article>
))}
</div>
</section>
<section className="landing-workflow" id="werkproces" aria-labelledby="workflow-title">
<div className="landing-workflow-visual">
<div className="landing-workflow-map-image" aria-hidden="true" />
<div className="landing-workflow-overlay">
<p>Kaart als werkomgeving</p>
<h2 id="workflow-title">Van selectie naar een resultaat dat u kunt verdedigen</h2>
<span>Bron, gebied, meetmoment en kwaliteitsbewijs blijven samen zichtbaar.</span>
</div>
<div className="landing-workflow-floating" aria-hidden="true">
<MousePointer2 />
<span><small>Selectie gecontroleerd</small><strong>2,14 km² · 186 objecten</strong></span>
</div>
</div>
<div className="landing-workflow-content">
<p className="landing-workflow-eyebrow">Een helder werkproces</p>
<h2>Vier stappen, zonder de technische context te verliezen</h2>
<div className="landing-workflow-steps">
{workflowSteps.map(([title, description], index) => (
<article key={title}>
<span>{String(index + 1).padStart(2, '0')}</span>
<div><h3>{title}</h3><p>{description}</p></div>
</article>
))}
</div>
<div className="landing-quality-callout" id="kwaliteit">
<BrainCircuit aria-hidden="true" />
<div>
<strong>AI blijft een gecontroleerde analysemethode</strong>
<p>Modelversie, confidence, referentiedata en validatiegrenzen blijven zichtbaar; ontbrekende configuratie wordt niet als succes voorgesteld.</p>
</div>
</div>
</div>
</section>
</main>
<footer className="landing-footer">
<div className="landing-footer-brand">
<GeoIntelMark className="landing-footer-mark" />
<div><strong>GeoIntel Atlas Workbench</strong><p>Operationele GIS-analyse voor België en de Belgische Noordzee.</p></div>
</div>
<div className="landing-footer-ownership">
<ItWorxSignature />
<p>© {new Date().getFullYear()} GeoIntel · Professionele operator- en demonstratieomgeving</p>
</div>
</footer>
</div>
)
}