feat(auth): enable submit buttons only when form fields are complete
All checks were successful
Deploy Waveform to VPS / deploy (push) Successful in 1m11s
All checks were successful
Deploy Waveform to VPS / deploy (push) Successful in 1m11s
This commit is contained in:
parent
5a387609b4
commit
e0f2b22bb3
2 changed files with 63 additions and 26 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue