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

This commit is contained in:
Chneemann 2026-10-12 10:41:09 +02:00
parent 5a387609b4
commit e0f2b22bb3
No known key found for this signature in database
2 changed files with 63 additions and 26 deletions

View file

@ -1,6 +1,6 @@
/** /**
* @file app/(auth)/login/page.tsx * @file app/(auth)/login/page.tsx
* @description Client component rendering the login page, handling form submission, authentication requests, error messaging, autocomplete, and redirection. * @description Client component rendering the login page, handling form submission, authentication requests, error messaging, autocomplete, form validation, and redirection.
*/ */
"use client"; "use client";
@ -11,7 +11,7 @@ import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { loginAsGuest, loginUser } from "@/lib/services/auth.service"; import { loginAsGuest, loginUser } from "@/lib/services/auth.service";
/** Renders the user login interface and manages authentication form state. */ /** Renders the user login interface, manages authentication form state, and enforces input completion for form submission. */
export default function LoginPage() { export default function LoginPage() {
const router = useRouter(); const router = useRouter();
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -19,6 +19,11 @@ export default function LoginPage() {
"credentials" | "guest" | null "credentials" | "guest" | null
>(null); >(null);
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 when user modifies input fields. */
const handleInputChange = () => { const handleInputChange = () => {
if (error) { if (error) {
@ -27,24 +32,22 @@ export default function LoginPage() {
}; };
/** Form submit handler extracting credentials from FormData and delegating to handleSignIn. */ /** Form submit handler extracting credentials from FormData and delegating to handleSignIn. */
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { const handleSubmit = async (e: React.SubmitEvent<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");
}; };
/** Authenticates the user with provided credentials and redirects upon success. */ /** Authenticates the user with provided credentials and redirects upon success. */
const handleSignIn = async ( const handleSignIn = async (
email: string, loginEmail: string,
password: string, loginPass: 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(loginEmail, loginPass);
if (result.error) { if (result.error) {
setError(result.error); setError(result.error);
@ -98,11 +101,7 @@ export default function LoginPage() {
)} )}
{/* Form */} {/* Form */}
<form <form onSubmit={handleSubmit} className="space-y-4">
onSubmit={handleSubmit}
onChange={handleInputChange}
className="space-y-4"
>
<div> <div>
<label <label
htmlFor="email" htmlFor="email"
@ -115,6 +114,11 @@ export default function LoginPage() {
name="email" name="email"
type="email" type="email"
required required
value={email}
onChange={(e) => {
setEmail(e.target.value);
handleInputChange();
}}
placeholder="name@example.com" placeholder="name@example.com"
maxLength={255} maxLength={255}
autoComplete="email" autoComplete="email"
@ -134,6 +138,11 @@ export default function LoginPage() {
name="password" name="password"
type="password" type="password"
required required
value={password}
onChange={(e) => {
setPassword(e.target.value);
handleInputChange();
}}
placeholder="•••••••••••••" placeholder="•••••••••••••"
maxLength={72} maxLength={72}
autoComplete="current-password" autoComplete="current-password"
@ -143,8 +152,8 @@ export default function LoginPage() {
<button <button
type="submit" type="submit"
disabled={loadingType !== null} disabled={loadingType !== null || !isFormValid}
className="w-full bg-accent hover:opacity-90 transition-opacity text-white font-medium py-2.5 rounded-lg text-sm disabled:opacity-50 mt-2 cursor-pointer" className="w-full bg-accent hover:opacity-90 transition-opacity text-white font-medium py-2.5 rounded-lg text-sm disabled:opacity-50 mt-2 cursor-pointer disabled:cursor-not-allowed"
> >
{loadingType === "credentials" ? "Signing in..." : "Sign In"} {loadingType === "credentials" ? "Signing in..." : "Sign In"}
</button> </button>

View file

@ -1,6 +1,6 @@
/** /**
* @file app/(auth)/register/page.tsx * @file app/(auth)/register/page.tsx
* @description Client component rendering the registration page, handling form submissions, client-side validation, error handling, autocomplete, and navigation. * @description Client component rendering the registration page, handling form submissions, client-side validation, form completion checks, error handling, autocomplete, and navigation.
*/ */
"use client"; "use client";
@ -11,12 +11,23 @@ import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { registerUser } from "@/lib/services/auth.service"; import { registerUser } from "@/lib/services/auth.service";
/** Renders the user registration interface and manages sign-up form state. */ /** Renders the user registration interface, manages sign-up form state, and requires all fields to be filled before submission. */
export default function RegisterPage() { export default function RegisterPage() {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const router = useRouter(); const router = useRouter();
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const isFormValid =
username.trim() !== "" &&
email.trim() !== "" &&
password.trim() !== "" &&
confirmPassword.trim() !== "";
/** Clears active error state when user modifies input fields. */ /** Clears active error state when user modifies input fields. */
const handleInputChange = () => { const handleInputChange = () => {
if (error) { if (error) {
@ -25,8 +36,9 @@ export default function RegisterPage() {
}; };
/** Handles user registration form submission, validates form data via auth service, and navigates to summary page on successful registration. */ /** Handles user registration form submission, validates form data via auth service, and navigates to summary page on successful registration. */
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) { async function handleSubmit(event: React.SubmitEvent<HTMLFormElement>) {
event.preventDefault(); event.preventDefault();
if (!isFormValid) return;
setError(null); setError(null);
const formData = new FormData(event.currentTarget); const formData = new FormData(event.currentTarget);
@ -79,11 +91,7 @@ export default function RegisterPage() {
)} )}
{/* Form */} {/* Form */}
<form <form onSubmit={handleSubmit} className="space-y-4">
onSubmit={handleSubmit}
onChange={handleInputChange}
className="space-y-4"
>
<div> <div>
<label <label
htmlFor="username" htmlFor="username"
@ -96,6 +104,11 @@ export default function RegisterPage() {
name="username" name="username"
type="text" type="text"
required required
value={username}
onChange={(e) => {
setUsername(e.target.value);
handleInputChange();
}}
placeholder="username" placeholder="username"
maxLength={50} maxLength={50}
autoComplete="username" autoComplete="username"
@ -115,6 +128,11 @@ export default function RegisterPage() {
name="email" name="email"
type="email" type="email"
required required
value={email}
onChange={(e) => {
setEmail(e.target.value);
handleInputChange();
}}
placeholder="name@example.com" placeholder="name@example.com"
maxLength={255} maxLength={255}
autoComplete="email" autoComplete="email"
@ -134,6 +152,11 @@ export default function RegisterPage() {
name="password" name="password"
type="password" type="password"
required required
value={password}
onChange={(e) => {
setPassword(e.target.value);
handleInputChange();
}}
placeholder="•••••••••••••" placeholder="•••••••••••••"
maxLength={72} maxLength={72}
autoComplete="new-password" autoComplete="new-password"
@ -153,6 +176,11 @@ export default function RegisterPage() {
name="confirmPassword" name="confirmPassword"
type="password" type="password"
required required
value={confirmPassword}
onChange={(e) => {
setConfirmPassword(e.target.value);
handleInputChange();
}}
placeholder="•••••••••••••" placeholder="•••••••••••••"
maxLength={72} maxLength={72}
autoComplete="new-password" autoComplete="new-password"
@ -162,8 +190,8 @@ export default function RegisterPage() {
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading || !isFormValid}
className="w-full bg-accent hover:opacity-90 transition-opacity text-white font-medium py-2.5 rounded-lg text-sm disabled:opacity-50 mt-2 cursor-pointer" className="w-full bg-accent hover:opacity-90 transition-opacity text-white font-medium py-2.5 rounded-lg text-sm disabled:opacity-50 mt-2 cursor-pointer disabled:cursor-not-allowed"
> >
{loading ? "Register..." : "Register"} {loading ? "Register..." : "Register"}
</button> </button>