Source: components/PersonForm.jsx

/**
 * @file PersonForm.jsx
 * Contains the form component for registering a new person.
 */

import React, { useState } from 'react'
import { validatePerson, validateAge, validateZipCode, validateCity, validateName, validateEmail } from '../domain/validator'
import { errorMessages, getErrorMessage } from '../utils/errorMessages'
import { ToastContainer, toast } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'
import './PersonForm.css'

/**
 * PersonForm Component
 *
 * A form for registering a person. Handles field validation and submission.
 * Displays validation errors and success notifications using react-toastify.
 *
 * @module PersonForm
 * @component
 *
 * @param {Object} props - The component props.
 * @param {function(Object): Promise<void>} props.addPerson - Async callback function to add a person object to parent state or storage.
 *
 * @returns {JSX.Element} The rendered registration form
 */
export default function PersonForm({ addPerson }) {
    const [form, setForm] = useState({
        firstName: '',
        lastName: '',
        email: '',
        birthDate: '',
        zip: '',
        city: '',
    })

    const [errors, setErrors] = useState({})

    /**
     * Validates a single form field and updates the component's error state.
     *
     * @module PersonForm
     * @function validateField
     * @private
     * @param {string} name - The name of the field to validate.
     * @param {string} value - The current value of the field.
     */
    const validateField = (name, value) => {
        if (!value) {
            setErrors(prev => ({ ...prev, [name]: '' }))
            return
        }

        try {
            switch (name) {
                case 'firstName':
                    validateName(value, 'firstName')
                    break
                case 'lastName':
                    validateName(value, 'lastName')
                    break
                case 'email':
                    validateEmail(value)
                    break
                case 'zip':
                    validateZipCode(value)
                    break
                case 'city':
                    validateCity(value)
                    break
                case 'birthDate':
                    validateAge(new Date(value))
                    break
            }
            setErrors(prev => ({ ...prev, [name]: '' }))
        } catch (err) {
            setErrors(prev => ({ ...prev, [name]: err.message }))
        }
    }

    /**
     * Handles changes in form input fields.
     * Updates the form state and triggers validation for the changed field.
     *
     * @module PersonForm
     * @function handleChange
     * @private
     * @param {React.ChangeEvent<HTMLInputElement>} e - The input change event.
     */
    const handleChange = (e) => {
        const { name, value } = e.target
        setForm(prev => ({ ...prev, [name]: value }))
        validateField(name, value)
    }

    /**
     * Handles the form submission.
     * Validates the entire person object, calls the addPerson API, 
     * and displays a success or error toast based on the result.
     *
     * @async
     * @module PersonForm
     * @function handleSubmit
     * @private
     * @param {React.FormEvent<HTMLFormElement>} e - The form submission event.
     */
    const handleSubmit = async (e) => {
        e.preventDefault()
        try {
            const person = {
                firstName: form.firstName,
                lastName: form.lastName,
                email: form.email,
                birthDate: new Date(form.birthDate),
                zip: form.zip,
                city: form.city
            }
            validatePerson(person)
            await addPerson(person)
            toast.success("Enregistré avec succès !", {
                toastId: "success-toast"
            });
            setForm({ firstName: '', lastName: '', email: '', birthDate: '', zip: '', city: '' })
            setErrors({})
        } catch (err) {
            /* istanbul ignore next */
            let key = 'form'
            /* istanbul ignore next */
            if (err.message.includes('SERVER_ERROR')) {
                toast.error(errorMessages.SERVER_ERROR, { toastId: "server-error-toast", className: 'toast-server-error' })
            } else if (err.message.includes('FIRST_NAME')) key = 'firstName'
            else if (err.message.includes('LAST_NAME')) key = 'lastName'
            else if (err.message.includes('INVALID_EMAIL') || err.message.includes('EMAIL_ALREADY_EXISTS')) key = 'email'
            else if (err.message.includes('ZIP')) key = 'zip'
            else if (err.message.includes('CITY')) key = 'city'
            else if (err.message.includes('UNDERAGE') || err.message.includes('FUTURE_DATE')) key = 'birthDate'

            /* istanbul ignore next */
            setErrors({ [key]: err.message })
        }
    }

    const isDisabled =
        !form.firstName || !form.lastName || !form.email || !form.birthDate || !form.zip || !form.city || Object.values(errors).some(Boolean)

    return (
        <div className="person-form-container">
            <div className="card">
                <h2>Formulaire d'inscription</h2>
                <form onSubmit={handleSubmit} className="person-form">
                    <div className="form-group">
                        <input
                            data-cy="firstName"
                            name="firstName"
                            aria-label="firstName"
                            placeholder="Prénom"
                            value={form.firstName}
                            onChange={handleChange}
                            onBlur={(e) => validateField('firstName', e.target.value)}
                        />
                        {errors.firstName && <span className="error">{getErrorMessage(errors.firstName)}</span>}
                    </div>

                    <div className="form-group">
                        <input
                            data-cy="lastName"
                            name="lastName"
                            aria-label="lastName"
                            placeholder="Nom"
                            value={form.lastName}
                            onChange={handleChange}
                            onBlur={(e) => validateField('lastName', e.target.value)}
                        />
                        {errors.lastName && <span className="error">{getErrorMessage(errors.lastName)}</span>}
                    </div>

                    <div className="form-group">
                        <input
                            data-cy="birthDate"
                            type="date"
                            name="birthDate"
                            data-testid="birthDate"
                            value={form.birthDate}
                            onChange={handleChange}
                            onBlur={(e) => validateField('birthDate', e.target.value)}
                        />
                        {errors.birthDate && <span className="error">{getErrorMessage(errors.birthDate)}</span>}
                    </div>

                    <div className="form-group">
                        <input
                            data-cy="zip"
                            name="zip"
                            aria-label="zip"
                            placeholder="Code Postal"
                            value={form.zip}
                            onChange={handleChange}
                            onBlur={(e) => validateField('zip', e.target.value)}
                        />
                        {errors.zip && <span className="error">{getErrorMessage(errors.zip)}</span>}
                    </div>

                    <div className="form-group">
                        <input
                            data-cy="city"
                            name="city"
                            aria-label="city"
                            placeholder="Ville"
                            value={form.city}
                            onChange={handleChange}
                            onBlur={(e) => validateField('city', e.target.value)}
                        />
                        {errors.city && <span className="error">{getErrorMessage(errors.city)}</span>}
                    </div>

                    <div className="form-group">
                        <input
                            data-cy="email"
                            name="email"
                            aria-label="email"
                            placeholder="Email"
                            value={form.email}
                            onChange={handleChange}
                            onBlur={(e) => validateField('email', e.target.value)}
                        />
                        {errors.email && <span className="error">{getErrorMessage(errors.email)}</span>}
                    </div>

                    <button data-cy="submit" type="submit" disabled={isDisabled}>
                        Soumettre
                    </button>
                </form>
            </div>
            <ToastContainer
                position="top-right"
                autoClose={3000}
                hideProgressBar={false}
                newestOnTop
                closeOnClick
                rtl={false}
                pauseOnFocusLoss
                draggable
                pauseOnHover
            />
        </div>)
}