feat(auth): enable submit buttons only when form fields are complete
All checks were successful
Deploy Flowstate to VPS / deploy (push) Successful in 47s

This commit is contained in:
Chneemann 2026-10-13 04:48:30 +02:00
parent 3be5077178
commit cf9265c127
No known key found for this signature in database
2 changed files with 73 additions and 35 deletions

View file

@ -1,6 +1,6 @@
/** /**
* @file app/(auth)/login/page.tsx * @file app/(auth)/login/page.tsx
* @description Client component providing a user login interface utilizing the auth service and ActionButton. * @description Client component providing a user login interface utilizing the auth service, ActionButton, and form completion tracking.
*/ */
"use client"; "use client";
@ -14,7 +14,7 @@ import { Loader2, UserCheck, KeyRound } from "lucide-react";
/** /**
* Renders the login page containing the authentication form, error handling, * Renders the login page containing the authentication form, error handling,
* and navigation links for user sign-in. * input completion checks, and navigation links for user sign-in.
* *
* @returns {JSX.Element} The rendered login page component. * @returns {JSX.Element} The rendered login page component.
*/ */
@ -25,13 +25,24 @@ export default function LoginPage() {
"credentials" | "guest" | null "credentials" | "guest" | null
>(null); >(null);
// Controlled form state tracking for input completion validation
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const isFormValid = email.trim() !== "" && password.trim() !== "";
/** /**
* Clears active error state when user modifies input fields. * Clears active error state and updates local state values when user modifies input fields.
*
* @param {React.ChangeEvent<HTMLInputElement>} e - The input change event.
*/ */
const handleInputChange = () => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (error) { if (error) {
setError(null); setError(null);
} }
const { name, value } = e.target;
if (name === "email") setEmail(value);
if (name === "password") setPassword(value);
}; };
/** /**
@ -39,19 +50,19 @@ export default function LoginPage() {
* Redirects to the summary page upon success or sets an error message on failure. * Redirects to the summary page upon success or sets an error message on failure.
* *
* @async * @async
* @param {string} email - The email address for login. * @param {string} emailAddress - The email address for login.
* @param {string} password - The account password. * @param {string} pass - The account password.
* @param {"credentials" | "guest"} type - The authentication trigger source type. * @param {"credentials" | "guest"} type - The authentication trigger source type.
*/ */
const handleSignIn = async ( const handleSignIn = async (
email: string, emailAddress: string,
password: string, pass: string,
type: "credentials" | "guest", type: "credentials" | "guest",
) => { ) => {
setError(null); setError(null);
setLoadingType(type); setLoadingType(type);
const result = await loginUser(email, password); const result = await loginUser(emailAddress, pass);
if (result.error) { if (result.error) {
setError(result.error); setError(result.error);
@ -65,13 +76,11 @@ export default function LoginPage() {
* Handles traditional credential-based form submissions. * Handles traditional credential-based form submissions.
* *
* @async * @async
* @param {React.SubmitEvent<HTMLFormElement>} e - The form submission event. * @param {React.FormEvent<HTMLFormElement>} e - The form submission event.
*/ */
const handleSubmit = async (e: React.SubmitEvent<HTMLFormElement>) => { const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
const formData = new FormData(e.currentTarget); if (!isFormValid) return;
const email = formData.get("email") as string;
const password = formData.get("password") as string;
await handleSignIn(email, password, "credentials"); await handleSignIn(email, password, "credentials");
}; };
@ -134,11 +143,7 @@ export default function LoginPage() {
</div> </div>
)} )}
<form <form onSubmit={handleSubmit} className="space-y-4">
onSubmit={handleSubmit}
onChange={handleInputChange}
className="space-y-4"
>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label <label
htmlFor="email" htmlFor="email"
@ -150,7 +155,9 @@ export default function LoginPage() {
id="email" id="email"
name="email" name="email"
type="email" type="email"
value={email}
placeholder="name@example.com" placeholder="name@example.com"
onChange={handleInputChange}
maxLength={255} maxLength={255}
autoComplete="email" autoComplete="email"
required required
@ -169,7 +176,9 @@ export default function LoginPage() {
id="password" id="password"
name="password" name="password"
type="password" type="password"
value={password}
placeholder="••••••••••••" placeholder="••••••••••••"
onChange={handleInputChange}
maxLength={72} maxLength={72}
autoComplete="current-password" autoComplete="current-password"
required required
@ -180,7 +189,7 @@ export default function LoginPage() {
<ActionButton <ActionButton
type="submit" type="submit"
variant="primary" variant="primary"
disabled={loadingType !== null} disabled={loadingType !== null || !isFormValid}
icon={loadingType === "credentials" ? Loader2 : KeyRound} icon={loadingType === "credentials" ? Loader2 : KeyRound}
className="w-full sm:w-full" className="w-full sm:w-full"
> >

View file

@ -1,6 +1,6 @@
/** /**
* @file app/(auth)/register/page.tsx * @file app/(auth)/register/page.tsx
* @description Client component providing a user registration interface with client-side validation, autocomplete, and ActionButton integration. * @description Client component providing a user registration interface with client-side validation, field completion checks, autocomplete, and ActionButton integration.
*/ */
"use client"; "use client";
@ -15,7 +15,7 @@ import { JSX } from "react/jsx-runtime";
/** /**
* Renders the registration page featuring a sign-up form, client-side validation, * Renders the registration page featuring a sign-up form, client-side validation,
* error feedback, and routing logic to the summary view upon successful account creation. * input completion tracking, error feedback, and routing logic upon successful account creation.
* *
* @returns {JSX.Element} The rendered registration page component. * @returns {JSX.Element} The rendered registration page component.
*/ */
@ -24,28 +24,51 @@ export default function RegisterPage(): JSX.Element {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const router = useRouter(); const router = useRouter();
// Controlled form state tracking for input completion validation
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const isFormValid =
firstName.trim() !== "" &&
lastName.trim() !== "" &&
email.trim() !== "" &&
password.trim() !== "" &&
confirmPassword.trim() !== "";
/** /**
* Clears active error state when user modifies input fields. * Clears active error state and updates corresponding input values on user modification.
*
* @param {React.ChangeEvent<HTMLInputElement>} e - The input change event.
*/ */
const handleInputChange = () => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (error) { if (error) {
setError(null); setError(null);
} }
const { name, value } = e.target;
if (name === "firstName") setFirstName(value);
if (name === "lastName") setLastName(value);
if (name === "email") setEmail(value);
if (name === "password") setPassword(value);
if (name === "confirmPassword") setConfirmPassword(value);
}; };
/** /**
* Handles user registration form submission, validates form data via auth service, * Handles user registration form submission, validates password equality, delegates registration
* and navigates to summary page on successful registration. * to the auth service, and navigates to the summary page upon success.
* *
* @async * @async
* @function handleSubmit * @param {React.FormEvent<HTMLFormElement>} event - The form submission event.
* @param {React.SubmitEvent<HTMLFormElement>} event - The form submission event.
* @returns {Promise<void>} Resolves when registration handling completes. * @returns {Promise<void>} Resolves when registration handling completes.
*/ */
async function handleSubmit( async function handleSubmit(
event: React.SubmitEvent<HTMLFormElement>, event: React.FormEvent<HTMLFormElement>,
): Promise<void> { ): Promise<void> {
event.preventDefault(); event.preventDefault();
if (!isFormValid) return;
setError(null); setError(null);
const formData = new FormData(event.currentTarget); const formData = new FormData(event.currentTarget);
@ -109,11 +132,7 @@ export default function RegisterPage(): JSX.Element {
</div> </div>
)} )}
<form <form onSubmit={handleSubmit} className="space-y-4">
onSubmit={handleSubmit}
onChange={handleInputChange}
className="space-y-4"
>
<div className="grid grid-cols-2 gap-2.5"> <div className="grid grid-cols-2 gap-2.5">
<div className="space-y-1"> <div className="space-y-1">
<label <label
@ -126,7 +145,9 @@ export default function RegisterPage(): JSX.Element {
id="firstName" id="firstName"
name="firstName" name="firstName"
type="text" type="text"
value={firstName}
placeholder="John" placeholder="John"
onChange={handleInputChange}
maxLength={50} maxLength={50}
autoComplete="given-name" autoComplete="given-name"
required required
@ -145,7 +166,9 @@ export default function RegisterPage(): JSX.Element {
id="lastName" id="lastName"
name="lastName" name="lastName"
type="text" type="text"
value={lastName}
placeholder="Doe" placeholder="Doe"
onChange={handleInputChange}
maxLength={50} maxLength={50}
autoComplete="family-name" autoComplete="family-name"
required required
@ -165,7 +188,9 @@ export default function RegisterPage(): JSX.Element {
id="email" id="email"
name="email" name="email"
type="email" type="email"
value={email}
placeholder="name@example.com" placeholder="name@example.com"
onChange={handleInputChange}
maxLength={255} maxLength={255}
autoComplete="email" autoComplete="email"
required required
@ -184,7 +209,9 @@ export default function RegisterPage(): JSX.Element {
id="password" id="password"
name="password" name="password"
type="password" type="password"
value={password}
placeholder="••••••••••••" placeholder="••••••••••••"
onChange={handleInputChange}
maxLength={72} maxLength={72}
autoComplete="new-password" autoComplete="new-password"
required required
@ -203,7 +230,9 @@ export default function RegisterPage(): JSX.Element {
id="confirmPassword" id="confirmPassword"
name="confirmPassword" name="confirmPassword"
type="password" type="password"
value={confirmPassword}
placeholder="••••••••••••" placeholder="••••••••••••"
onChange={handleInputChange}
maxLength={72} maxLength={72}
autoComplete="new-password" autoComplete="new-password"
required required
@ -214,7 +243,7 @@ export default function RegisterPage(): JSX.Element {
<ActionButton <ActionButton
type="submit" type="submit"
variant="primary" variant="primary"
disabled={loading} disabled={loading || !isFormValid}
icon={loading ? Loader2 : UserPlus} icon={loading ? Loader2 : UserPlus}
className="w-full sm:w-full" className="w-full sm:w-full"
> >