mega-plant/components/auth/AuthForms.tsx
Dave 2594dead84 feat: add user access and improve plant dashboard
Add PocketBase login, first-sign-in password changes, account settings,
and user activation, reactivation and deletion with self-removal guards.
Protect application pages and APIs and keep private pages out of PWA
caches.

Resolve plant locations from site assignments and improve map controls,
profit chart views, navigation and mobile table layouts.

Validation: production build and TypeScript passed; 28 unit tests passed.
Backend access integration tests and mobile verification are documented.
2026-10-06 04:02:08 +02:00

73 lines
4.3 KiB
TypeScript

"use client";
import { useActionState, useEffect, useState } from "react";
import { ArrowRight, UserPlus, LoaderCircle } from "lucide-react";
import { loginAction, changePasswordAction, addUserAction, type FormState } from "@/app/actions/auth";
import { STARTER_PASSWORD } from "@/lib/auth-policy";
import { PasswordField, inputClass } from "./PasswordField";
function Feedback({ state }: { state: FormState }) {
return <>
{state.error && <p role="alert" className="rounded-xl border border-[#D1495B]/20 bg-[#D1495B]/5 p-3 text-sm leading-relaxed text-[#D1495B]">{state.error}</p>}
{state.success && <p role="status" className="break-words rounded-xl border border-[#00798C]/20 bg-[#00798C]/5 p-3 text-sm leading-relaxed text-[#00798C] dark:text-[#00A6BE]">{state.success}</p>}
</>;
}
function Submit({ pending, children, adding = false }: { pending: boolean; children: React.ReactNode; adding?: boolean }) {
return <button type="submit" disabled={pending}
className="flex min-h-12 w-full items-center justify-center gap-2 rounded-xl bg-[#30638E] px-5 py-3 text-sm font-semibold text-white transition hover:bg-[#003D5B] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#30638E] disabled:cursor-wait disabled:opacity-60">
{pending ? <LoaderCircle className="h-4 w-4 animate-spin" /> : adding ? <UserPlus className="h-4 w-4" /> : null}
{pending ? "Please wait…" : children}
{!pending && !adding && <ArrowRight className="h-4 w-4" />}
</button>;
}
export function LoginForm() {
const [state, action, pending] = useActionState(loginAction, {});
const [email, setEmail] = useState("");
return <form action={action} className="space-y-5">
<div>
<label htmlFor="email" className="mb-2 block text-sm font-medium">Email address</label>
<input id="email" name="email" type="email" autoComplete="username" value={email} onChange={(event) => setEmail(event.target.value)} required maxLength={254} className={inputClass} placeholder="you@company.com" />
</div>
<PasswordField name="password" label="Password" autoComplete="current-password" />
<Feedback state={state} />
<Submit pending={pending}>Sign in</Submit>
<p className="text-center text-xs leading-relaxed text-gray-500 dark:text-gray-400">Use the account your team has created for you.</p>
</form>;
}
export function ChangePasswordForm() {
const [state, action, pending] = useActionState(changePasswordAction, {});
return <form action={action} className="space-y-5">
<PasswordField name="password" label="Your new password" autoComplete="new-password" minLength={8}
hint="At least 8 characters. No uppercase letters, numbers or special characters required." />
<PasswordField name="confirmation" label="Confirm new password" autoComplete="new-password" minLength={8} />
<Feedback state={state} />
<Submit pending={pending}>Save password & continue</Submit>
</form>;
}
export function AddUserForm() {
const [state, action, pending] = useActionState(addUserAction, {});
const [name, setName] = useState("");
const [email, setEmail] = useState("");
useEffect(() => { if (state.success) { setName(""); setEmail(""); } }, [state]);
return <form action={action} className="space-y-5">
<div>
<label htmlFor="name" className="mb-2 block text-sm font-medium">Full name</label>
<input id="name" name="name" value={name} onChange={(event) => setName(event.target.value)} autoComplete="off" required className={inputClass} placeholder="Person's name" />
</div>
<div>
<label htmlFor="email" className="mb-2 block text-sm font-medium">Email address</label>
<input id="email" name="email" type="email" value={email} onChange={(event) => setEmail(event.target.value)} autoComplete="off" required maxLength={254} className={inputClass} placeholder="person@company.com" />
</div>
<div>
<label htmlFor="starter" className="mb-2 block text-sm font-medium">Starter password</label>
<input id="starter" value={STARTER_PASSWORD} readOnly className={`${inputClass} font-mono bg-gray-50`} aria-describedby="starter-hint" />
<p id="starter-hint" className="mt-2 text-xs leading-relaxed text-gray-500 dark:text-gray-400">They'll choose their own password when they first sign in.</p>
</div>
<Feedback state={state} />
<Submit pending={pending} adding>Add user</Submit>
</form>;
}