Repository: chec/commercejs-chopchop-demo Branch: main Commit: 9ff079217bdc Files: 54 Total size: 81.5 KB Directory structure: gitextract_jvvou8o_/ ├── .babelrc ├── .chec.json ├── .editorconfig ├── .gitignore ├── LICENSE.md ├── README.md ├── components/ │ ├── Breadcrumbs.js │ ├── Button.js │ ├── Cart.js │ ├── CartItem.js │ ├── CartSummary.js │ ├── Checkout/ │ │ ├── AddressFields.js │ │ ├── BillingForm.js │ │ ├── Checkout.js │ │ ├── CheckoutSummary.js │ │ ├── ExtraFieldsForm.js │ │ ├── OrderSummary.js │ │ ├── ShippingForm.js │ │ ├── Success.js │ │ └── index.js │ ├── Footer.js │ ├── Form/ │ │ ├── FormCheckbox.js │ │ ├── FormError.js │ │ ├── FormInput.js │ │ ├── FormSelect.js │ │ ├── FormTextarea.js │ │ └── index.js │ ├── Header.js │ ├── Layout.js │ ├── Modal.js │ ├── Product.js │ ├── ProductAttributes.js │ ├── ProductGrid.js │ ├── ProductImages.js │ ├── ProductList.js │ ├── RelatedProducts.js │ └── VariantPicker.js ├── context/ │ ├── cart.js │ ├── checkout.js │ ├── modal.js │ └── theme.js ├── lib/ │ ├── commerce.js │ └── gtag.js ├── next.config.js ├── package.json ├── pages/ │ ├── _app.js │ ├── _document.js │ ├── index.js │ └── products/ │ └── [permalink].js ├── postcss.config.js ├── seeds/ │ ├── assets.json │ ├── categories.json │ └── products.json └── tailwind.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": ["next/babel"], "plugins": ["inline-react-svg"] } ================================================ FILE: .chec.json ================================================ { "npm": "npm", "buildScripts": ["seed", "dev"], "dotenv": { "NODE_ENV": "development", "NEXT_PUBLIC_CHEC_PUBLIC_API_KEY": "%chec_pkey%", "CHEC_API_URL": "%chec_api_url%", "CHEC_SECRET_KEY": "%chec_skey%" } } ================================================ FILE: .editorconfig ================================================ # For more information about the properties used in this file, # please see the EditorConfig documentation: # http://editorconfig.org [*] charset = utf-8 end_of_line = lf indent_size = 4 indent_style = space insert_final_newline = true trim_trailing_whitespace = true [*.{yml,js,json,css,scss,feature,eslintrc}] indent_size = 2 ================================================ FILE: .gitignore ================================================ # Build output .next # Environment variables .env # Dependency directories node_modules # Logs npm-debug.log* yarn-debug.log* yarn-error.log* # Misc .vercel ================================================ FILE: LICENSE.md ================================================ Copyright (c) 2021 Chec Platform LLC, All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3) Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================

A Next.js, Commerce.js, Stripe, and Vercel powered, open source storefront, cart and checkout experience.

License
commercejs.com | @commercejs | Slack

View demo

View demo

## Introduction ChopChop is our beautifully designed, elegantly developed demo store and starter kit that sells fine tools for thoughtful cooks. We’ve created a premium brand with a commerce experience to match. Read more about this resource on the [Commerce.js blog](https://commercejs.com/blog/chopchop-nextjs-starter-commerce/). ## 🥞 ChopChop Stack * [Next.js](https://nextjs.org/) * [Commerce.js](https://commercejs.com) * [Tailwind CSS](https://tailwindcss.com/) * [Stripe](https://stripe.com) * [Vercel](https://vercel.com/) ## Live demo Check out https://commercejs-chopchop-demo.vercel.app to see this project in action. ## Getting started ### Prerequisites - IDE or code editor of your choice - Node (v12 or higher) - NPM or Yarn - Optional: [Chec CLI](https://github.com/chec/cli) ### Use the Chec CLI You can use the [Chec CLI](https://github.com/chec/cli) to quickly and easily install demo stores like this, and also to install sample data into your account. To install the Chec CLI, run `npm install -g @chec/cli` (or `yarn global add @chec/cli`). * Navigate to your projects folder: `cd ~/Projects` * Install the ChopChop demo store: `chec demo-store` * Choose "Chop Chop demo store (Next.js)" from the list * This will install dependencies and sample data, then start your dev server * Stop the server, open `.env` and add your `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` for using Stripe, then re-run `npm run dev` * Open [http://localhost:3000](http://localhost:3000) and get started! ### Manual installation Clone the project, then get started by installing the dependencies, creating a `.env` file, and starting the dev server. ``` npm install cp .env.example .env npm run dev ``` Once the server is running, open it up in your browser, start editing the code, and enjoy! ### Sample data This repository comes with some sample products and images for you to use if you want to get up and running quickly. To install sample data, first copy `.env.example` to `.env`, then edit `.env` and fill out the following variables: * `NEXT_PUBLIC_CHEC_PUBLIC_API_KEY`: Your Chec public/sandbox API key, available from the Chec Dashboard under Developers > API keys * `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`: Your Stripe test publishable key, available from the Stripe dashboard * `CHEC_SECRET_KEY`: Your Chec secret API key, used for seeding * `NEXT_PUBLIC_GA_TRACKING_ID`: Set this with your Google Analytics ID if you want to enable GA. Once this is done, save and close your file. You can now run the seeder to install sample data: ``` npm run seed ... ✔ Completed seeding Added: 3 categories 6 products 9 assets ``` And you're ready to go! ### Deploying to Vercel (with one click) The one-click deploy allows you to add the Vercel application to your GitHub account to clone this repository and deploy it automatically. Be sure to go to [Vercel](https://vercel.com/signup) and sign up for an account with Github, GitLab, or GitBucket before clicking the deploy button. [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/project?template=https://github.com/chec/commercejs-chopchop-demo) Please make sure that you enter the required environment variables listed above during deployment. #### Caveats for sample data To make your ChopChop experience even better, there are a couple of things you can do that are not included with the sample data: * **Add related products:** Go into the [Chec Dashboard](https://dashboard.chec.io) and set related products for each of your new products. This helps to provide upsell suggestions on your website. * **Set up shipping rates:** Also in the dashboard, set up some shipping zones and rates in Settings > Shipping, then enable them on each of your products. This will enable the "Shipping" checkout screen, and allow you to charge shipping for your customers as well. ## Customizations and Extendability - Integrate another payment gateway, either one of our supported gateways or your own with our [manual gateway API](https://commercejs.com/docs/guides/manual-payment-integration) - Integrate with the Google Calendar API to automatically add ticketed items to a customer’s calendars - Suggest products from other sources based on items purchased, i.e. a book on knife skills if you buy the knife set - Add [Algolia](https://www.algolia.com/) for integrated search - Add additional modules to the checkout flow to handle other content types, like booking a time to pickup in-store purchases - Integrate with a headless CMS to make the content editable - Create a customers login section using our [customers endpoint](https://commercejs.com/docs/api/#customers) - Use webhooks to deliver SMS notifications about orders ## License This project is licensed under [BSD-3-Clause](LICENSE.md). ## ⚠️ Note ### This repository is no longer maintained However, we will accept issue reports and contributions for this repository. See the [contribute to the commerce community](https://commercejs.com/docs/community/contribute) page for more information on how to contribute to our open source projects. For update-to-date APIs, please check the latest version of the [API documentation](https://commercejs.com/docs/api/). ================================================ FILE: components/Breadcrumbs.js ================================================ import { useCheckoutState } from "../context/checkout"; // TODO: Build array of crumbs dynamically from available steps function Breadcrumbs({ inCart }) { const { currentStep, extrafields } = useCheckoutState(); if (inCart) { return Shopping Bag; } if (currentStep === "success") { return Order received; } return (
{currentStep === "extrafields" && ( <> Shopping Bag Booking Shipping Payment )} {currentStep === "shipping" && ( <> Shopping Bag {extrafields.length > 0 && ( <> Booking )} Shipping Payment )} {currentStep === "billing" && ( <> Shopping Bag {extrafields.length > 0 && ( <> Booking )} Shipping Payment )}
); } export default Breadcrumbs; ================================================ FILE: components/Button.js ================================================ import cc from "classcat"; import { useThemeState } from "../context/theme"; const buttonStyle = (theme) => { switch (theme) { case "kitchen-sink-journal-chopchop-shop": return "bg-clementine text-black"; case "walnut-cooks-tools-chopchop-shop": return "bg-tumbleweed text-black"; case "essential-knife-set-chopchop-shop": return "bg-hawkes-blue text-black"; case "private-cooking-class-chopchop-shop": return "bg-asparagus text-black"; case "ceramic-dutch-oven-chopchop-shop": return "bg-goldenrod text-black"; default: return "bg-white-rock"; } }; function Button({ className, ...props }) { const theme = useThemeState(); const buttonClass = cc([ "appearance-none border-none py-0.5 px-1.5 md:px-2 text-lg md:text-xl rounded transition focus:outline-none", buttonStyle(theme), className, ]); if (props.href) return ; return )} ); } ================================================ FILE: components/CartItem.js ================================================ import React from "react"; import Image from "next/image"; import { toast } from "react-toastify"; import { commerce } from "../lib/commerce"; import { useCartDispatch } from "../context/cart"; function CartItem({ id, media, name, quantity, line_total, selected_options }) { const { setCart } = useCartDispatch(); const hasVariants = selected_options.length >= 1; const handleUpdateCart = ({ cart }) => { setCart(cart); return cart; }; const handleRemoveItem = () => commerce.cart .remove(id) .then(handleUpdateCart) .then(({ subtotal }) => toast( `${name} has been removed from your cart. Your new subtotal is now ${subtotal.formatted_with_symbol}` ) ); const decrementQuantity = () => { quantity > 1 ? commerce.cart .update(id, { quantity: quantity - 1 }) .then(handleUpdateCart) .then(({ subtotal }) => toast( `1 "${name}" has been removed from your cart. Your new subtotal is now ${subtotal.formatted_with_symbol}` ) ) : handleRemoveItem(); }; const incrementQuantity = () => commerce.cart .update(id, { quantity: quantity + 1 }) .then(handleUpdateCart) .then(({ subtotal }) => toast( `Another "${name}" has been added to your cart. Your new subtotal is now ${subtotal.formatted_with_symbol}` ) ); return (
{name}

{name}

{hasVariants && (

{selected_options.map(({ option_name }, index) => ( {index ? `, ${option_name}` : option_name} ))}

)}
{line_total.formatted_with_symbol}
Quantity: {quantity}
); } export default CartItem; ================================================ FILE: components/CartSummary.js ================================================ import { useCartState } from "../context/cart"; import { useModalDispatch } from "../context/modal"; function CartSummary() { const { total_unique_items } = useCartState(); const { openModal } = useModalDispatch(); return ( ); } export default CartSummary; ================================================ FILE: components/Checkout/AddressFields.js ================================================ import React from "react"; import { FormInput, FormSelect } from "../Form"; function AddressFields({ prefix = "", countries = {}, subdivisions = {} }) { const reducer = ([code, name]) => ({ value: code, label: name, }); const formattedCountries = subdivisions ? Object.entries(countries).map(reducer) : []; const formattedSubdivisions = subdivisions ? Object.entries(subdivisions).map(reducer) : []; return (
); } export default AddressFields; ================================================ FILE: components/Checkout/BillingForm.js ================================================ import { useState, useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { useDebounce } from "use-debounce"; import { CardNumberElement, CardExpiryElement, CardCvcElement, } from "@stripe/react-stripe-js"; import { commerce } from "../../lib/commerce"; import { useCheckoutState, useCheckoutDispatch } from "../../context/checkout"; import { FormCheckbox, FormInput, FormError } from "../Form"; import AddressFields from "./AddressFields"; const style = { base: { "::placeholder": { color: "rgba(21,7,3,0.3)", }, color: "#150703", fontSize: "16px", fontFamily: `Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`, iconColor: "#6B7280", }, }; function BillingForm() { const [countries, setCountries] = useState(); const [subdivisions, setSubdivisions] = useState(); const methods = useFormContext(); const { collects } = useCheckoutState(); const { setError } = useCheckoutDispatch(); const { watch, setValue, clearErrors } = methods; const shipping = watch("shipping"); const [watchCountry] = useDebounce(watch("billing.country"), 600); useEffect(() => { fetchCountries(); }, []); useEffect(() => { watchCountry && fetchSubdivisions(watchCountry); }, [watchCountry]); const fetchCountries = async () => { try { const { countries } = await commerce.services.localeListCountries(); setCountries(countries); } catch (err) { // noop } }; const fetchSubdivisions = async (country) => { try { const { subdivisions } = await commerce.services.localeListSubdivisions( country ); setSubdivisions(subdivisions); } catch (err) { // noop } }; const onStripeChange = () => { clearErrors("stripe"); setError(null); }; return (
Billing address {collects?.shipping_address && ( checked && setValue("billing", shipping) } /> )}
Payment
); } export default BillingForm; ================================================ FILE: components/Checkout/Checkout.js ================================================ import { useState, useEffect } from "react"; import { useForm, FormProvider } from "react-hook-form"; import { useStripe, useElements } from "@stripe/react-stripe-js"; import { useCartDispatch } from "../../context/cart"; import { useCheckoutState, useCheckoutDispatch } from "../../context/checkout"; import ExtraFieldsForm from "./ExtraFieldsForm"; import ShippingForm from "./ShippingForm"; import BillingForm from "./BillingForm"; import Success from "./Success"; import CheckoutSummary from "./CheckoutSummary"; import OrderSummary from "./OrderSummary"; import LoadingSVG from "../../svg/loading.svg"; function Checkout({ cartId }) { const [order, setOrder] = useState(); const { reset: resetCart } = useCartDispatch(); const { currentStep, id, live } = useCheckoutState(); const { generateToken, setCurrentStep, nextStepFrom, capture, setProcessing, setError: setCheckoutError, } = useCheckoutDispatch(); const methods = useForm({ shouldUnregister: false, }); const { handleSubmit, setError } = methods; const stripe = useStripe(); const elements = useElements(); useEffect(() => { generateToken(cartId); }, [cartId]); const captureOrder = async (values) => { setProcessing(true); const { customer, shipping, billing: { firstname, lastname, region: county_state, ...billing }, ...data } = values; const { error, paymentMethod } = await stripe.createPaymentMethod({ type: "card", card: elements.getElement("cardNumber"), billing_details: { name: `${billing.firstname} ${billing.lastname}`, email: customer.email, }, }); if (error) { setError("stripe", { type: "manual", message: error.message }); setProcessing(false); return; } const checkoutPayload = { ...data, customer: { ...customer, firstname, lastname, }, ...(shipping && { shipping: { ...shipping, name: `${shipping.firstname} ${shipping.lastname}`, }, }), billing: { ...billing, name: `${firstname} ${lastname}`, county_state, }, }; try { const newOrder = await capture({ ...checkoutPayload, payment: { gateway: "stripe", stripe: { payment_method_id: paymentMethod.id, }, }, }); handleOrderSuccess(newOrder); setProcessing(false); } catch (res) { if ( res.statusCode !== 402 || res.data.error.type !== "requires_verification" ) { setCheckoutError(res.data.error.message); setProcessing(false); return; } const { error, paymentIntent } = await stripe.handleCardAction( res.data.error.param ); if (error) { setError("stripe", { type: "manual", message: error.message }); setProcessing(false); return; } try { const newOrder = await capture({ ...checkoutPayload, payment: { gateway: "stripe", stripe: { payment_intent_id: paymentIntent.id, }, }, }); handleOrderSuccess(newOrder); setProcessing(false); } catch (err) { setError("stripe", { type: "manual", message: error.message }); setProcessing(false); } } }; const handleOrderSuccess = (order) => { setOrder(order); setCurrentStep("success"); resetCart(); }; const onSubmit = (values) => { if (currentStep === "billing") return captureOrder(values); return setCurrentStep(nextStepFrom(currentStep)); }; if (!id) return (

Preparing checkout

); return (
{currentStep === "extrafields" && } {currentStep === "shipping" && } {currentStep === "billing" && } {currentStep === "success" && } {order ? : }
); } export default Checkout; ================================================ FILE: components/Checkout/CheckoutSummary.js ================================================ import cc from "classcat"; import { useCheckoutState } from "../../context/checkout"; import Button from "../Button"; function CheckoutSummary({ subtotal, tax, shipping, line_items = [], total }) { const { processing, error } = useCheckoutState(); const count = line_items.length; return (
    {subtotal &&
  1. Subtotal: {subtotal.formatted_with_symbol}
  2. } {tax &&
  3. Tax: {tax.amount.formatted_with_symbol}
  4. } {shipping && (
  5. Shipping: {shipping.price.formatted_with_symbol}
  6. )} {total && (
  7. Total: {total.formatted_with_symbol}, {count}{" "} {count === 1 ? "item" : "items"}
  8. )}
{error && {error}}
); } export default CheckoutSummary; ================================================ FILE: components/Checkout/ExtraFieldsForm.js ================================================ import { useEffect } from "react"; import { useCheckoutState, useCheckoutDispatch } from "../../context/checkout"; import { FormInput, FormCheckbox, FormTextarea } from "../Form"; // TODO: Update the UI to be built from the API // once products have extrafields that can be of // any type. E.g. "date", "textarea" // const fields = { // BookingDate: (props) => ( // <> // // // ), // }; function ExtraFieldsForm() { const { extrafields } = useCheckoutState(); const { setCurrentStep, nextStepFrom } = useCheckoutDispatch(); useEffect(() => { if (extrafields.length === 0) { setCurrentStep(nextStepFrom("extrafields")); } return null; }, [extrafields]); return (
Booking
Lesson Plan

Thanks for joining us for a lesson! Let us know what you might like to learn or cook below.

{extrafields.map(({ id }) => { const computedFieldName = `extrafields.${id}`; return ( ); })}
); } export default ExtraFieldsForm; ================================================ FILE: components/Checkout/OrderSummary.js ================================================ import Button from "../Button"; function CheckoutSummary({ has, fulfillment, order }) { const { subtotal, tax, shipping, line_items, total } = order; const count = line_items.length; return (
  1. Subtotal: {subtotal.formatted_with_symbol}
  2. {tax &&
  3. Tax: {tax.amount.formatted_with_symbol}
  4. } {shipping && (
  5. Shipping: {shipping.price.formatted_with_symbol}
  6. )} {total && (
  7. Total: {total.formatted_with_symbol}, {count}{" "} {count === 1 ? "item" : "items"}
  8. )}
{has.digital_fulfillment && (
{fulfillment.digital.downloads.map((download, index) => (
{download.packages.map(({ access_link, name }, index) => ( ))}
))}
)}
); } export default CheckoutSummary; ================================================ FILE: components/Checkout/ShippingForm.js ================================================ import { useState, useEffect } from "react"; import { useFormContext } from "react-hook-form"; import { useDebounce } from "use-debounce"; import { commerce } from "../../lib/commerce"; import { useCheckoutState, useCheckoutDispatch } from "../../context/checkout"; import AddressFields from "./AddressFields"; import { FormCheckbox as FormRadio, FormError } from "../Form"; function ShippingForm() { const { id } = useCheckoutState(); const { setShippingMethod } = useCheckoutDispatch(); const [countries, setCountries] = useState(); const [subdivisions, setSubdivisions] = useState(); const [shippingOptions, setShippingOptions] = useState([]); const methods = useFormContext(); const { watch, setValue } = methods; const [watchCountry] = useDebounce(watch("shipping.country"), 600); const watchSubdivision = watch("shipping.region"); useEffect(() => { fetchCountries(id); }, []); useEffect(() => { setValue("shipping.region", ""); if (watchCountry) { fetchSubdivisions(id, watchCountry); fetchShippingOptions(id, watchCountry); } }, [watchCountry]); useEffect(() => { if (watchSubdivision) { fetchShippingOptions(id, watchCountry, watchSubdivision); } }, [watchSubdivision]); const fetchCountries = async (checkoutId) => { try { const { countries } = await commerce.services.localeListShippingCountries( checkoutId ); setCountries(countries); } catch (err) { // noop } }; const fetchSubdivisions = async (checkoutId, countryCode) => { try { const { subdivisions, } = await commerce.services.localeListShippingSubdivisions( checkoutId, countryCode ); setSubdivisions(subdivisions); } catch (err) { // noop } }; const fetchShippingOptions = async (checkoutId, country, region) => { if (!checkoutId && !country) return; setValue("fulfillment.shipping_method", null); try { const shippingOptions = await commerce.checkout.getShippingOptions( checkoutId, { country, ...(region && { region }), } ); setShippingOptions(shippingOptions); if (shippingOptions.length === 1) { const [shippingOption] = shippingOptions; setValue("fulfillment.shipping_method", shippingOption.id); selectShippingMethod(shippingOption.id); } } catch (err) { // noop } }; const onShippingSelect = ({ target: { value } }) => selectShippingMethod(value); const selectShippingMethod = async (optionId) => { try { await setShippingMethod(optionId, watchCountry, watchSubdivision); } catch (err) { // noop } }; return (
Shipping address
Shipping
{watchCountry ? ( <>
{shippingOptions.map(({ id, description, price }) => (
))}
) : (

Please enter your address to fetch shipping options

)}
); } export default ShippingForm; ================================================ FILE: components/Checkout/Success.js ================================================ import Image from 'next/image'; function Success({ has }) { return (

Thanks!

{has.digital_fulfillment ? "You’ll receive an email with your receipt, and a backup link to re-download your purchase" : "You’ll receive an email with your receipt, and tracking information."}

ChopChop doesn't exist!

...if it did, we'd offer you a 100% real store credit, but since it doesn't, we'd love for you to check out commercejs.com and the repo for this store instead.

Thanks for visiting 'Chop chop' what are you waiting for
); } export default Success; ================================================ FILE: components/Checkout/index.js ================================================ export { default } from "./Checkout"; ================================================ FILE: components/Footer.js ================================================ import Link from "next/link"; import LogoSVG from "../svg/logo.svg"; import CommerceJsSVG from "../svg/commercejs.svg"; function Footer() { return ( ); } export default Footer; ================================================ FILE: components/Form/FormCheckbox.js ================================================ import { useFormContext } from "react-hook-form"; function FormCheckbox({ label, children, name, required = false, validation = {}, ...props }) { const { register } = useFormContext(); const isRequired = required ? typeof required === "boolean" ? `${label || name} is required` : required : false; return (
); } export default FormCheckbox; ================================================ FILE: components/Form/FormError.js ================================================ import cc from "classcat"; import { ErrorMessage } from "@hookform/error-message"; function FormError({ className, ...props }) { return (
( {message} )} />
); } export default FormError; ================================================ FILE: components/Form/FormInput.js ================================================ import { useFormContext } from "react-hook-form"; import FormError from "./FormError"; function FormInput({ label, name, type = "text", required = false, validation = {}, ...props }) { const { register } = useFormContext(); const isRequired = required ? `${label || name} is required` : false; return (
); } export default FormInput; ================================================ FILE: components/Form/FormSelect.js ================================================ import { useFormContext } from "react-hook-form"; import Chevron from "../../svg/chevron.svg"; import FormError from "./FormError"; function FormSelect({ label, name, options, required = false, validation = {}, placeholder, ...props }) { const { register } = useFormContext(); const isRequired = required ? `${label || name} is required` : false; return (
); } export default FormSelect; ================================================ FILE: components/Form/FormTextarea.js ================================================ import { useFormContext } from "react-hook-form"; import FormError from "./FormError"; function FormTextarea({ label, name, required = false, validation = {}, ...props }) { const { register } = useFormContext(); const isRequired = required ? `${label || name} is required` : false; return (