├── .babelrc
├── .chec.json
├── .editorconfig
├── .env.example
├── .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-lock.json
├── package.json
├── pages
├── _app.js
├── _document.js
├── index.js
└── products
│ └── [permalink].js
├── postcss.config.js
├── public
├── checkout
│ └── doesntexist.svg
└── product-attributes
│ ├── 2-lovely-hours.svg
│ ├── 41-delicious-recipes.svg
│ ├── 5-fine-pieces.svg
│ ├── 6-piece-set.svg
│ ├── 77-pages.svg
│ ├── actual-magick.svg
│ ├── an-actually-good-bread-knife.svg
│ ├── cap.svg
│ ├── clean-easily.svg
│ ├── diam.svg
│ ├── dishwasher-safe.svg
│ ├── free-book-mark.svg
│ ├── glossy-paper.svg
│ ├── hand-bound.svg
│ ├── hand-carved-walnut.svg
│ ├── high-carbon-steel.svg
│ ├── high.svg
│ ├── hot-tips.svg
│ ├── learn-knife-skills.svg
│ ├── made-in-france.svg
│ ├── made-in-germany.svg
│ ├── made-in-new-york.svg
│ ├── magnetic-holder.svg
│ ├── pdf-included.svg
│ ├── quality-scoops.svg
│ ├── stain-resistant.svg
│ ├── stop-crying-over-onions.svg
│ ├── tappered-grip.svg
│ ├── thanks.svg
│ ├── url-or-irl.svg
│ └── will-outlive-you.svg
├── seeds
├── assets.json
├── categories.json
└── products.json
├── svg
├── chevron.svg
├── commercejs.svg
├── loading.svg
├── logo.svg
└── quality-badge.svg
└── tailwind.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": ["inline-react-svg"]
4 | }
--------------------------------------------------------------------------------
/.chec.json:
--------------------------------------------------------------------------------
1 | {
2 | "npm": "npm",
3 | "buildScripts": ["seed", "dev"],
4 | "dotenv": {
5 | "NODE_ENV": "development",
6 | "NEXT_PUBLIC_CHEC_PUBLIC_API_KEY": "%chec_pkey%",
7 | "CHEC_API_URL": "%chec_api_url%",
8 | "CHEC_SECRET_KEY": "%chec_skey%"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # For more information about the properties used in this file,
2 | # please see the EditorConfig documentation:
3 | # http://editorconfig.org
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | indent_size = 4
9 | indent_style = space
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [*.{yml,js,json,css,scss,feature,eslintrc}]
14 | indent_size = 2
15 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_CHEC_PUBLIC_API_KEY=pk_test_193037b865ee81e89ff82cb93aa448630d5614b9b0098
2 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_mvn44h2BYsLB1qwhqRAXMkOT
3 | NEXT_PUBLIC_GA_TRACKING_ID=
4 | # Secret key is used with chec/seeder to access your Chec account to seed it with sample data
5 | CHEC_SECRET_KEY=
6 | CHEC_API_URL=https://api.chec.io
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Build output
2 | .next
3 |
4 | # Environment variables
5 | .env
6 |
7 | # Dependency directories
8 | node_modules
9 |
10 | # Logs
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 |
15 | # Misc
16 | .vercel
17 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 Chec Platform LLC, All rights reserved.
2 |
3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4 |
5 | 1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6 | 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.
7 | 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.
8 |
9 | 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.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | A Next.js, Commerce.js, Stripe, and Vercel powered, open source storefront, cart and checkout experience.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | commercejs.com | @commercejs | Slack
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | ## Introduction
27 |
28 | 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/).
29 |
30 |
31 | ## 🥞 ChopChop Stack
32 |
33 | * [Next.js](https://nextjs.org/)
34 | * [Commerce.js](https://commercejs.com)
35 | * [Tailwind CSS](https://tailwindcss.com/)
36 | * [Stripe](https://stripe.com)
37 | * [Vercel](https://vercel.com/)
38 |
39 | ## Live demo
40 |
41 | Check out https://commercejs-chopchop-demo.vercel.app to see this project in action.
42 |
43 | ## Getting started
44 |
45 | ### Prerequisites
46 |
47 | - IDE or code editor of your choice
48 | - Node (v12 or higher)
49 | - NPM or Yarn
50 | - Optional: [Chec CLI](https://github.com/chec/cli)
51 |
52 | ### Use the Chec CLI
53 |
54 | You can use the [Chec CLI](https://github.com/chec/cli) to quickly and easily install demo stores like this, and also
55 | to install sample data into your account. To install the Chec CLI, run `npm install -g @chec/cli` (or `yarn global add @chec/cli`).
56 |
57 | * Navigate to your projects folder: `cd ~/Projects`
58 | * Install the ChopChop demo store: `chec demo-store`
59 | * Choose "Chop Chop demo store (Next.js)" from the list
60 | * This will install dependencies and sample data, then start your dev server
61 | * Stop the server, open `.env` and add your `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` for using Stripe, then re-run `npm run dev`
62 | * Open [http://localhost:3000](http://localhost:3000) and get started!
63 |
64 | ### Manual installation
65 |
66 | Clone the project, then get started by installing the dependencies, creating a `.env` file, and starting the dev server.
67 |
68 | ```
69 | npm install
70 | cp .env.example .env
71 | npm run dev
72 | ```
73 |
74 | Once the server is running, open it up in your browser, start editing the code, and enjoy!
75 |
76 | ### Sample data
77 |
78 | This repository comes with some sample products and images for you to use if you want to get up and running quickly.
79 |
80 | To install sample data, first copy `.env.example` to `.env`, then edit `.env` and fill out the
81 | following variables:
82 |
83 | * `NEXT_PUBLIC_CHEC_PUBLIC_API_KEY`: Your Chec public/sandbox API key, available from the Chec Dashboard under
84 | Developers > API keys
85 | * `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`: Your Stripe test publishable key, available from the Stripe dashboard
86 | * `CHEC_SECRET_KEY`: Your Chec secret API key, used for seeding
87 | * `NEXT_PUBLIC_GA_TRACKING_ID`: Set this with your Google Analytics ID if you want to enable GA.
88 |
89 | Once this is done, save and close your file. You can now run the seeder to install sample data:
90 |
91 | ```
92 | npm run seed
93 | ...
94 | ✔ Completed seeding
95 | Added:
96 | 3 categories
97 | 6 products
98 | 9 assets
99 | ```
100 |
101 | And you're ready to go!
102 |
103 | ### Deploying to Vercel (with one click)
104 |
105 | 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.
106 |
107 | [](https://vercel.com/new/project?template=https://github.com/chec/commercejs-chopchop-demo)
108 |
109 | Please make sure that you enter the required environment variables listed above during deployment.
110 |
111 | #### Caveats for sample data
112 |
113 | To make your ChopChop experience even better, there are a couple of things you can do that are not included with
114 | the sample data:
115 |
116 | * **Add related products:** Go into the [Chec Dashboard](https://dashboard.chec.io) and set related products for each
117 | of your new products. This helps to provide upsell suggestions on your website.
118 | * **Set up shipping rates:** Also in the dashboard, set up some shipping zones and rates in Settings > Shipping, then
119 | enable them on each of your products. This will enable the "Shipping" checkout screen, and allow you to charge
120 | shipping for your customers as well.
121 |
122 | ## Customizations and Extendability
123 |
124 | - 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)
125 | - Integrate with the Google Calendar API to automatically add ticketed items to a customer’s calendars
126 | - Suggest products from other sources based on items purchased, i.e. a book on knife skills if you buy the knife set
127 | - Add [Algolia](https://www.algolia.com/) for integrated search
128 | - Add additional modules to the checkout flow to handle other content types, like booking a time to pickup in-store purchases
129 | - Integrate with a headless CMS to make the content editable
130 | - Create a customers login section using our [customers endpoint](https://commercejs.com/docs/api/#customers)
131 | - Use webhooks to deliver SMS notifications about orders
132 |
133 | ## License
134 |
135 | This project is licensed under [BSD-3-Clause](LICENSE.md).
136 |
137 | ## ⚠️ Note
138 |
139 | ### This repository is no longer maintained
140 | 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/).
141 |
--------------------------------------------------------------------------------
/components/Breadcrumbs.js:
--------------------------------------------------------------------------------
1 | import { useCheckoutState } from "../context/checkout";
2 |
3 | // TODO: Build array of crumbs dynamically from available steps
4 |
5 | function Breadcrumbs({ inCart }) {
6 | const { currentStep, extrafields } = useCheckoutState();
7 |
8 | if (inCart) {
9 | return Shopping Bag ;
10 | }
11 |
12 | if (currentStep === "success") {
13 | return Order received ;
14 | }
15 |
16 | return (
17 |
18 | {currentStep === "extrafields" && (
19 | <>
20 | Shopping Bag
21 | →
22 | Booking
23 | →
24 | Shipping
25 | →
26 | Payment
27 | >
28 | )}
29 | {currentStep === "shipping" && (
30 | <>
31 | Shopping Bag
32 |
33 | {extrafields.length > 0 && (
34 | <>
35 | →
36 | Booking
37 | >
38 | )}
39 | →
40 | Shipping
41 | →
42 | Payment
43 | >
44 | )}
45 | {currentStep === "billing" && (
46 | <>
47 | Shopping Bag
48 | {extrafields.length > 0 && (
49 | <>
50 | →
51 | Booking
52 | >
53 | )}
54 | →
55 | Shipping
56 | →
57 | Payment
58 | >
59 | )}
60 |
61 | );
62 | }
63 |
64 | export default Breadcrumbs;
65 |
--------------------------------------------------------------------------------
/components/Button.js:
--------------------------------------------------------------------------------
1 | import cc from "classcat";
2 |
3 | import { useThemeState } from "../context/theme";
4 |
5 | const buttonStyle = (theme) => {
6 | switch (theme) {
7 | case "kitchen-sink-journal-chopchop-shop":
8 | return "bg-clementine text-black";
9 | case "walnut-cooks-tools-chopchop-shop":
10 | return "bg-tumbleweed text-black";
11 | case "essential-knife-set-chopchop-shop":
12 | return "bg-hawkes-blue text-black";
13 | case "private-cooking-class-chopchop-shop":
14 | return "bg-asparagus text-black";
15 | case "ceramic-dutch-oven-chopchop-shop":
16 | return "bg-goldenrod text-black";
17 | default:
18 | return "bg-white-rock";
19 | }
20 | };
21 |
22 | function Button({ className, ...props }) {
23 | const theme = useThemeState();
24 |
25 | const buttonClass = cc([
26 | "appearance-none border-none py-0.5 px-1.5 md:px-2 text-lg md:text-xl rounded transition focus:outline-none",
27 | buttonStyle(theme),
28 | className,
29 | ]);
30 |
31 | if (props.href) return ;
32 |
33 | return ;
34 | }
35 |
36 | export default Button;
37 |
--------------------------------------------------------------------------------
/components/Cart.js:
--------------------------------------------------------------------------------
1 | import { useCartState } from "../context/cart";
2 | import { useModalDispatch } from "../context/modal";
3 |
4 | import Button from "./Button";
5 | import CartItem from "./CartItem";
6 |
7 | export default function Cart() {
8 | const { line_items, subtotal, total_unique_items } = useCartState();
9 | const { showCheckout } = useModalDispatch();
10 |
11 | const isEmpty = line_items.length === 0;
12 |
13 | return (
14 |
15 |
16 | {line_items.map((item) => (
17 |
18 | ))}
19 |
20 |
21 |
22 | {isEmpty ? (
23 |
Your cart is empty.
24 | ) : (
25 | <>
26 |
27 | Total: {subtotal?.formatted_with_symbol}, {total_unique_items}{" "}
28 | {total_unique_items === 1 ? "item" : "items"}
29 |
30 |
31 |
35 | Check Out
36 |
37 |
38 | >
39 | )}
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/components/CartItem.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Image from "next/image";
3 | import { toast } from "react-toastify";
4 |
5 | import { commerce } from "../lib/commerce";
6 | import { useCartDispatch } from "../context/cart";
7 |
8 | function CartItem({ id, media, name, quantity, line_total, selected_options }) {
9 | const { setCart } = useCartDispatch();
10 | const hasVariants = selected_options.length >= 1;
11 |
12 | const handleUpdateCart = ({ cart }) => {
13 | setCart(cart);
14 |
15 | return cart;
16 | };
17 |
18 | const handleRemoveItem = () =>
19 | commerce.cart
20 | .remove(id)
21 | .then(handleUpdateCart)
22 | .then(({ subtotal }) =>
23 | toast(
24 | `${name} has been removed from your cart. Your new subtotal is now ${subtotal.formatted_with_symbol}`
25 | )
26 | );
27 |
28 | const decrementQuantity = () => {
29 | quantity > 1
30 | ? commerce.cart
31 | .update(id, { quantity: quantity - 1 })
32 | .then(handleUpdateCart)
33 | .then(({ subtotal }) =>
34 | toast(
35 | `1 "${name}" has been removed from your cart. Your new subtotal is now ${subtotal.formatted_with_symbol}`
36 | )
37 | )
38 | : handleRemoveItem();
39 | };
40 |
41 | const incrementQuantity = () =>
42 | commerce.cart
43 | .update(id, { quantity: quantity + 1 })
44 | .then(handleUpdateCart)
45 | .then(({ subtotal }) =>
46 | toast(
47 | `Another "${name}" has been added to your cart. Your new subtotal is now ${subtotal.formatted_with_symbol}`
48 | )
49 | );
50 |
51 | return (
52 |
53 |
54 |
62 |
63 |
64 |
65 |
66 | {name}
67 |
68 | {hasVariants && (
69 |
70 | {selected_options.map(({ option_name }, index) => (
71 |
72 | {index ? `, ${option_name}` : option_name}
73 |
74 | ))}
75 |
76 | )}
77 |
78 |
79 |
80 | {line_total.formatted_with_symbol}
81 |
82 |
83 |
84 | Quantity:
85 |
89 | -
90 |
91 | {quantity}
92 |
96 | +
97 |
98 |
99 |
100 |
104 | Remove
105 |
106 |
107 |
108 |
109 |
110 |
111 | );
112 | }
113 |
114 | export default CartItem;
115 |
--------------------------------------------------------------------------------
/components/CartSummary.js:
--------------------------------------------------------------------------------
1 | import { useCartState } from "../context/cart";
2 | import { useModalDispatch } from "../context/modal";
3 |
4 | function CartSummary() {
5 | const { total_unique_items } = useCartState();
6 | const { openModal } = useModalDispatch();
7 |
8 | return (
9 |
10 | Shopping Bag ({total_unique_items})
11 |
12 | );
13 | }
14 |
15 | export default CartSummary;
16 |
--------------------------------------------------------------------------------
/components/Checkout/AddressFields.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { FormInput, FormSelect } from "../Form";
4 |
5 | function AddressFields({ prefix = "", countries = {}, subdivisions = {} }) {
6 | const reducer = ([code, name]) => ({
7 | value: code,
8 | label: name,
9 | });
10 |
11 | const formattedCountries = subdivisions
12 | ? Object.entries(countries).map(reducer)
13 | : [];
14 |
15 | const formattedSubdivisions = subdivisions
16 | ? Object.entries(subdivisions).map(reducer)
17 | : [];
18 |
19 | return (
20 |
21 |
22 |
23 |
29 |
30 |
31 |
37 |
38 |
39 |
40 |
46 |
52 |
53 |
54 |
55 |
63 |
64 |
65 |
73 |
74 |
75 |
81 |
82 |
83 |
84 | );
85 | }
86 |
87 | export default AddressFields;
88 |
--------------------------------------------------------------------------------
/components/Checkout/BillingForm.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { useFormContext } from "react-hook-form";
3 | import { useDebounce } from "use-debounce";
4 | import {
5 | CardNumberElement,
6 | CardExpiryElement,
7 | CardCvcElement,
8 | } from "@stripe/react-stripe-js";
9 |
10 | import { commerce } from "../../lib/commerce";
11 |
12 | import { useCheckoutState, useCheckoutDispatch } from "../../context/checkout";
13 |
14 | import { FormCheckbox, FormInput, FormError } from "../Form";
15 | import AddressFields from "./AddressFields";
16 |
17 | const style = {
18 | base: {
19 | "::placeholder": {
20 | color: "rgba(21,7,3,0.3)",
21 | },
22 | color: "#150703",
23 | fontSize: "16px",
24 | 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"`,
25 | iconColor: "#6B7280",
26 | },
27 | };
28 |
29 | function BillingForm() {
30 | const [countries, setCountries] = useState();
31 | const [subdivisions, setSubdivisions] = useState();
32 | const methods = useFormContext();
33 | const { collects } = useCheckoutState();
34 | const { setError } = useCheckoutDispatch();
35 |
36 | const { watch, setValue, clearErrors } = methods;
37 |
38 | const shipping = watch("shipping");
39 | const [watchCountry] = useDebounce(watch("billing.country"), 600);
40 |
41 | useEffect(() => {
42 | fetchCountries();
43 | }, []);
44 |
45 | useEffect(() => {
46 | watchCountry && fetchSubdivisions(watchCountry);
47 | }, [watchCountry]);
48 |
49 | const fetchCountries = async () => {
50 | try {
51 | const { countries } = await commerce.services.localeListCountries();
52 |
53 | setCountries(countries);
54 | } catch (err) {
55 | // noop
56 | }
57 | };
58 |
59 | const fetchSubdivisions = async (country) => {
60 | try {
61 | const { subdivisions } = await commerce.services.localeListSubdivisions(
62 | country
63 | );
64 |
65 | setSubdivisions(subdivisions);
66 | } catch (err) {
67 | // noop
68 | }
69 | };
70 |
71 | const onStripeChange = () => {
72 | clearErrors("stripe");
73 | setError(null);
74 | };
75 |
76 | return (
77 |
78 |
79 |
80 |
81 | Billing address
82 |
83 |
84 | {collects?.shipping_address && (
85 |
89 | checked && setValue("billing", shipping)
90 | }
91 | />
92 | )}
93 |
94 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | Payment
106 |
107 |
108 |
121 |
122 |
123 |
124 |
129 |
130 |
131 |
132 |
133 |
139 |
140 |
141 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 | );
154 | }
155 |
156 | export default BillingForm;
157 |
--------------------------------------------------------------------------------
/components/Checkout/Checkout.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { useForm, FormProvider } from "react-hook-form";
3 | import { useStripe, useElements } from "@stripe/react-stripe-js";
4 |
5 | import { useCartDispatch } from "../../context/cart";
6 | import { useCheckoutState, useCheckoutDispatch } from "../../context/checkout";
7 |
8 | import ExtraFieldsForm from "./ExtraFieldsForm";
9 | import ShippingForm from "./ShippingForm";
10 | import BillingForm from "./BillingForm";
11 | import Success from "./Success";
12 | import CheckoutSummary from "./CheckoutSummary";
13 | import OrderSummary from "./OrderSummary";
14 |
15 | import LoadingSVG from "../../svg/loading.svg";
16 |
17 | function Checkout({ cartId }) {
18 | const [order, setOrder] = useState();
19 | const { reset: resetCart } = useCartDispatch();
20 | const { currentStep, id, live } = useCheckoutState();
21 | const {
22 | generateToken,
23 | setCurrentStep,
24 | nextStepFrom,
25 | capture,
26 | setProcessing,
27 | setError: setCheckoutError,
28 | } = useCheckoutDispatch();
29 | const methods = useForm({
30 | shouldUnregister: false,
31 | });
32 | const { handleSubmit, setError } = methods;
33 |
34 | const stripe = useStripe();
35 | const elements = useElements();
36 |
37 | useEffect(() => {
38 | generateToken(cartId);
39 | }, [cartId]);
40 |
41 | const captureOrder = async (values) => {
42 | setProcessing(true);
43 |
44 | const {
45 | customer,
46 | shipping,
47 | billing: { firstname, lastname, region: county_state, ...billing },
48 | ...data
49 | } = values;
50 |
51 | const { error, paymentMethod } = await stripe.createPaymentMethod({
52 | type: "card",
53 | card: elements.getElement("cardNumber"),
54 | billing_details: {
55 | name: `${billing.firstname} ${billing.lastname}`,
56 | email: customer.email,
57 | },
58 | });
59 |
60 | if (error) {
61 | setError("stripe", { type: "manual", message: error.message });
62 | setProcessing(false);
63 | return;
64 | }
65 |
66 | const checkoutPayload = {
67 | ...data,
68 | customer: {
69 | ...customer,
70 | firstname,
71 | lastname,
72 | },
73 | ...(shipping && {
74 | shipping: {
75 | ...shipping,
76 | name: `${shipping.firstname} ${shipping.lastname}`,
77 | },
78 | }),
79 | billing: {
80 | ...billing,
81 | name: `${firstname} ${lastname}`,
82 | county_state,
83 | },
84 | };
85 |
86 | try {
87 | const newOrder = await capture({
88 | ...checkoutPayload,
89 | payment: {
90 | gateway: "stripe",
91 | stripe: {
92 | payment_method_id: paymentMethod.id,
93 | },
94 | },
95 | });
96 |
97 | handleOrderSuccess(newOrder);
98 | setProcessing(false);
99 | } catch (res) {
100 | if (
101 | res.statusCode !== 402 ||
102 | res.data.error.type !== "requires_verification"
103 | ) {
104 | setCheckoutError(res.data.error.message);
105 | setProcessing(false);
106 | return;
107 | }
108 |
109 | const { error, paymentIntent } = await stripe.handleCardAction(
110 | res.data.error.param
111 | );
112 |
113 | if (error) {
114 | setError("stripe", { type: "manual", message: error.message });
115 | setProcessing(false);
116 | return;
117 | }
118 |
119 | try {
120 | const newOrder = await capture({
121 | ...checkoutPayload,
122 | payment: {
123 | gateway: "stripe",
124 | stripe: {
125 | payment_intent_id: paymentIntent.id,
126 | },
127 | },
128 | });
129 |
130 | handleOrderSuccess(newOrder);
131 | setProcessing(false);
132 | } catch (err) {
133 | setError("stripe", { type: "manual", message: error.message });
134 | setProcessing(false);
135 | }
136 | }
137 | };
138 |
139 | const handleOrderSuccess = (order) => {
140 | setOrder(order);
141 | setCurrentStep("success");
142 | resetCart();
143 | };
144 |
145 | const onSubmit = (values) => {
146 | if (currentStep === "billing") return captureOrder(values);
147 |
148 | return setCurrentStep(nextStepFrom(currentStep));
149 | };
150 |
151 | if (!id)
152 | return (
153 |
154 |
155 |
Preparing checkout
156 |
157 | );
158 |
159 | return (
160 |
161 |
172 |
173 | );
174 | }
175 |
176 | export default Checkout;
177 |
--------------------------------------------------------------------------------
/components/Checkout/CheckoutSummary.js:
--------------------------------------------------------------------------------
1 | import cc from "classcat";
2 |
3 | import { useCheckoutState } from "../../context/checkout";
4 |
5 | import Button from "../Button";
6 |
7 | function CheckoutSummary({ subtotal, tax, shipping, line_items = [], total }) {
8 | const { processing, error } = useCheckoutState();
9 | const count = line_items.length;
10 |
11 | return (
12 |
13 |
14 |
15 |
16 | {subtotal && Subtotal: {subtotal.formatted_with_symbol} }
17 | {tax && Tax: {tax.amount.formatted_with_symbol} }
18 | {shipping && (
19 | Shipping: {shipping.price.formatted_with_symbol}
20 | )}
21 | {total && (
22 |
23 | Total: {total.formatted_with_symbol}, {count}{" "}
24 | {count === 1 ? "item" : "items"}
25 |
26 | )}
27 |
28 |
29 |
30 |
31 | {error && {error} }
32 |
42 | {processing ? "Processing order" : "Continue"}
43 |
44 |
45 |
46 |
47 |
48 | );
49 | }
50 |
51 | export default CheckoutSummary;
52 |
--------------------------------------------------------------------------------
/components/Checkout/ExtraFieldsForm.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useCheckoutState, useCheckoutDispatch } from "../../context/checkout";
3 |
4 | import { FormInput, FormCheckbox, FormTextarea } from "../Form";
5 |
6 | // TODO: Update the UI to be built from the API
7 | // once products have extrafields that can be of
8 | // any type. E.g. "date", "textarea"
9 |
10 | // const fields = {
11 | // BookingDate: (props) => (
12 | // <>
13 | //
14 | // >
15 | // ),
16 | // };
17 |
18 | function ExtraFieldsForm() {
19 | const { extrafields } = useCheckoutState();
20 | const { setCurrentStep, nextStepFrom } = useCheckoutDispatch();
21 |
22 | useEffect(() => {
23 | if (extrafields.length === 0) {
24 | setCurrentStep(nextStepFrom("extrafields"));
25 | }
26 | return null;
27 | }, [extrafields]);
28 |
29 | return (
30 |
31 |
32 |
33 |
34 | Booking
35 |
36 |
37 |
38 |
39 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | Lesson Plan
52 |
53 |
54 |
55 | Thanks for joining us for a lesson! Let us know what you might like
56 | to learn or cook below.
57 |
58 |
59 | {extrafields.map(({ id }) => {
60 | const computedFieldName = `extrafields.${id}`;
61 |
62 | return (
63 |
70 | );
71 | })}
72 |
73 |
74 |
75 | );
76 | }
77 |
78 | export default ExtraFieldsForm;
79 |
--------------------------------------------------------------------------------
/components/Checkout/OrderSummary.js:
--------------------------------------------------------------------------------
1 | import Button from "../Button";
2 |
3 | function CheckoutSummary({ has, fulfillment, order }) {
4 | const { subtotal, tax, shipping, line_items, total } = order;
5 |
6 | const count = line_items.length;
7 |
8 | return (
9 |
10 |
11 |
12 |
13 | Subtotal: {subtotal.formatted_with_symbol}
14 | {tax && Tax: {tax.amount.formatted_with_symbol} }
15 | {shipping && (
16 | Shipping: {shipping.price.formatted_with_symbol}
17 | )}
18 | {total && (
19 |
20 | Total: {total.formatted_with_symbol}, {count}{" "}
21 | {count === 1 ? "item" : "items"}
22 |
23 | )}
24 |
25 |
26 | {has.digital_fulfillment && (
27 |
28 | {fulfillment.digital.downloads.map((download, index) => (
29 |
33 | {download.packages.map(({ access_link, name }, index) => (
34 |
40 | Download {name}
41 |
42 | ))}
43 |
44 | ))}
45 |
46 | )}
47 |
48 |
49 | );
50 | }
51 |
52 | export default CheckoutSummary;
53 |
--------------------------------------------------------------------------------
/components/Checkout/ShippingForm.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { useFormContext } from "react-hook-form";
3 | import { useDebounce } from "use-debounce";
4 |
5 | import { commerce } from "../../lib/commerce";
6 |
7 | import { useCheckoutState, useCheckoutDispatch } from "../../context/checkout";
8 |
9 | import AddressFields from "./AddressFields";
10 | import { FormCheckbox as FormRadio, FormError } from "../Form";
11 |
12 | function ShippingForm() {
13 | const { id } = useCheckoutState();
14 | const { setShippingMethod } = useCheckoutDispatch();
15 | const [countries, setCountries] = useState();
16 | const [subdivisions, setSubdivisions] = useState();
17 | const [shippingOptions, setShippingOptions] = useState([]);
18 | const methods = useFormContext();
19 | const { watch, setValue } = methods;
20 |
21 | const [watchCountry] = useDebounce(watch("shipping.country"), 600);
22 | const watchSubdivision = watch("shipping.region");
23 |
24 | useEffect(() => {
25 | fetchCountries(id);
26 | }, []);
27 |
28 | useEffect(() => {
29 | setValue("shipping.region", "");
30 |
31 | if (watchCountry) {
32 | fetchSubdivisions(id, watchCountry);
33 | fetchShippingOptions(id, watchCountry);
34 | }
35 | }, [watchCountry]);
36 |
37 | useEffect(() => {
38 | if (watchSubdivision) {
39 | fetchShippingOptions(id, watchCountry, watchSubdivision);
40 | }
41 | }, [watchSubdivision]);
42 |
43 | const fetchCountries = async (checkoutId) => {
44 | try {
45 | const { countries } = await commerce.services.localeListShippingCountries(
46 | checkoutId
47 | );
48 |
49 | setCountries(countries);
50 | } catch (err) {
51 | // noop
52 | }
53 | };
54 |
55 | const fetchSubdivisions = async (checkoutId, countryCode) => {
56 | try {
57 | const {
58 | subdivisions,
59 | } = await commerce.services.localeListShippingSubdivisions(
60 | checkoutId,
61 | countryCode
62 | );
63 |
64 | setSubdivisions(subdivisions);
65 | } catch (err) {
66 | // noop
67 | }
68 | };
69 |
70 | const fetchShippingOptions = async (checkoutId, country, region) => {
71 | if (!checkoutId && !country) return;
72 |
73 | setValue("fulfillment.shipping_method", null);
74 |
75 | try {
76 | const shippingOptions = await commerce.checkout.getShippingOptions(
77 | checkoutId,
78 | {
79 | country,
80 | ...(region && { region }),
81 | }
82 | );
83 |
84 | setShippingOptions(shippingOptions);
85 |
86 | if (shippingOptions.length === 1) {
87 | const [shippingOption] = shippingOptions;
88 |
89 | setValue("fulfillment.shipping_method", shippingOption.id);
90 | selectShippingMethod(shippingOption.id);
91 | }
92 | } catch (err) {
93 | // noop
94 | }
95 | };
96 |
97 | const onShippingSelect = ({ target: { value } }) =>
98 | selectShippingMethod(value);
99 |
100 | const selectShippingMethod = async (optionId) => {
101 | try {
102 | await setShippingMethod(optionId, watchCountry, watchSubdivision);
103 | } catch (err) {
104 | // noop
105 | }
106 | };
107 |
108 | return (
109 |
110 |
111 |
112 |
113 | Shipping address
114 |
115 |
116 |
121 |
122 |
123 |
124 |
125 |
126 | Shipping
127 |
128 |
129 | {watchCountry ? (
130 | <>
131 |
132 | {shippingOptions.map(({ id, description, price }) => (
133 |
134 |
143 |
144 | ))}
145 |
146 |
147 |
148 | >
149 | ) : (
150 |
151 | Please enter your address to fetch shipping options
152 |
153 | )}
154 |
155 |
156 |
157 |
158 | );
159 | }
160 |
161 | export default ShippingForm;
162 |
--------------------------------------------------------------------------------
/components/Checkout/Success.js:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 |
3 | function Success({ has }) {
4 | return (
5 |
6 |
7 |
8 | Thanks!
9 |
10 |
11 | {has.digital_fulfillment
12 | ? "You’ll receive an email with your receipt, and a backup link to re-download your purchase"
13 | : "You’ll receive an email with your receipt, and tracking information."}
14 |
15 |
16 |
17 |
18 |
19 |
26 |
27 |
28 |
29 |
...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.
30 |
31 |
37 | 'Chop chop' what are you waiting for
38 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
46 | export default Success;
47 |
--------------------------------------------------------------------------------
/components/Checkout/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./Checkout";
2 |
--------------------------------------------------------------------------------
/components/Footer.js:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import LogoSVG from "../svg/logo.svg";
4 | import CommerceJsSVG from "../svg/commercejs.svg";
5 |
6 | function Footer() {
7 | return (
8 |
9 |
10 |
17 |
18 |
30 |
31 |
68 |
69 |
70 | );
71 | }
72 |
73 | export default Footer;
74 |
--------------------------------------------------------------------------------
/components/Form/FormCheckbox.js:
--------------------------------------------------------------------------------
1 | import { useFormContext } from "react-hook-form";
2 |
3 | function FormCheckbox({
4 | label,
5 | children,
6 | name,
7 | required = false,
8 | validation = {},
9 |
10 | ...props
11 | }) {
12 | const { register } = useFormContext();
13 |
14 | const isRequired = required
15 | ? typeof required === "boolean"
16 | ? `${label || name} is required`
17 | : required
18 | : false;
19 |
20 | return (
21 |
22 |
26 |
34 |
35 | {(children || label) && (
36 | {children || label}
37 | )}
38 |
39 |
40 | );
41 | }
42 |
43 | export default FormCheckbox;
44 |
--------------------------------------------------------------------------------
/components/Form/FormError.js:
--------------------------------------------------------------------------------
1 | import cc from "classcat";
2 | import { ErrorMessage } from "@hookform/error-message";
3 |
4 | function FormError({ className, ...props }) {
5 | return (
6 |
7 | (
10 |
11 | {message}
12 |
13 | )}
14 | />
15 |
16 | );
17 | }
18 |
19 | export default FormError;
20 |
--------------------------------------------------------------------------------
/components/Form/FormInput.js:
--------------------------------------------------------------------------------
1 | import { useFormContext } from "react-hook-form";
2 |
3 | import FormError from "./FormError";
4 |
5 | function FormInput({
6 | label,
7 | name,
8 | type = "text",
9 | required = false,
10 | validation = {},
11 | ...props
12 | }) {
13 | const { register } = useFormContext();
14 |
15 | const isRequired = required ? `${label || name} is required` : false;
16 |
17 | return (
18 |
19 |
27 |
28 |
29 | );
30 | }
31 |
32 | export default FormInput;
33 |
--------------------------------------------------------------------------------
/components/Form/FormSelect.js:
--------------------------------------------------------------------------------
1 | import { useFormContext } from "react-hook-form";
2 |
3 | import Chevron from "../../svg/chevron.svg";
4 |
5 | import FormError from "./FormError";
6 |
7 | function FormSelect({
8 | label,
9 | name,
10 | options,
11 | required = false,
12 | validation = {},
13 | placeholder,
14 | ...props
15 | }) {
16 | const { register } = useFormContext();
17 |
18 | const isRequired = required ? `${label || name} is required` : false;
19 |
20 | return (
21 |
22 |
23 |
31 |
32 | {placeholder || `Select a ${label}`}
33 |
34 |
35 | {options.map(({ value, label }) => (
36 |
37 | {label || value}
38 |
39 | ))}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | );
50 | }
51 |
52 | export default FormSelect;
53 |
--------------------------------------------------------------------------------
/components/Form/FormTextarea.js:
--------------------------------------------------------------------------------
1 | import { useFormContext } from "react-hook-form";
2 |
3 | import FormError from "./FormError";
4 |
5 | function FormTextarea({
6 | label,
7 | name,
8 | required = false,
9 | validation = {},
10 | ...props
11 | }) {
12 | const { register } = useFormContext();
13 |
14 | const isRequired = required ? `${label || name} is required` : false;
15 |
16 | return (
17 |
18 |
25 |
26 |
27 | );
28 | }
29 |
30 | export default FormTextarea;
31 |
--------------------------------------------------------------------------------
/components/Form/index.js:
--------------------------------------------------------------------------------
1 | export { default as FormInput } from "./FormInput";
2 | export { default as FormTextarea } from "./FormTextarea";
3 | export { default as FormCheckbox } from "./FormCheckbox";
4 | export { default as FormSelect } from "./FormSelect";
5 | export { default as FormError } from "./FormError";
6 |
--------------------------------------------------------------------------------
/components/Header.js:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import CartSummary from "./CartSummary";
4 |
5 | import LogoSVG from "../svg/logo.svg";
6 |
7 | function Header() {
8 | return (
9 |
10 |
11 |
12 |
Shop
13 |
14 |
,
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 |
27 | export default Header;
28 |
--------------------------------------------------------------------------------
/components/Layout.js:
--------------------------------------------------------------------------------
1 | import Footer from "./Footer";
2 |
3 | function Layout({ children }) {
4 | return (
5 | <>
6 |
9 |
10 | >
11 | );
12 | }
13 |
14 | export default Layout;
15 |
--------------------------------------------------------------------------------
/components/Modal.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useRouter } from "next/router";
3 | import { AnimatePresence, motion } from "framer-motion";
4 |
5 | import { useModalState, useModalDispatch } from "../context/modal";
6 | import { useCheckoutDispatch } from "../context/checkout";
7 | import { useCartState } from "../context/cart";
8 |
9 | import Breadcrumbs from "./Breadcrumbs";
10 | import Cart from "./Cart";
11 | import Checkout from "./Checkout";
12 |
13 | function CurrentStep({ step }) {
14 | const { id } = useCartState();
15 |
16 | switch (step) {
17 | case "cart":
18 | return ;
19 | case "checkout":
20 | return ;
21 | default:
22 | return null;
23 | }
24 | }
25 |
26 | function Modal() {
27 | const { open, step } = useModalState();
28 | const { closeModal } = useModalDispatch();
29 | const { reset: resetCheckout } = useCheckoutDispatch();
30 | const router = useRouter();
31 |
32 | useEffect(() => {
33 | router.events.on("routeChangeStart", closeModal);
34 |
35 | return () => {
36 | router.events.off("routeChangeStart", closeModal);
37 | };
38 | }, []);
39 |
40 | const closeAndResetModal = () => {
41 | closeModal();
42 | resetCheckout();
43 | };
44 |
45 | return (
46 |
47 | {open && (
48 |
57 |
58 |
59 |
60 |
61 |
62 |
66 | Close
67 |
68 |
69 |
70 |
71 |
72 |
73 | )}
74 |
75 | );
76 | }
77 |
78 | export default Modal;
79 |
--------------------------------------------------------------------------------
/components/Product.js:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 | import cc from "classcat";
4 |
5 | function Product({ media, name, permalink, price, className }) {
6 | const imageClass = cc([
7 | "relative rounded-lg hover:rounded-none overflow-hidden w-full transition-all",
8 | className,
9 | ]);
10 |
11 | return (
12 |
13 |
14 | {media?.source && (
15 |
16 |
24 |
25 | )}
26 |
27 | {name}
28 |
29 | {price.formatted_with_symbol}
30 |
31 |
32 |
33 |
34 | );
35 | }
36 |
37 | export default Product;
38 |
--------------------------------------------------------------------------------
/components/ProductAttributes.js:
--------------------------------------------------------------------------------
1 | function ProductAttributes({ attributes = [] }) {
2 | if (!attributes || attributes.length === 0) return null;
3 |
4 | return (
5 |
6 | {attributes.map((fileName) => (
7 |
11 |
15 |
16 | ))}
17 |
18 | );
19 | }
20 |
21 | export default ProductAttributes;
22 |
--------------------------------------------------------------------------------
/components/ProductGrid.js:
--------------------------------------------------------------------------------
1 | import Product from "./Product";
2 |
3 | function ProductGrid({ products, ...props }) {
4 | if (!products || products.length === 0) return null;
5 |
6 | return (
7 |
8 | {products.map((product) => (
9 |
10 | ))}
11 |
12 | );
13 | }
14 |
15 | export default ProductGrid;
16 |
--------------------------------------------------------------------------------
/components/ProductImages.js:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | function ProductImages({ images = [] }) {
4 | if (!images || images.length === 0) return null;
5 |
6 | return images.map(({ id, url, image_dimensions }) => (
7 |
8 |
17 |
18 | ));
19 | }
20 |
21 | export default ProductImages;
22 |
--------------------------------------------------------------------------------
/components/ProductList.js:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | function ProductList({ products }) {
4 | if (!products || products.length === 0) return null;
5 |
6 | return products.map(({ name, permalink }, index) => (
7 |
8 | {index ? ", " : ""}
9 |
10 | {name}
11 |
12 |
13 | ));
14 | }
15 |
16 | export default ProductList;
17 |
--------------------------------------------------------------------------------
/components/RelatedProducts.js:
--------------------------------------------------------------------------------
1 | import Product from "./Product";
2 |
3 | function RelatedProducts({ products }) {
4 | if (!products || products.length === 0) return null;
5 |
6 | return (
7 |
8 |
9 | Some other things you might like
10 |
11 |
12 |
13 | {products.map((product) => (
14 |
19 | ))}
20 |
21 |
22 | );
23 | }
24 |
25 | export default RelatedProducts;
26 |
--------------------------------------------------------------------------------
/components/VariantPicker.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Chevron from "../svg/chevron.svg";
4 |
5 | function VariantPicker({ variantGroups = [], defaultValues = {}, ...props }) {
6 | if (!variantGroups || variantGroups.length === 0) return null;
7 |
8 | return (
9 |
10 | {variantGroups.map(({ options, ...group }) => (
11 |
15 |
16 | {group.name}:
17 |
18 |
19 |
25 | {options.map((option) => (
26 |
27 | {option.name}
28 |
29 | ))}
30 |
31 |
32 |
33 |
34 |
35 |
36 | ))}
37 |
38 | );
39 | }
40 |
41 | export default VariantPicker;
42 |
--------------------------------------------------------------------------------
/context/cart.js:
--------------------------------------------------------------------------------
1 | import { createContext, useReducer, useEffect, useContext } from "react";
2 | import { useCycle } from "framer-motion";
3 |
4 | import { commerce } from "../lib/commerce";
5 |
6 | const CartStateContext = createContext();
7 | const CartDispatchContext = createContext();
8 |
9 | const SET_CART = "SET_CART";
10 | const RESET = "RESET";
11 |
12 | const initialState = {
13 | total_items: 0,
14 | total_unique_items: 0,
15 | line_items: [],
16 | };
17 |
18 | const reducer = (state, action) => {
19 | switch (action.type) {
20 | case SET_CART:
21 | return { ...state, ...action.payload };
22 | case RESET:
23 | return initialState;
24 | default:
25 | throw new Error(`Unknown action: ${action.type}`);
26 | }
27 | };
28 |
29 | export const CartProvider = ({ children }) => {
30 | const [open, toggle] = useCycle(false, true);
31 | const [state, dispatch] = useReducer(reducer, initialState);
32 |
33 | useEffect(() => {
34 | getCart();
35 | }, []);
36 |
37 | const getCart = async () => {
38 | try {
39 | const cart = await commerce.cart.retrieve();
40 |
41 | dispatch({ type: SET_CART, payload: cart });
42 | } catch (err) {
43 | // noop
44 | }
45 | };
46 |
47 | const setCart = async (payload) => dispatch({ type: SET_CART, payload });
48 |
49 | const showCart = () => {
50 | toggle();
51 | document.body.classList.add("overflow-hidden");
52 | };
53 |
54 | const closeCart = () => {
55 | toggle();
56 | document.body.classList.remove("overflow-hidden");
57 | };
58 |
59 | const reset = async () => dispatch({ type: RESET });
60 |
61 | return (
62 |
65 |
66 | {children}
67 |
68 |
69 | );
70 | };
71 |
72 | export const useCartState = () => useContext(CartStateContext);
73 | export const useCartDispatch = () => useContext(CartDispatchContext);
74 |
--------------------------------------------------------------------------------
/context/checkout.js:
--------------------------------------------------------------------------------
1 | import { createContext, useReducer, useContext } from "react";
2 |
3 | import { commerce } from "../lib/commerce";
4 |
5 | const CheckoutStateContext = createContext();
6 | const CheckoutDispatchContext = createContext();
7 |
8 | const SET_CURRENT_STEP = "SET_CURRENT_STEP";
9 | const SET_CHECKOUT = "SET_CHECKOUT";
10 | const SET_LIVE = "SET_LIVE";
11 | const SET_PROCESSING = "SET_PROCESSING";
12 | const SET_ERROR = "SET_ERROR";
13 | const RESET = "RESET";
14 |
15 | const initialState = {
16 | currentStep: "extrafields",
17 | processing: false,
18 | error: null,
19 | };
20 |
21 | const reducer = (state, action) => {
22 | switch (action.type) {
23 | case SET_CURRENT_STEP:
24 | return {
25 | ...state,
26 | currentStep: action.payload,
27 | };
28 | case SET_CHECKOUT:
29 | return {
30 | ...state,
31 | ...action.payload,
32 | };
33 | case SET_LIVE:
34 | return { ...state, live: { ...state.live, ...action.payload } };
35 | case SET_PROCESSING:
36 | return { ...state, processing: action.payload };
37 | case SET_ERROR:
38 | return { ...state, error: action.payload };
39 | case RESET:
40 | return initialState;
41 | default:
42 | throw new Error(`Unknown action: ${action.type}`);
43 | }
44 | };
45 |
46 | export const CheckoutProvider = ({ children }) => {
47 | const [state, dispatch] = useReducer(reducer, initialState);
48 |
49 | const generateToken = async (cartId) => {
50 | if (!cartId) return;
51 |
52 | try {
53 | const payload = await commerce.checkout.generateToken(cartId, {
54 | type: "cart",
55 | });
56 |
57 | dispatch({ type: SET_CHECKOUT, payload });
58 | } catch (err) {
59 | // noop
60 | }
61 | };
62 |
63 | const setShippingMethod = async (shipping_option_id, country, region) => {
64 | try {
65 | const { live } = await commerce.checkout.checkShippingOption(state.id, {
66 | shipping_option_id,
67 | country,
68 | ...(region && { region }),
69 | });
70 |
71 | dispatch({ type: SET_LIVE, payload: live });
72 | } catch (err) {
73 | // noop
74 | }
75 | };
76 |
77 | const setCurrentStep = (step) =>
78 | dispatch({ type: SET_CURRENT_STEP, payload: step });
79 |
80 | const nextStepFrom = (currentStep) => {
81 | switch (currentStep) {
82 | case "extrafields":
83 | return state.collects.shipping_address ? "shipping" : "billing";
84 | case "shipping":
85 | default:
86 | return "billing";
87 | }
88 | };
89 |
90 | const capture = (values) => commerce.checkout.capture(state.id, values);
91 |
92 | const setProcessing = (payload) =>
93 | dispatch({ type: SET_PROCESSING, payload });
94 |
95 | const setError = (payload) => dispatch({ type: SET_ERROR, payload });
96 |
97 | const reset = () => dispatch({ type: RESET });
98 |
99 | return (
100 |
112 |
113 | {children}
114 |
115 |
116 | );
117 | };
118 |
119 | export const useCheckoutState = () => useContext(CheckoutStateContext);
120 | export const useCheckoutDispatch = () => useContext(CheckoutDispatchContext);
121 |
--------------------------------------------------------------------------------
/context/modal.js:
--------------------------------------------------------------------------------
1 | import { createContext, useReducer, useContext } from "react";
2 | import { useCycle } from "framer-motion";
3 |
4 | const ModalStateContext = createContext();
5 | const ModalDispatchContext = createContext();
6 |
7 | const SHOW_CART = "SHOW_CART";
8 | const SHOW_CHECKOUT = "SHOW_CHECKOUT";
9 |
10 | const initialState = {
11 | step: "cart",
12 | };
13 |
14 | const reducer = (state, action) => {
15 | switch (action.type) {
16 | case SHOW_CART:
17 | return { ...state, step: "cart" };
18 | case SHOW_CHECKOUT:
19 | return { ...state, step: "checkout" };
20 | default:
21 | throw new Error(`Unknown action: ${action.type}`);
22 | }
23 | };
24 |
25 | export const ModalProvider = ({ children }) => {
26 | const [open, toggle] = useCycle(false, true);
27 | const [state, dispatch] = useReducer(reducer, initialState);
28 |
29 | const openModal = () => {
30 | toggle();
31 | document.body.classList.add("overflow-hidden");
32 | };
33 |
34 | const closeModal = () => {
35 | toggle(0);
36 | document.body.classList.remove("overflow-hidden");
37 | dispatch({ type: "SHOW_CART" });
38 | };
39 |
40 | const showCart = () => dispatch({ type: "SHOW_CART" });
41 |
42 | const showCheckout = () => dispatch({ type: "SHOW_CHECKOUT" });
43 |
44 | return (
45 |
48 |
49 | {children}
50 |
51 |
52 | );
53 | };
54 |
55 | export const useModalState = () => useContext(ModalStateContext);
56 | export const useModalDispatch = () => useContext(ModalDispatchContext);
57 |
--------------------------------------------------------------------------------
/context/theme.js:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | const ThemeStateContext = React.createContext();
4 | const ThemeDispatchContext = React.createContext();
5 |
6 | const initialState = null;
7 |
8 | export const ThemeProvider = ({ children }) => {
9 | const [theme, setTheme] = React.useState(initialState);
10 |
11 | return (
12 |
13 |
14 | {children}
15 |
16 |
17 | );
18 | };
19 |
20 | export const useThemeState = () => React.useContext(ThemeStateContext);
21 | export const useThemeDispatch = () => React.useContext(ThemeDispatchContext);
22 |
--------------------------------------------------------------------------------
/lib/commerce.js:
--------------------------------------------------------------------------------
1 | import CommerceSDK from "@chec/commerce.js";
2 |
3 | const checAPIKey = process.env.NEXT_PUBLIC_CHEC_PUBLIC_API_KEY;
4 | const devEnvironment = process.env.NODE_ENV === 'development';
5 |
6 | // Commerce.js constructor options
7 | const commerceConfig = {
8 | axiosConfig: {
9 | headers: {
10 | 'X-Chec-Agent': 'commerce.js/v2',
11 | 'Chec-Version': '2021-03-10',
12 | },
13 | },
14 | };
15 |
16 | if (devEnvironment && !checAPIKey) {
17 | throw Error('Your public API key must be provided as an environment variable named `NEXT_PUBLIC_CHEC_PUBLIC_API_KEY`. Obtain your Chec public key by logging into your Chec account and navigate to Setup > Developer, or can be obtained with the Chec CLI via with the command chec whoami');
18 | }
19 |
20 | export const commerce = new CommerceSDK(
21 | checAPIKey,
22 | devEnvironment,
23 | commerceConfig,
24 | );
25 |
--------------------------------------------------------------------------------
/lib/gtag.js:
--------------------------------------------------------------------------------
1 | export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GA_TRACKING_ID;
2 |
3 | export const pageview = (url) => {
4 | if (!GA_TRACKING_ID) return;
5 |
6 | window.gtag("config", GA_TRACKING_ID, {
7 | page_path: url,
8 | });
9 | };
10 |
11 | export const event = ({ action, category, label, value }) => {
12 | if (!GA_TRACKING_ID) return;
13 |
14 | window.gtag("event", action, {
15 | event_category: category,
16 | event_label: label,
17 | value: value,
18 | });
19 | };
20 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | i18n: {
3 | locales: ['en-US'],
4 | defaultLocale: 'en-US',
5 | },
6 | images: {
7 | domains: ["cdn.chec.io"],
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "chopchop",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "dev": "next",
7 | "build": "next build",
8 | "start": "next start",
9 | "seed": "chec-seed seeds"
10 | },
11 | "dependencies": {
12 | "@chec/commerce.js": "2.2.0",
13 | "@hookform/error-message": "0.0.5",
14 | "@stripe/react-stripe-js": "1.1.2",
15 | "@stripe/stripe-js": "1.11.0",
16 | "autoprefixer": "10.0.4",
17 | "classcat": "4.1.0",
18 | "framer-motion": "2.9.4",
19 | "next": "10.0.2",
20 | "next-google-fonts": "1.2.1",
21 | "postcss": "8.1.14",
22 | "react": "17.0.1",
23 | "react-dom": "17.0.1",
24 | "react-hook-form": "6.11.5",
25 | "react-toastify": "6.1.0",
26 | "use-debounce": "^7.0.0"
27 | },
28 | "devDependencies": {
29 | "@chec/seeder": "^1.1.0",
30 | "babel-plugin-inline-react-svg": "1.1.2",
31 | "tailwindcss": "2.0.1"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import "react-toastify/dist/ReactToastify.css";
2 | import "tailwindcss/tailwind.css";
3 |
4 | import { useEffect } from "react";
5 | import { AnimatePresence } from "framer-motion";
6 | import { Elements } from "@stripe/react-stripe-js";
7 | import { loadStripe } from "@stripe/stripe-js";
8 | import { ToastContainer } from "react-toastify";
9 |
10 | import * as gtag from "../lib/gtag";
11 |
12 | import { ThemeProvider } from "../context/theme";
13 | import { ModalProvider } from "../context/modal";
14 | import { CartProvider } from "../context/cart";
15 | import { CheckoutProvider } from "../context/checkout";
16 |
17 | import Layout from "../components/Layout";
18 | import Modal from "../components/Modal";
19 |
20 | const stripePromise = loadStripe(
21 | process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
22 | );
23 |
24 | const toastOptions = {
25 | position: "bottom-center",
26 | draggable: false,
27 | hideProgressBar: true,
28 | className: "w-full md:max-w-xl",
29 | toastClassName: "bg-ecru-white rounded-lg text-black px-3 shadow-md",
30 | };
31 |
32 | function MyApp({ Component, pageProps, router }) {
33 | useEffect(() => {
34 | const handleRouteChange = (url) => {
35 | gtag.pageview(url);
36 | };
37 |
38 | router.events.on("routeChangeComplete", handleRouteChange);
39 |
40 | return () => {
41 | router.events.off("routeChangeComplete", handleRouteChange);
42 | };
43 | }, [router.events]);
44 |
45 | return (
46 | <>
47 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | >
75 | );
76 | }
77 |
78 | export default MyApp;
79 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from "next/document";
2 | import GoogleFonts from "next-google-fonts";
3 |
4 | import { GA_TRACKING_ID } from "../lib/gtag";
5 |
6 | class MyDocument extends Document {
7 | render() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 | {GA_TRACKING_ID && (
16 | <>
17 |
21 |
33 | >
34 | )}
35 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 | }
43 | }
44 |
45 | export default MyDocument;
46 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import { motion } from "framer-motion";
3 |
4 | import { commerce } from "../lib/commerce";
5 |
6 | import Header from "../components/Header";
7 | import ProductList from "../components/ProductList";
8 | import ProductGrid from "../components/ProductGrid";
9 |
10 | export async function getStaticProps() {
11 | const { data } = await commerce.products.list();
12 |
13 | const products = data.filter(({ active }) => active);
14 |
15 | return {
16 | props: {
17 | products,
18 | },
19 | revalidate: 60,
20 | };
21 | }
22 |
23 | function IndexPage({ products }) {
24 | return (
25 | <>
26 |
27 | ChopChop
28 |
29 |
30 |
31 |
32 |
33 |
34 |
46 | Shop:
47 |
48 |
51 |
52 |
53 |
54 |
60 |
64 |
65 |
66 |
67 | >
68 | );
69 | }
70 |
71 | export default IndexPage;
72 |
--------------------------------------------------------------------------------
/pages/products/[permalink].js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Head from "next/head";
3 | import { motion } from "framer-motion";
4 | import { toast } from "react-toastify";
5 |
6 | import { commerce } from "../../lib/commerce";
7 | import { useCartDispatch } from "../../context/cart";
8 | import { useThemeDispatch } from "../../context/theme";
9 | import { useModalDispatch } from "../../context/modal";
10 |
11 | import Header from "../../components/Header";
12 | import Button from "../../components/Button";
13 | import VariantPicker from "../../components/VariantPicker";
14 | import ProductImages from "../../components/ProductImages";
15 | import ProductAttributes from "../../components/ProductAttributes";
16 | import RelatedProducts from "../../components/RelatedProducts";
17 |
18 | export async function getStaticProps({ params }) {
19 | const { permalink } = params;
20 |
21 | const product = await commerce.products.retrieve(permalink, {
22 | type: "permalink",
23 | });
24 |
25 | return {
26 | props: {
27 | product,
28 | },
29 | revalidate: 60,
30 | };
31 | }
32 |
33 | export async function getStaticPaths() {
34 | const { data: products } = await commerce.products.list();
35 |
36 | return {
37 | paths: products.map(({ permalink }) => ({
38 | params: {
39 | permalink,
40 | },
41 | })),
42 | fallback: false,
43 | };
44 | }
45 |
46 | function ProductPage({ product }) {
47 | const { setCart } = useCartDispatch();
48 | const {
49 | variant_groups: variantGroups,
50 | assets,
51 | meta,
52 | related_products: relatedProducts,
53 | } = product;
54 | const images = assets.filter(({ is_image }) => is_image);
55 | const setTheme = useThemeDispatch();
56 | const { openModal } = useModalDispatch();
57 |
58 | const initialVariants = React.useMemo(
59 | () =>
60 | variantGroups.reduce((all, { id, options }) => {
61 | const [firstOption] = options;
62 |
63 | return { ...all, [id]: firstOption.id };
64 | }, {}),
65 | [product.permalink]
66 | );
67 |
68 | const [selectedVariants, setSelectedVariants] = React.useState(
69 | initialVariants
70 | );
71 |
72 | React.useEffect(() => {
73 | setSelectedVariants(initialVariants);
74 | setTheme(product.permalink);
75 |
76 | return () => setTheme("default");
77 | }, [product.permalink]);
78 |
79 | const handleVariantChange = ({ target: { id, value } }) =>
80 | setSelectedVariants({
81 | ...selectedVariants,
82 | [id]: value,
83 | });
84 |
85 | const addToCart = () =>
86 | commerce.cart
87 | .add(product.id, 1, selectedVariants)
88 | .then(({ cart }) => {
89 | setCart(cart);
90 |
91 | return cart;
92 | })
93 | .then(({ subtotal }) =>
94 | toast(
95 | `${product.name} is now in your cart. Your subtotal is now ${subtotal.formatted_with_symbol}. Click to view what's in your cart.`,
96 | {
97 | onClick: openModal,
98 | }
99 | )
100 | )
101 | .catch(() => {
102 | toast.error("Please try again.");
103 | });
104 |
105 | return (
106 |
107 |
108 | {product.seo.title}
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
134 |
135 | {product.name}
136 |
137 |
138 |
139 |
140 |
141 |
142 | {product.price.formatted_with_symbol}
143 |
144 |
145 |
146 |
151 |
152 |
153 |
Add to Bag
154 |
155 |
156 |
160 |
161 |
162 |
163 |
164 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 | );
181 | }
182 |
183 | export default ProductPage;
184 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/public/product-attributes/5-fine-pieces.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/product-attributes/6-piece-set.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/product-attributes/77-pages.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/product-attributes/cap.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/product-attributes/clean-easily.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/product-attributes/diam.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/product-attributes/free-book-mark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/product-attributes/glossy-paper.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/product-attributes/high-carbon-steel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/product-attributes/high.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/product-attributes/made-in-france.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/product-attributes/made-in-germany.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/product-attributes/quality-scoops.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/product-attributes/tappered-grip.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/product-attributes/thanks.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/public/product-attributes/url-or-irl.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/seeds/assets.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "link": "products[5].id",
4 | "filename": "ChopChop_Tote.jpg",
5 | "url": "https://cdn.chec.io/merchants/19303/assets/X1HxOcELkPfQBY22_ChopChop_Tote.jpg"
6 | },
7 | {
8 | "link": "products[4].id",
9 | "filename": "Kitchen-Sink-Journal-1.jpg",
10 | "url": "https://cdn.chec.io/merchants/19303/assets/9yI6YD9osPkZqmXC_Kitchen-Sink-Journal-1.jpg"
11 | },
12 | {
13 | "link": "products[3].id",
14 | "filename": "italic-rqWlTD5GwKc-unsplash-1.jpg",
15 | "url": "https://cdn.chec.io/merchants/19303/assets/italic-rqWlTD5GwKc-unsplash%201.jpg"
16 | },
17 | {
18 | "link": "products[3].id",
19 | "filename": "dutch-oven-recolor-green.jpg",
20 | "url": "https://cdn.chec.io/merchants/19303/assets/fhEBFIGPSx4jXhEJ_dutch-oven-recolor-green.jpg"
21 | },
22 | {
23 | "link": "products[3].id",
24 | "filename": "dutch-oven-recolor-red.jpg",
25 | "url": "https://cdn.chec.io/merchants/19303/assets/VvLWyLKtYa17nQEo_dutch-oven-recolor-red.jpg"
26 | },
27 | {
28 | "link": "products[2].id",
29 | "filename": "italic--wPo52T1z-8-unsplash-1.jpg",
30 | "url": "https://cdn.chec.io/merchants/19303/assets/italic--wPo52T1z-8-unsplash%201.jpg"
31 | },
32 | {
33 | "link": "products[2].id",
34 | "filename": "italic-dGoB5OrHDS0-unsplash-1.jpg",
35 | "url": "https://cdn.chec.io/merchants/19303/assets/italic-dGoB5OrHDS0-unsplash%201.jpg"
36 | },
37 | {
38 | "link": "products[1].id",
39 | "filename": "emy-kOtEYRJspm8-unsplash.jpg",
40 | "url": "https://cdn.chec.io/merchants/19303/assets/emy-kOtEYRJspm8-unsplash.jpg"
41 | },
42 | {
43 | "link": "products[0].id",
44 | "filename": "photo-1594761077961-cadd185540a4-1.jpg",
45 | "url": "https://cdn.chec.io/merchants/19303/assets/photo-1594761077961-cadd185540a4%201.jpg"
46 | }
47 | ]
48 |
--------------------------------------------------------------------------------
/seeds/categories.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "slug": "journal",
4 | "name": "Journal"
5 | },
6 | {
7 | "slug": "cooking-class",
8 | "name": "Cooking Class"
9 | },
10 | {
11 | "slug": "cookware",
12 | "name": "Cookware"
13 | }
14 | ]
15 |
--------------------------------------------------------------------------------
/seeds/products.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "product": {
4 | "name": "Walnut Cook's Tools",
5 | "description": "Carved by our friends at Mason St. Workshop, these cook’s tools are made to last. We went back and forth for months deliberating on woods, handles and the exact tools to include before landing on the current set of five which includes a spatula, three different spoons and a large ladle.",
6 | "price": "40.00",
7 | "category_id": "categories[2].id"
8 | },
9 | "collect": {
10 | "billing": true,
11 | "fullname": true
12 | },
13 | "seo": {
14 | "title": "Walnut Cook's Tools | ChopChop",
15 | "description": "Carved by our friends at Mason St. Workshop, these cook’s tools are made to last. A set of five which includes a spatula, three different spoons and a large ladle."
16 | }
17 | },
18 | {
19 | "product": {
20 | "name": "Private Cooking Class",
21 | "description": "Learn core skills or advanced techniques in our private cooking classes. Classes run two hours (online or in person at our Brooklyn storefront) and cover a range of recipes, approaches and techniques. Tell us what you want to tackle or learn a classic recipe. Just let us know in the form at checkout.",
22 | "price": "120.00",
23 | "category_id": "categories[1].id"
24 | },
25 | "extra_field": [
26 | {
27 | "name": "Lesson Plan",
28 | "required": false
29 | }
30 | ],
31 | "collect": {
32 | "fullname": true
33 | },
34 | "seo": {
35 | "title": "Private Cooking Class | ChopChop",
36 | "description": "Learn core skills or advanced techniques in our private cooking classes. Classes run two hours (online or in person at our Brooklyn storefront) and cover a range of recipes, approaches and techniques."
37 | }
38 | },
39 | {
40 | "product": {
41 | "name": "Essential Knife Set",
42 | "description": "There are a lot of knife sets out there, a lot of them are fine, but they also have a bunch of stuff you probably don’t need. We put together the essential knife set so you can snag exactly what you need to get cooking, no more no less. If you want a slightly different variation just get in touch and let us know, we’re happy to put something custom together for your needs.",
43 | "price": "120.00",
44 | "category_id": "categories[2].id"
45 | },
46 | "collect": {
47 | "billing": true,
48 | "fullname": true
49 | },
50 | "seo": {
51 | "title": "Essential Knife Set | ChopChop",
52 | "description": "We put together the essential knife set so you can snag exactly what you need to get cooking, no more no less."
53 | }
54 | },
55 | {
56 | "product": {
57 | "name": "Ceramic Dutch Oven",
58 | "description": "A colorful, stovetop multi-tool that will outlive you with even the most minimal amount of care, the ceramic coated, cast iron dutch oven is the Coach duffle of stovetop cooking - gorgeous, functional and the envy of literally every penny pinching home cook alive today.",
59 | "price": "250.00",
60 | "category_id": "categories[2].id"
61 | },
62 | "variant": [
63 | {
64 | "name": "Color",
65 | "options": [
66 | {
67 | "description": "Yellow",
68 | "quantity": "0",
69 | "price": "0.00"
70 | },
71 | {
72 | "description": "Green",
73 | "price": "0.00"
74 | },
75 | {
76 | "description": "Red",
77 | "price": "0.00"
78 | }
79 | ]
80 | }
81 | ],
82 | "collect": {
83 | "billing": true,
84 | "fullname": true
85 | },
86 | "seo": {
87 | "title": "Ceramic Dutch Oven | ChopChop",
88 | "description": "The ceramic coated, cast iron dutch oven is the Coach duffle of stovetop cooking - gorgeous, functional and the envy of literally every penny pinching home cook alive today."
89 | }
90 | },
91 | {
92 | "product": {
93 | "name": "Kitchen Sink Journal",
94 | "description": "Kitchen Sink Journal, our first publication, documents a year of culinary experiments by the Chop Chop team and some friends of the shop. While it includes 41 detailed recipes, in practice we use it more as a reference on how to tackle a given flavor, texture or ingredient. Hopefully you’ll find it just as useful!",
95 | "price": "35.00",
96 | "category_id": "categories[0].id"
97 | },
98 | "collect": {
99 | "billing": true,
100 | "fullname": true
101 | },
102 | "seo": {
103 | "title": "Kitchen Sink Journal | ChopChop",
104 | "description": "Our first publication documents a year of culinary experiments by the Chop Chop team and some friends of the shop. While it includes 41 detailed recipes, in practice we use it more as a reference on how to tackle a given flavor, texture or ingredient."
105 | }
106 | },
107 | {
108 | "product": {
109 | "name": "Tote bag",
110 | "price": "0.00",
111 | "active": false
112 | }
113 | }
114 | ]
115 |
--------------------------------------------------------------------------------
/svg/chevron.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/svg/commercejs.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/svg/loading.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
11 |
12 |
13 |
17 |
21 |
22 |
23 |
27 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/svg/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/svg/quality-badge.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const defaultTheme = require("tailwindcss/defaultTheme");
2 |
3 | module.exports = {
4 | purge: ["./{components,pages}/**/*.js"],
5 | theme: {
6 | extend: {
7 | colors: {
8 | clementine: "#EF7300",
9 | tumbleweed: "#D9A876",
10 | "hawkes-blue": "#C7DDFD",
11 | asparagus: "#789750",
12 | goldenrod: "#FFCE70",
13 | black: "#150703",
14 | "faded-black": "rgba(21,7,3,0.6)",
15 | "ecru-white": "#FAF8F3",
16 | "white-rock": "#E8E0CF",
17 | },
18 | height: {
19 | 112: "28rem",
20 | },
21 | rotate: {
22 | '-25': '-25deg',
23 | },
24 | boxShadow: {
25 | 'thank-you': '-2.63365px 5.92572px 8.55938px rgba(0, 0, 0, 0.25)',
26 | },
27 | },
28 | fontFamily: {
29 | sans: ["Inter", ...defaultTheme.fontFamily.sans],
30 | serif: ["'EB Garamond'", ...defaultTheme.fontFamily.serif],
31 | },
32 | },
33 | variants: {
34 | extend: {
35 | backgroundColor: ["checked"],
36 | borderColor: ["checked"],
37 | borderRadius: ["hover"],
38 | fontStyle: ["hover"],
39 | textColor: ["checked"],
40 | },
41 | },
42 | };
43 |
--------------------------------------------------------------------------------