SPB Git

spb/chat-spboucher Public

Private universal chat interface over the OpenRouter ecosystem — 400+ models, branching, streaming, usage tracking. Next.js 16 + SQLite, PWA, deployed on m4m64a at chat.spboucher.ai

TypeScript 78.8% CSS 15.1% JavaScript 4.9% Shell 1.2%
2.1 KB · 73 lines tsx
Raw Blame History
1// Author: Simon-Pierre Boucher2// Contact: contact@spboucher.ai3// Project: chat.spboucher.ai45"use client";67import { useState } from "react";8import { useRouter } from "next/navigation";910export default function LoginPage() {11  const router = useRouter();12  const [username, setUsername] = useState("");13  const [password, setPassword] = useState("");14  const [error, setError] = useState<string | null>(null);15  const [busy, setBusy] = useState(false);1617  async function submit(e: React.FormEvent) {18    e.preventDefault();19    setBusy(true);20    setError(null);21    try {22      const res = await fetch("/api/auth/login", {23        method: "POST",24        headers: { "Content-Type": "application/json" },25        body: JSON.stringify({ username, password }),26      });27      const json = await res.json().catch(() => ({}));28      if (!res.ok) {29        setError(json.error ?? "Sign-in failed. Try again.");30        setBusy(false);31        return;32      }33      router.replace("/");34      router.refresh();35    } catch {36      setError("Could not reach the server. Check your connection and try again.");37      setBusy(false);38    }39  }4041  return (42    <div className="login-page">43      <form className="login-card" onSubmit={submit}>44        <span className="wordmark">45          chat.spboucher<span className="tld">.ai</span>46        </span>47        <p className="sub">Private instrument. Sign in to continue.</p>48        <label htmlFor="username">Username</label>49        <input50          id="username"51          autoComplete="username"52          autoCapitalize="none"53          autoCorrect="off"54          value={username}55          onChange={(e) => setUsername(e.target.value)}56        />57        <label htmlFor="password">Password</label>58        <input59          id="password"60          type="password"61          autoComplete="current-password"62          value={password}63          onChange={(e) => setPassword(e.target.value)}64        />65        <button className="login-btn" type="submit" disabled={busy || !username || !password}>66          {busy ? "Signing in…" : "Sign in"}67        </button>68        {error && <p className="login-error">{error}</p>}69      </form>70    </div>71  );72}73