diff --git a/README.md b/README.md index 81a8c899c..a5ab6240f 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ - Cart handling and checkout with WooCommerce (Cash On Delivery only for now) - Algolia search - Apollo Client with GraphQL +- React Hook Form with form validation and error display - Animations with React-Spring and Animate.css - Loading spinner created with Styled Components - Shows page load progress with Nprogress during navigation @@ -42,8 +43,6 @@ ## TODO - Display product variation name in cart / checkout -- Implement React-hook-form for validation -- Fix bug with changing quantity for variable products - Hide products not in stock - Add better SEO - Add a better README.md diff --git a/components/Checkout/Billing.component.jsx b/components/Checkout/Billing.component.jsx index 56e126bbb..db88c5f1a 100644 --- a/components/Checkout/Billing.component.jsx +++ b/components/Checkout/Billing.component.jsx @@ -1,124 +1,197 @@ -import Error from './Error.component'; +import { useForm } from 'react-hook-form'; + import CheckoutTitle from 'components/Header/CheckoutTitle.component'; -const Billing = ({ input, handleOnChange }) => { - // https://react-hook-form.com/get-started#Quickstart +const Input = ({ + name, + label, + register, + placeholder, + value, + parameters, + type = 'text', + readOnly = false, +}) => ( + <> + <label className="pb-4">{label}</label> + <input + className="w-full px-4 py-2 mt-2 text-base bg-white border border-gray-400 rounded focus:outline-none focus:border-black" + name={name} + placeholder={placeholder} + type="text" + value={value} + ref={register(parameters)} + type={type} + readOnly={readOnly} + /> + </> +); + +const Billing = ({ onSubmit }) => { + const { register, handleSubmit, errors } = useForm(); + return ( <> <section className="relative text-gray-700 body-font"> - <div className="container px-5 py-2 mx-auto"> - <CheckoutTitle title="Betalingsdetaljer" /> - <div className="mx-auto lg:w-1/2 md:w-2/3"> - <div className="flex flex-wrap -m-2"> - <div className="w-1/2 p-2"> - <label className="pb-4">Fornavn</label> - <input - className="w-full px-4 py-2 mt-2 text-base bg-white border border-gray-400 rounded focus:outline-none focus:border-black" - placeholder="Fornavn" - type="text" - onChange={handleOnChange} - value={input.firstName} - name="firstName" - id="first-name" - /> - <Error errors={input.errors} fieldName={'firstName'} /> - </div> - <div className="w-1/2 p-2"> - <label className="pb-4">Etternavn</label> - <input - className="w-full px-4 py-2 mt-2 text-base bg-white border border-gray-400 rounded focus:outline-none focus:border-black" - placeholder="Etternavn" - type="text" - onChange={handleOnChange} - value={input.lastName} - name="lastName" - id="last-name" - /> - <Error errors={input.errors} fieldName={'lastName'} /> - </div> - <div className="w-1/2 p-2"> - <label className="pb-4">Adresse</label> - <input - className="w-full px-4 py-2 mt-2 text-base bg-white border border-gray-400 rounded focus:outline-none focus:border-black" - placeholder="Addresse" - type="text" - onChange={handleOnChange} - value={input.address1} - name="address1" - id="address1" - /> - <Error errors={input.errors} fieldName={'address1'} /> - </div> - <div className="w-1/2 p-2"> - <label className="pb-4">Postnummer</label> - <input - className="w-full px-4 py-2 mt-2 text-base bg-white border border-gray-400 rounded focus:outline-none focus:border-black" - placeholder="Postnummer" - type="text" - onChange={handleOnChange} - value={input.postcode} - name="postcode" - id="post-code" - /> - <Error errors={input.errors} fieldName={'postcode'} /> - </div> - <div className="w-1/2 p-2"> - <label className="pb-4">Sted</label> - <input - className="w-full px-4 py-2 mt-2 text-base bg-white border border-gray-400 rounded focus:outline-none focus:border-black" - placeholder="Sted" - type="text" - onChange={handleOnChange} - value={input.city} - name="city" - id="city" - /> - <Error errors={input.errors} fieldName={'city'} /> - </div> - <div className="w-1/2 p-2"> - <label className="pb-4">Epost</label> - <input - className="w-full px-4 py-2 mt-2 text-base bg-white border border-gray-400 rounded focus:outline-none focus:border-black" - placeholder="Epost" - type="text" - onChange={handleOnChange} - value={input.email} - name="email" - id="email" - /> - <Error errors={input.errors} fieldName={'email'} /> - </div> - <div className="w-1/2 p-2"> - <label className="pb-4">Telefon</label> - <input - className="w-full px-4 py-2 mt-2 text-base bg-white border border-gray-400 rounded focus:outline-none focus:border-black" - placeholder="Telefon" - type="text" - onChange={handleOnChange} - value={input.phone} - name="phone" - id="phone1" - /> - <Error errors={input.errors} fieldName={'phone'} /> - </div> - <div className="w-1/2 p-2"> - <input - className="hidden" - value="bacs" - name="paymentMethod" - type="radio" - checked - readOnly - /> - </div> - <div className="w-full p-2"> - <button className="flex px-4 py-2 mx-auto font-bold bg-white border border-gray-400 border-solid rounded hover:bg-gray-400"> - BESTILL - </button> + <form onSubmit={handleSubmit(onSubmit)}> + <div className="container px-5 py-2 mx-auto"> + <CheckoutTitle title="Betalingsdetaljer" /> + <div className="mx-auto lg:w-1/2 md:w-2/3"> + <div className="flex flex-wrap -m-2"> + <div className="w-1/2 p-2"> + <Input + name="firstName" + placeholder="Fornavn" + label="Fornavn" + register={register} + parameters={{ required: 'Dette feltet er påkrevd' }} + /> + {errors.firstName && ( + <span className="text-red-500"> + FEIL: {errors.firstName.message} + </span> + )} + </div> + <div className="w-1/2 p-2"> + <Input + name="lastName" + placeholder="Etternavn" + label="Etternavn" + register={register} + parameters={{ required: 'Dette feltet er påkrevd' }} + /> + {errors.lastName && ( + <span className="text-red-500"> + FEIL: {errors.lastName.message} + </span> + )} + </div> + <div className="w-1/2 p-2"> + <Input + name="address1" + placeholder="Adresse" + label="Adresse" + register={register} + parameters={{ required: 'Dette feltet er påkrevd' }} + /> + {errors.address1 && ( + <span className="text-red-500"> + FEIL: {errors.address1.message} + </span> + )} + </div> + <div className="w-1/2 p-2"> + <Input + name="postcode" + placeholder="Postnummer" + label="Postnummer" + register={register} + parameters={{ + required: 'Dette feltet er påkrevd', + minLength: { + value: 4, + message: 'Postnummer må være minimum 4 tall', + }, + maxLength: { + value: 4, + message: 'Postnummer må være maksimalt 4 tall', + }, + pattern: { + value: /^[0-9]+$/i, + message: 'Postnummer må bare være tall', + }, + }} + /> + {errors.postcode && ( + <span className="text-red-500"> + FEIL: {errors.postcode.message} + </span> + )} + </div> + <div className="w-1/2 p-2"> + <Input + name="city" + placeholder="Sted" + label="Sted" + register={register} + parameters={{ required: 'Dette feltet er påkrevd' }} + /> + {errors.city && ( + <span className="text-red-500"> + FEIL: {errors.city.message} + </span> + )} + </div> + <div className="w-1/2 p-2"> + <Input + name="email" + placeholder="Epost" + label="Epost" + register={register} + parameters={{ + required: 'Dette feltet er påkrevd', + pattern: { + value: /[^@]+@[^@]+\.[^@]+/i, + message: 'Du må oppgi en gyldig epost', + }, + }} + /> + {errors.email && ( + <span className="text-red-500"> + FEIL: {errors.email.message} + </span> + )} + </div> + <div className="w-1/2 p-2"> + <Input + name="phone" + placeholder="Telefon" + label="Telefon" + register={register} + parameters={{ + required: 'Dette feltet er påkrevd', + minLength: { + value: 8, + message: 'Minimum 8 tall i telefonnummeret', + }, + maxLength: { + value: 8, + message: 'Maksimalt 8 tall i telefonnummeret', + }, + + pattern: { + value: /^[0-9]+$/i, + message: 'Ikke gyldig telefonnummer', + }, + }} + /> + {errors.phone && ( + <span className="text-red-500"> + FEIL: {errors.phone.message} + </span> + )} + </div> + <div className="w-1/2 p-2"> + <Input + name="paymentMethod" + placeholder="paymentMethod" + label="" + type="hidden" + value="cod" + register={register} + checked + readOnly + /> + </div> + <div className="w-full p-2"> + <button className="flex px-4 py-2 mx-auto font-bold bg-white border border-gray-400 border-solid rounded hover:bg-gray-400"> + BESTILL + </button> + </div> </div> </div> </div> - </div> + </form> </section> </> ); diff --git a/components/Checkout/CheckoutForm.component.jsx b/components/Checkout/CheckoutForm.component.jsx index a1653652a..14c0bfa8c 100644 --- a/components/Checkout/CheckoutForm.component.jsx +++ b/components/Checkout/CheckoutForm.component.jsx @@ -17,8 +17,6 @@ import { createCheckoutData, } from 'utils/functions/functions'; -import validateAndSanitizeCheckoutForm from 'utils/validator/checkoutValidator'; - const CheckoutForm = () => { const [cart, setCart] = useContext(AppContext); const [input, setInput] = useState(INITIAL_STATE); @@ -57,42 +55,6 @@ const CheckoutForm = () => { }, }); - /* - * Handle form submit. - * - * @param {Object} event Event Object. - * - * @return {void} - */ - const handleFormSubmit = (event) => { - event.preventDefault(); - const result = validateAndSanitizeCheckoutForm(input); - if (!result.isValid) { - setInput({ ...input, errors: result.errors }); - return; - } - const checkOutData = createCheckoutData(input); - setOrderData(checkOutData); - setRequestError(null); - }; - - /* - * Handle onChange input. - * - * @param {Object} event Event Object. - * - * @return {void} - */ - const handleOnChange = (event) => { - if ('createAccount' === event.target.name) { - const newState = { ...input, [event.target.name]: !input.createAccount }; - setInput(newState); - } else { - const newState = { ...input, [event.target.name]: event.target.value }; - setInput(newState); - } - }; - useEffect(() => { if (null !== orderData) { // Perform checkout mutation when the value for orderData changes. @@ -100,28 +62,32 @@ const CheckoutForm = () => { } }, [orderData]); + const onSubmit = (data) => { + const checkOutData = createCheckoutData(data); + setOrderData(checkOutData); + setRequestError(null); + }; + return ( <> {cart ? ( - <form onSubmit={handleFormSubmit} className=""> - <div className="container mx-auto"> - {/* Order*/} - <OrderDetails cart={cart} /> - {/*Payment Details*/} - <div className=""> - <Billing input={input} handleOnChange={handleOnChange} /> - </div> - {/* Checkout Loading*/} - {checkoutLoading && ( - <div className="text-xl text-center"> - Behandler ordre, vennligst vent ... - <br /> - <LoadingSpinner /> - </div> - )} - {requestError} + <div className="container mx-auto"> + {/* Order*/} + <OrderDetails cart={cart} /> + {/*Payment Details*/} + <div className=""> + <Billing onSubmit={onSubmit} /> </div> - </form> + {/* Checkout Loading*/} + {checkoutLoading && ( + <div className="text-xl text-center"> + Behandler ordre, vennligst vent ... + <br /> + <LoadingSpinner /> + </div> + )} + {requestError} + </div> ) : ( <> {orderCompleted && ( diff --git a/components/Checkout/Error.component.jsx b/components/Checkout/Error.component.jsx deleted file mode 100644 index f5a61ef40..000000000 --- a/components/Checkout/Error.component.jsx +++ /dev/null @@ -1,10 +0,0 @@ -const Error = ( { errors, fieldName } ) => { - - return( - errors && ( errors.hasOwnProperty( fieldName ) ) ? ( - <div className="invalid-feedback d-block">{ errors[fieldName] }</div> - ) : '' - ) -}; - -export default Error; diff --git a/package-lock.json b/package-lock.json index b15e7e3ac..46ce807af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "nextjs-woocommerce", - "version": "1.0.0", + "version": "0.9.8", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/utils/validator/checkoutValidator.js b/utils/validator/checkoutValidator.js index f2c63b5ed..a33a22435 100644 --- a/utils/validator/checkoutValidator.js +++ b/utils/validator/checkoutValidator.js @@ -1,4 +1,9 @@ const validateAndSanitizeCheckoutForm = (data) => { + + console.log(data) + + + return { isValid: true, };