From 4f1056cdcdd74e8aafba79be31975964590d0531 Mon Sep 17 00:00:00 2001 From: yiekheng Date: Sun, 10 May 2026 17:52:35 +0800 Subject: [PATCH] feat(web): /login page with username + password form Server-rendered card-style login. Form posts to loginAction; on failure the client renders the generic 'Invalid username or password' error. Centred, mobile-first, autocomplete-friendly so phone PWAs autofill from the keychain on subsequent logins. --- apps/web/src/app/login/login-form-client.tsx | 67 ++++++++++++++++++++ apps/web/src/app/login/page.tsx | 28 ++++++++ 2 files changed, 95 insertions(+) create mode 100644 apps/web/src/app/login/login-form-client.tsx create mode 100644 apps/web/src/app/login/page.tsx diff --git a/apps/web/src/app/login/login-form-client.tsx b/apps/web/src/app/login/login-form-client.tsx new file mode 100644 index 0000000..190cd25 --- /dev/null +++ b/apps/web/src/app/login/login-form-client.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { Loader2Icon, LockIcon } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { loginAction } from "@/actions/auth"; + +export function LoginFormClient({ next }: { next: string }) { + const [pending, start] = useTransition(); + const [error, setError] = useState(null); + + function handle(formData: FormData) { + formData.append("next", next); + start(async () => { + setError(null); + const r = await loginAction(formData); + // On success, the action redirects (no return). If we land here, + // something failed and `r` is the error shape. + if (r && !r.ok) setError(r.error); + }); + } + + return ( +
+
+ + +
+
+ + +
+ {error && ( +
{error}
+ )} + +

+ First time? Run ./scripts/set-password.sh <username>{" "} + in your tools container. +

+
+ ); +} diff --git a/apps/web/src/app/login/page.tsx b/apps/web/src/app/login/page.tsx new file mode 100644 index 0000000..2d29e57 --- /dev/null +++ b/apps/web/src/app/login/page.tsx @@ -0,0 +1,28 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { LoginFormClient } from "./login-form-client"; + +export const metadata = { + title: "Sign in", +}; + +interface PageProps { + searchParams: Promise<{ next?: string }>; +} + +export default async function LoginPage({ searchParams }: PageProps) { + const sp = await searchParams; + const next = sp.next ?? "/"; + + return ( +
+ + + Sign in + + + + + +
+ ); +}