SPB Git forge

spb/spinza

Public
8commits 1branches 0releases
1.6 MBsize
maindefault branch
16 days agolast push
TypeScript 97.6% SQL 1.4% JavaScript 0.5%
3.8 KB · 105 lines tsx
Raw Blame History
1"use client";23import { Suspense, useEffect, useState } from "react";4import Link from "next/link";5import { useRouter, useSearchParams } from "next/navigation";6import { api } from "@/lib/api";7import { useSession } from "@/lib/store";8import { Button, Input, Skeleton } from "@/components/ui";910function safeNext(raw: string | null): string {11  if (!raw || !raw.startsWith("/") || raw.startsWith("//")) return "/";12  return raw;13}1415function LoginForm() {16  const router = useRouter();17  const params = useSearchParams();18  const next = safeNext(params.get("next"));19  const expired = params.get("reason") === "expired";20  const refresh = useSession((s) => s.refresh);21  const status = useSession((s) => s.status);22  const [username, setUsername] = useState("");23  const [password, setPassword] = useState("");24  const [error, setError] = useState<string | null>(null);25  const [busy, setBusy] = useState(false);2627  useEffect(() => {28    if (status === "authenticated" && !busy) router.replace(next);29  }, [status, busy, next, router]);3031  const submit = async (e: React.FormEvent) => {32    e.preventDefault();33    if (busy) return;34    setBusy(true);35    setError(null);36    try {37      await api("/api/auth/login", { json: { username: username.trim().toLowerCase(), password } });38      await refresh();39      router.push(next);40      router.refresh();41    } catch (err) {42      setError(err instanceof Error ? err.message : "Invalid username or password.");43      setBusy(false);44    }45  };4647  return (48    <div className="surface rounded-xl p-6 sm:p-8">49      <div className="mb-6">50        <div className="eyebrow mb-2">Sign in</div>51        <h1 className="text-2xl font-semibold tracking-tight">Welcome back.</h1>52        <p className="mt-1.5 text-sm text-fg-3">Your credits, streak and progress are exactly where you left them.</p>53      </div>5455      {expired ? (56        <p className="mb-4 rounded-md border border-line-2 bg-surface-2 px-4 py-3 text-sm text-fg-2" role="status">57          Your session expired. Sign in to continue.58        </p>59      ) : null}6061      <form onSubmit={submit} className="space-y-4" noValidate>62        <Input label="Username" value={username} onChange={(e) => setUsername(e.target.value)} autoComplete="username" autoCapitalize="none" autoCorrect="off" spellCheck={false} placeholder="your username" required />63        <Input label="Password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="current-password" placeholder="your password" required />64        {error ? (65          <p className="rounded-md border border-danger/30 bg-danger/10 px-4 py-3 text-sm text-danger" role="alert">66            {error}67          </p>68        ) : null}69        <Button type="submit" size="lg" className="w-full" loading={busy} disabled={!username || !password}>70          Sign in71        </Button>72      </form>7374      <div className="mt-6 flex flex-col items-center gap-2 text-sm text-fg-3">75        <Link href="/recover" className="tap inline-flex h-9 min-h-0 items-center font-medium text-fg-2 underline-offset-4 hover:underline">76          Forgot your password? Use your recovery code77        </Link>78        <p>79          New to Spinza?{" "}80          <Link href={next !== "/" ? `/register?next=${encodeURIComponent(next)}` : "/register"} className="font-medium text-accent-2 underline-offset-4 hover:underline">81            Create a free account82          </Link>83        </p>84      </div>85    </div>86  );87}8889export default function LoginPage() {90  return (91    <Suspense92      fallback={93        <div className="surface rounded-xl p-6 sm:p-8" aria-busy>94          <Skeleton className="h-7 w-40" />95          <Skeleton className="mt-6 h-12 w-full" />96          <Skeleton className="mt-4 h-12 w-full" />97          <Skeleton className="mt-4 h-12 w-full" />98        </div>99      }100    >101      <LoginForm />102    </Suspense>103  );104}105