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%
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