Files
triqura-ecd/app/login/components/login-form.tsx
colinislit 05836c5c6a feat(cortex): implement activePatient fallback in various components
- Updated DagnotatieBlock to use activePatient for prefill if no patientId is provided.
- Enhanced PatientDashboardBlock to fallback to activePatient for patientId and name.
- Adjusted usePatientSelection to handle pending actions and re-route after patient selection.
- Refined chat components to manage nudge messages and integrate them into the chat flow.
- Improved UI elements for better patient selection experience.

This update enhances user experience by ensuring that the active patient context is utilized across multiple components, streamlining workflows and reducing manual input.
2026-01-03 22:18:30 +01:00

309 lines
12 KiB
TypeScript

'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import { Eye, EyeOff, Zap, Layout } from 'lucide-react'
import { loginWithPassword, signUpWithPassword, updateInterfacePreference, type InterfacePreference } from '@/lib/auth/client'
export function LoginForm() {
const router = useRouter()
const [mode, setMode] = useState<'login' | 'signup'>('login')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [loading, setLoading] = useState(false)
const [showPassword, setShowPassword] = useState(false)
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
const [interfacePreference, setInterfacePreference] = useState<InterfacePreference>('cortex')
// Redirect path based on interface preference
const getRedirectPath = () => {
return interfacePreference === 'cortex' ? '/epd/cortex' : '/epd/clients'
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setMessage(null)
try {
if (mode === 'signup') {
if (password !== confirmPassword) {
throw new Error('Wachtwoorden komen niet overeen')
}
const result = await signUpWithPassword(email, password)
if (result.session) {
// Save interface preference after successful signup
await updateInterfacePreference(interfacePreference)
setMessage({ type: 'success', text: 'Account aangemaakt! Je wordt doorgestuurd...' })
setTimeout(() => {
router.push(getRedirectPath())
}, 1000)
} else {
setMessage({
type: 'success',
text: 'Check je inbox voor een verificatie link om je account te activeren.'
})
}
} else {
await loginWithPassword(email, password)
// Save interface preference after successful login
await updateInterfacePreference(interfacePreference)
setMessage({ type: 'success', text: 'Ingelogd! Je wordt doorgestuurd...' })
setTimeout(() => {
router.push(getRedirectPath())
}, 1000)
}
} catch (error: any) {
if (error.code === 'user_already_registered' && error.data?.session) {
// Save interface preference for auto-logged in user
await updateInterfacePreference(interfacePreference)
setMessage({ type: 'success', text: 'Dit emailadres bestaat al. Je bent nu ingelogd!' })
setTimeout(() => {
router.push(getRedirectPath())
}, 1000)
} else if (error.code === 'user_already_registered') {
setMessage({
type: 'error',
text: error.message || 'Dit emailadres is al geregistreerd. Probeer in te loggen.'
})
setTimeout(() => {
setMode('login')
setPassword('')
setConfirmPassword('')
}, 2000)
} else if (error.code === 'invalid_credentials' && mode === 'login') {
setMessage({ type: 'error', text: error.message || 'Email of wachtwoord is onjuist.' })
} else if (error.code === 'email_not_confirmed') {
setMessage({ type: 'error', text: error.message || 'Je email is nog niet geverifieerd. Check je inbox.' })
} else {
setMessage({ type: 'error', text: error.message || 'Er ging iets mis. Probeer opnieuw.' })
}
} finally {
setLoading(false)
}
}
const handleQuickDemoLogin = async () => {
setMode('login')
setEmail('demo@mini-ecd.demo')
setPassword('Demo2024!')
setLoading(true)
try {
await loginWithPassword('demo@mini-ecd.demo', 'Demo2024!')
// Save interface preference for demo user
await updateInterfacePreference(interfacePreference)
router.push(getRedirectPath())
} catch (error: any) {
setMessage({ type: 'error', text: 'Demo login mislukt. Probeer handmatig in te loggen.' })
setLoading(false)
}
}
return (
<div className="md:w-2/5 bg-white p-8 md:p-12 flex flex-col justify-center">
<div className="w-full max-w-md mx-auto">
<div className="text-center mb-8">
<h2 className="text-2xl md:text-3xl font-bold text-slate-900 mb-2">
{mode === 'login' ? 'Welkom terug' : 'Maak een account'}
</h2>
<p className="text-slate-600">
{mode === 'login'
? 'Log in om toegang te krijgen tot het EPD'
: 'Start vandaag nog met snellere rapportages'}
</p>
</div>
<div className="flex bg-slate-100 p-1 rounded-lg mb-6">
<button
className={`flex-1 py-2 text-sm font-medium rounded-md transition-all ${
mode === 'login' ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-500 hover:text-slate-900'
}`}
onClick={() => setMode('login')}
>
Inloggen
</button>
<button
className={`flex-1 py-2 text-sm font-medium rounded-md transition-all ${
mode === 'signup' ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-500 hover:text-slate-900'
}`}
onClick={() => setMode('signup')}
>
Registreren
</button>
</div>
{/* Interface Selector */}
<div className="mb-6">
<label className="block text-sm font-medium text-slate-700 mb-2">
Interface voorkeur
</label>
<div className="grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => setInterfacePreference('klassiek')}
className={`flex items-center gap-2 p-3 rounded-lg border-2 transition-all ${
interfacePreference === 'klassiek'
? 'border-teal-500 bg-teal-50 text-teal-700'
: 'border-slate-200 hover:border-slate-300 text-slate-600'
}`}
>
<Layout className="w-5 h-5" />
<div className="text-left">
<div className="font-medium text-sm">Klassiek</div>
<div className="text-xs opacity-70">Menu navigatie</div>
</div>
</button>
<button
type="button"
onClick={() => setInterfacePreference('cortex')}
className={`flex items-center gap-2 p-3 rounded-lg border-2 transition-all ${
interfacePreference === 'cortex'
? 'border-amber-500 bg-amber-50 text-amber-700'
: 'border-slate-200 hover:border-slate-300 text-slate-600'
}`}
>
<Zap className="w-5 h-5" />
<div className="text-left">
<div className="font-medium text-sm">Cortex</div>
<div className="text-xs opacity-70">Je EPD dat meedenkt</div>
</div>
</button>
</div>
</div>
{message && (
<div
className={`mb-6 p-4 rounded-lg ${
message.type === 'success'
? 'bg-teal-50 text-teal-800 border border-teal-200'
: 'bg-red-50 text-red-800 border border-red-200'
}`}
>
<p className="text-sm font-medium">{message.text}</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-700 mb-1">
Email
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="jouw@email.nl"
required
className="w-full px-4 py-2.5 border border-slate-300 rounded-lg focus:ring-2 focus:ring-slate-400 focus:border-slate-400"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-slate-700 mb-1">
Wachtwoord
</label>
<div className="relative">
<input
id="password"
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
minLength={8}
className="w-full px-4 py-2.5 pr-11 border border-slate-300 rounded-lg focus:ring-2 focus:ring-slate-400 focus:border-slate-400"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 transition-colors"
aria-label={showPassword ? 'Verberg wachtwoord' : 'Toon wachtwoord'}
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
{mode === 'login' && (
<div className="mt-1 text-right">
<Link href="/reset-password" className="text-xs text-slate-500 hover:text-teal-600">
Wachtwoord vergeten?
</Link>
</div>
)}
</div>
{mode === 'signup' && (
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-slate-700 mb-1">
Bevestig Wachtwoord
</label>
<div className="relative">
<input
id="confirmPassword"
type={showConfirmPassword ? 'text' : 'password'}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••"
required
minLength={8}
className="w-full px-4 py-2.5 pr-11 border border-slate-300 rounded-lg focus:ring-2 focus:ring-slate-400 focus:border-slate-400"
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 transition-colors"
aria-label={showConfirmPassword ? 'Verberg wachtwoord' : 'Toon wachtwoord'}
>
{showConfirmPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
)}
<button
type="submit"
disabled={loading}
className="w-full bg-teal-600 hover:bg-teal-700 text-white font-medium py-2.5 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Laden...' : mode === 'login' ? 'Inloggen' : 'Registreren'}
</button>
</form>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-slate-300" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-slate-500">of</span>
</div>
</div>
<button
onClick={handleQuickDemoLogin}
disabled={loading}
className="w-full bg-amber-50 hover:bg-amber-100 text-amber-800 text-sm font-medium py-2 px-4 rounded-lg border border-amber-200 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
>
<span></span>
Demo Account Proberen
</button>
<p className="text-center text-xs text-slate-500 mt-8">
Build in Public door{' '}
<a
href="https://ikbenlit.nl"
target="_blank"
rel="noopener noreferrer"
className="text-teal-600 hover:text-teal-700 font-medium"
>
AI Speedrun
</a>
</p>
</div>
</div>
)
}