"use client"; import { Suspense, useEffect, useState } from "react"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { api } from "@/lib/api"; import { useSession } from "@/lib/store"; import { Button, Input, Skeleton } from "@/components/ui"; function safeNext(raw: string | null): string { if (!raw || !raw.startsWith("/") || raw.startsWith("//")) return "/"; return raw; } function LoginForm() { const router = useRouter(); const params = useSearchParams(); const next = safeNext(params.get("next")); const expired = params.get("reason") === "expired"; const refresh = useSession((s) => s.refresh); const status = useSession((s) => s.status); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); useEffect(() => { if (status === "authenticated" && !busy) router.replace(next); }, [status, busy, next, router]); const submit = async (e: React.FormEvent) => { e.preventDefault(); if (busy) return; setBusy(true); setError(null); try { await api("/api/auth/login", { json: { username: username.trim().toLowerCase(), password } }); await refresh(); router.push(next); router.refresh(); } catch (err) { setError(err instanceof Error ? err.message : "Invalid username or password."); setBusy(false); } }; return (
Sign in

Welcome back.

Your credits, streak and progress are exactly where you left them.

{expired ? (

Your session expired. Sign in to continue.

) : null}
setUsername(e.target.value)} autoComplete="username" autoCapitalize="none" autoCorrect="off" spellCheck={false} placeholder="your username" required /> setPassword(e.target.value)} autoComplete="current-password" placeholder="your password" required /> {error ? (

{error}

) : null}
Forgot your password? Use your recovery code

New to Spinza?{" "} Create a free account

); } export default function LoginPage() { return ( } > ); }