feat(auth): enable submit buttons only when form fields are complete
All checks were successful
Deploy Flowstate to VPS / deploy (push) Successful in 47s
All checks were successful
Deploy Flowstate to VPS / deploy (push) Successful in 47s
This commit is contained in:
parent
3be5077178
commit
cf9265c127
2 changed files with 73 additions and 35 deletions
|
|
@ -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"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue