Source: components/UserForm.jsx

import { useState, useMemo, useEffect } from "react";
import { ToastContainer } from "react-toastify";
import * as validators from "../validators";
import { useUsers } from "../contexts/UserContext";
import "react-toastify/dist/ReactToastify.css";
import "./UserForm.css";

/**
 * Initial state for form data
 * @constant {Object}
 * @property {string} firstName - Empty first name field
 * @property {string} lastName - Empty last name field
 * @property {string} email - Empty email field
 * @property {string} birthDate - Empty birth date field
 * @property {string} postalCode - Empty postal code field
 * @property {string} city - Empty city field
 */
const INITIAL_FORM_DATA = {
  firstName: "",
  lastName: "",
  email: "",
  birthDate: "",
  postalCode: "",
  city: "",
};

/**
 * Initial state for validation errors
 * @constant {Object}
 * @property {string} firstName - Empty error message for first name
 * @property {string} lastName - Empty error message for last name
 * @property {string} email - Empty error message for email
 * @property {string} birthDate - Empty error message for birth date
 * @property {string} postalCode - Empty error message for postal code
 * @property {string} city - Empty error message for city
 */
const INITIAL_ERRORS = {
  firstName: "",
  lastName: "",
  email: "",
  birthDate: "",
  postalCode: "",
  city: "",
};

/**
 * Initial state for field touch status (tracks if user has interacted with field)
 * @constant {Object}
 * @property {boolean} firstName - First name field not yet touched
 * @property {boolean} lastName - Last name field not yet touched
 * @property {boolean} email - Email field not yet touched
 * @property {boolean} birthDate - Birth date field not yet touched
 * @property {boolean} postalCode - Postal code field not yet touched
 * @property {boolean} city - City field not yet touched
 */
const INITIAL_TOUCHED = {
  firstName: false,
  lastName: false,
  email: false,
  birthDate: false,
  postalCode: false,
  city: false,
};

/**
 * UserForm Component - Registration form with real-time validation
 *
 * @component
 * @description Form component that collects user information (first name, last name, email,
 * birth date, postal code, city) with immediate validation feedback.
 *
 * @param {Object} props - Component props
 * @param {Function} [props.onUserRegistered] - Optional callback function called after successful registration with user data
 * @returns {JSX.Element} The rendered form component
 */
const UserForm = ({ onUserRegistered }) => {
  const [formData, setFormData] = useState(INITIAL_FORM_DATA);
  const [errors, setErrors] = useState(INITIAL_ERRORS);
  const [touched, setTouched] = useState(INITIAL_TOUCHED);

  const { users } = useUsers();

  /**
   * Validates a single field using the appropriate validator function
   *
   * @param {string} fieldName - Name of the field to validate
   * @param {string} value - Value to validate
   * @returns {string} Error message if validation fails, empty string otherwise
   */
  const validateField = (fieldName, value) => {
    try {
      switch (fieldName) {
        case "firstName":
          validators.validateIdentity(value);
          return "";

        case "lastName":
          validators.validateIdentity(value);
          return "";

        case "email":
          validators.validateEmailComplete(value, users);
          return "";

        case "birthDate":
          if (!value) {
            return "Birth date is required";
          }
          const date = new Date(value);
          validators.validateAge(date);
          return "";

        case "postalCode":
          validators.validatePostalCode(value);
          return "";

        case "city":
          validators.validateIdentity(value);
          return "";

        /* istanbul ignore next */
        default:
          return "";
      }
    } catch (error) {
      return error.message;
    }
  };

  /**
   * Detects browser autofill and triggers validation
   * Chrome and other browsers fill forms without triggering React onChange events
   * This effect polls the DOM to detect autofilled values and validates them
   */
  useEffect(() => {
    const fields = ["firstName", "lastName", "email", "birthDate", "postalCode", "city"];

    const checkAutofill = () => {
      fields.forEach((fieldName) => {
        const input = document.getElementById(fieldName);
        if (input && input.value && input.value !== formData[fieldName]) {
          setFormData((prev) => ({ ...prev, [fieldName]: input.value }));

          setTouched((prev) => ({ ...prev, [fieldName]: true }));

          const errorMessage = validateField(fieldName, input.value);
          setErrors((prev) => ({ ...prev, [fieldName]: errorMessage }));
        }
      });
    };

    const timer1 = setTimeout(checkAutofill, 100);
    const timer2 = setTimeout(checkAutofill, 500);

    const handleAutoComplete = () => {
      setTimeout(checkAutofill, 50);
    };

    const inputRefs = [];

    fields.forEach((fieldName) => {
      const input = document.getElementById(fieldName);
      if (input) {
        input.addEventListener("input", handleAutoComplete);
        inputRefs.push(input);
      }
    });

    return () => {
      clearTimeout(timer1);
      clearTimeout(timer2);
      inputRefs.forEach((input) => {
        input.removeEventListener("input", handleAutoComplete);
      });
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  /**
   * Handles input change events with real-time validation
   *
   * @param {Event} e - Input change event
   */
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData((prev) => ({ ...prev, [name]: value }));

    if (touched[name]) {
      const errorMessage = validateField(name, value);
      setErrors((prev) => ({ ...prev, [name]: errorMessage }));
    }
  };

  /**
   * Handles blur event (focus out) to trigger validation
   *
   * @param {Event} e - Blur event
   */
  const handleBlur = (e) => {
    const { name, value } = e.target;
    setTouched((prev) => ({ ...prev, [name]: true }));

    const errorMessage = validateField(name, value);
    setErrors((prev) => ({ ...prev, [name]: errorMessage }));
  };

  /**
   * Checks if the entire form is valid
   * Validates all fields are filled and checks for any validation errors
   *
   * @returns {boolean} True if form is valid, false otherwise
   */
  const isFormValid = () => {
    const allFieldsFilled = Object.values(formData).every((value) => value.trim() !== "");
    if (!allFieldsFilled) return false;

    const fieldValidations = [
      { name: "firstName", value: formData.firstName },
      { name: "lastName", value: formData.lastName },
      { name: "email", value: formData.email },
      { name: "birthDate", value: formData.birthDate },
      { name: "postalCode", value: formData.postalCode },
      { name: "city", value: formData.city },
    ];

    for (const field of fieldValidations) {
      const error = validateField(field.name, field.value);
      if (error) return false;
    }

    return true;
  };

  /**
   * Handles form submission
   * Saves data to localStorage, displays success message, and resets form
   *
   * @param {Event} e - Form submit event
   */
  const handleSubmit = (e) => {
    e.preventDefault();

    if (isFormValid()) {
      const birthDate = new Date(formData.birthDate);
      const today = new Date();
      let age = today.getFullYear() - birthDate.getFullYear();
      const monthDiff = today.getMonth() - birthDate.getMonth();

      if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
        age--;
      }

      const userData = {
        firstName: formData.firstName,
        lastName: formData.lastName,
        email: formData.email,
        age: age,
        postalCode: formData.postalCode,
        city: formData.city,
        timestamp: new Date().toISOString(),
      };

      onUserRegistered(userData);

      setFormData(INITIAL_FORM_DATA);
      setErrors(INITIAL_ERRORS);
      setTouched(INITIAL_TOUCHED);
    }
  };

  /**
   * Memoized button disabled state
   * Recalculates only when formData changes to avoid unnecessary localStorage reads
   */
  const isButtonDisabled = useMemo(() => {
    const allFieldsFilled = Object.values(formData).every((value) => value.trim() !== "");
    if (!allFieldsFilled) return true;

    const hasErrors = Object.values(errors).some((error) => error !== "");
    if (hasErrors) return true;

    return false;
  }, [formData, errors]);

  return (
    <div className="user-form-container">
      <ToastContainer />
      <form className="user-form" onSubmit={handleSubmit} noValidate aria-label="User registration form">
        <h1 data-cy="form-title">Registration Form</h1>

        <div className="form-group">
          <label htmlFor="firstName">First Name *</label>
          <input
            type="text"
            id="firstName"
            name="firstName"
            value={formData.firstName}
            onChange={handleChange}
            onBlur={handleBlur}
            className={errors.firstName && touched.firstName ? "error" : ""}
            aria-invalid={errors.firstName && touched.firstName ? "true" : "false"}
            aria-describedby={errors.firstName && touched.firstName ? "firstName-error" : undefined}
            data-cy="input-firstName"
          />
          {errors.firstName && touched.firstName && (
            <span id="firstName-error" className="error-message" role="alert" data-cy="error-firstName">
              {errors.firstName}
            </span>
          )}
        </div>

        <div className="form-group">
          <label htmlFor="lastName">Last Name *</label>
          <input
            type="text"
            id="lastName"
            name="lastName"
            value={formData.lastName}
            onChange={handleChange}
            onBlur={handleBlur}
            className={errors.lastName && touched.lastName ? "error" : ""}
            aria-invalid={errors.lastName && touched.lastName ? "true" : "false"}
            aria-describedby={errors.lastName && touched.lastName ? "lastName-error" : undefined}
            data-cy="input-lastName"
          />
          {errors.lastName && touched.lastName && (
            <span id="lastName-error" className="error-message" role="alert" data-cy="error-lastName">
              {errors.lastName}
            </span>
          )}
        </div>

        <div className="form-group">
          <label htmlFor="email">Email *</label>
          <input
            type="email"
            id="email"
            name="email"
            value={formData.email}
            onChange={handleChange}
            onBlur={handleBlur}
            className={errors.email && touched.email ? "error" : ""}
            aria-invalid={errors.email && touched.email ? "true" : "false"}
            aria-describedby={errors.email && touched.email ? "email-error" : undefined}
            data-cy="input-email"
          />
          {errors.email && touched.email && (
            <span id="email-error" className="error-message" role="alert" data-cy="error-email">
              {errors.email}
            </span>
          )}
        </div>

        <div className="form-group">
          <label htmlFor="birthDate">Birth date *</label>
          <input
            type="date"
            id="birthDate"
            name="birthDate"
            value={formData.birthDate}
            onChange={handleChange}
            onBlur={handleBlur}
            className={errors.birthDate && touched.birthDate ? "error" : ""}
            aria-invalid={errors.birthDate && touched.birthDate ? "true" : "false"}
            aria-describedby={errors.birthDate && touched.birthDate ? "birthDate-error" : undefined}
            data-cy="input-birthDate"
          />
          {errors.birthDate && touched.birthDate && (
            <span id="birthDate-error" className="error-message" role="alert" data-cy="error-birthDate">
              {errors.birthDate}
            </span>
          )}
        </div>

        <div className="form-group">
          <label htmlFor="postalCode">Postal Code *</label>
          <input
            type="text"
            id="postalCode"
            name="postalCode"
            value={formData.postalCode}
            onChange={handleChange}
            onBlur={handleBlur}
            className={errors.postalCode && touched.postalCode ? "error" : ""}
            aria-invalid={errors.postalCode && touched.postalCode ? "true" : "false"}
            aria-describedby={errors.postalCode && touched.postalCode ? "postalCode-error" : undefined}
            maxLength="5"
            data-cy="input-postalCode"
          />
          {errors.postalCode && touched.postalCode && (
            <span id="postalCode-error" className="error-message" role="alert" data-cy="error-postalCode">
              {errors.postalCode}
            </span>
          )}
        </div>

        <div className="form-group">
          <label htmlFor="city">City *</label>
          <input
            type="text"
            id="city"
            name="city"
            value={formData.city}
            onChange={handleChange}
            onBlur={handleBlur}
            className={errors.city && touched.city ? "error" : ""}
            aria-invalid={errors.city && touched.city ? "true" : "false"}
            aria-describedby={errors.city && touched.city ? "city-error" : undefined}
            data-cy="input-city"
          />
          {errors.city && touched.city && (
            <span id="city-error" className="error-message" role="alert" data-cy="error-city">
              {errors.city}
            </span>
          )}
        </div>

        <button type="submit" className="submit-button" disabled={isButtonDisabled} aria-label="Submit the form" data-cy="submit-button">
          Submit
        </button>
      </form>
    </div>
  );
};

export default UserForm;