'use client'; import { useState, useTransition } from 'react'; import { useTranslations } from 'next-intl'; import Link from 'next/link'; import { login } from '@/lib/auth-actions'; import { sanitizeNextPath } from '@/lib/safe-next'; import { BRAND_YELLOW } from '@/components/brand/brand'; import { TesseraLogo } from '@/components/brand/tessera-logo'; import { DesktopDownloadLinks } from '@/components/desktop/desktop-download-links'; /** * Split-screen login page (D-01). * Left: Tessera branding on the brand yellow background (BRAND_YELLOW). * Right: Login form on neutral background. * No sidebar or header (D-04, handled by (auth) layout). */ export default function LoginPage() { const t = useTranslations('auth'); const [isPending, startTransition] = useTransition(); const [error, setError] = useState(null); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setError(null); const formData = new FormData(e.currentTarget); startTransition(async () => { const result = await login(formData); if (result.success) { // `next` erst beim Absenden aus window.location.search lesen statt // per useSearchParams(): der Hook verlangt in Next 15 eine // Suspense-Grenze, sonst bricht `next build` fuer die statisch // vorgerenderte Anmeldeseite ab (quick-260917-gyd). const next = new URLSearchParams(window.location.search).get('next'); window.location.href = sanitizeNextPath(next); } else { setError(result.error ?? 'invalidCredentials'); } }); } return (
{/* Left: Branding panel (hidden on mobile, visible on md+) */}
{/* Right: Login form */}
{/* Mobile-only branding */}

{t('branding.tagline')}

{/* Login heading */}

{t('login')}

{/* Error message */} {error && (
{t(`error.${error}`)}
)} {/* Login form */}
{/* Username */}
{/* Password */}
{/* Remember me (D-02) */}
{/* Forgot password link (D-03) */}
{t('forgotPassword')}
{/* Submit button */}
{/* Desktop-App-Link (D-12), blendet sich aus ohne API-Antwort */}
); }