TryGhost/Ghost

View on GitHub
apps/signup-form/src/components/pages/FormView.tsx

Summary

Maintainability
A
35 mins
Test Coverage
import React, {FormEventHandler} from 'react';
import {ReactComponent as LoadingIcon} from '../../../assets/icons/spinner.svg';
import {useAppContext} from '../../AppContext';

export const FormView: React.FC<FormProps & {
    isMinimal: boolean
    title?: string
    description?: string
    icon?: string
    backgroundColor?: string
    textColor?: string
}> = ({isMinimal, title, description, icon, backgroundColor, textColor, error, ...formProps}) => {
    if (isMinimal) {
        return (
            <>
                <Form error={error} isMinimal={isMinimal} {...formProps} />
                {error && <p className='text-red-500' data-testid="error-message">{error}</p>}
            </>
        );
    }

    return (
        <div
            className='flex h-[100vh] flex-col items-center justify-center px-4 sm:px-6 md:px-10'
            data-testid="wrapper"
            style={{backgroundColor, color: textColor}}
        >
            {icon && <img alt={title} className='mb-2 h-[64px] w-auto' src={icon}/>}
            {title && <h1 className="text-center text-lg font-bold sm:text-xl md:text-2xl lg:text-3xl">{title}</h1>}
            {description && <p className='mb-4 text-center font-medium md:mb-5'>{description}</p>}
            <div className='relative w-full max-w-[440px]'>
                <Form error={error} {...formProps} />
                <p className={`h-5 w-full text-left text-red-500 ${error ? 'visible' : 'invisible'}`} data-testid="error-message">{error}</p>
            </div>

        </div>
    );
};

type FormProps = {
    buttonColor?: string
    buttonTextColor?: string
    isMinimal?: boolean
    loading: boolean
    success: boolean
    error?: string
    onSubmit: (values: { email: string }) => void
}

const Form: React.FC<FormProps> = ({isMinimal, loading, success, error, buttonColor, buttonTextColor, onSubmit}) => {
    const [email, setEmail] = React.useState('');
    const {t} = useAppContext();

    const submitHandler: FormEventHandler<HTMLFormElement> = (e) => {
        e.preventDefault();
        onSubmit({email});
    };

    // The complicated transitions are here so that we animate visibility: hidden (step-start/step-end), which is required for screen readers to know what is visible (they ignore opacity: 0)
    return (
        <>
            <form className={`relative flex w-full rounded-[.5rem] border bg-white p-[3px] text-grey-900 transition hover:border-grey-400 focus-visible:border-grey-500 ${error ? '!border-red-500' : 'border-grey-300'}`} onSubmit={submitHandler}>
                <input
                    className={`w-full px-2 py-1 focus-visible:outline-none disabled:bg-white xs:p-2`}
                    data-testid="input"
                    disabled={loading || success}
                    placeholder={t('Your email address')}
                    type="text"
                    value={email}
                    onChange={e => setEmail(e.target.value)}
                />
                <button
                    className='my-auto grid h-7 touch-manipulation grid-cols-[1fr] items-center rounded-[.3rem] px-2 font-medium text-white xs:h-[3rem] xs:px-3'
                    data-testid="button"
                    disabled={loading || success}
                    style={{backgroundColor: buttonColor, color: buttonTextColor}}
                    type='submit'
                >
                    <span className={`col-start-1 row-start-1 whitespace-nowrap ${loading || success ? '[opacity_200ms,visibility_200ms_step-end] invisible opacity-0' : 'opacity-1 [opacity_200ms,visibility_200ms_step-start] visible'}`}>{t('Subscribe')}</span>
                    {isMinimal && <span className={`col-start-1 row-start-1 whitespace-nowrap ${loading || !success ? 'invisible mx-[-40px] opacity-0 [transition:margin_300ms,opacity_200ms,visibility_200ms_step-end]' : 'opacity-1 visible mx-0 [transition:margin_300ms,opacity_200ms,visibility_200ms_step-start]'}`}>{t('Email sent')}</span>}
                    <span className={`inset-0 col-start-1 row-start-1 flex items-center justify-center transition-opacity duration-200 ${!loading ? '[opacity_200ms,visibility_200ms_step-end] invisible opacity-0' : 'opacity-1 [opacity_200ms,visibility_200ms_step-start] visible' }`}><LoadingIcon /></span>
                </button>
            </form>
        </>
    );
};