├── .env.template ├── .gitignore ├── README.md ├── gatsby-browser.js ├── gatsby-config.js ├── package.json ├── src ├── components │ ├── cart-view │ │ └── cart-view.jsx │ ├── checkout │ │ ├── checkout-step.jsx │ │ ├── checkout-summary.jsx │ │ ├── information-step.jsx │ │ ├── injectable-payment-card.jsx │ │ ├── input-field.jsx │ │ ├── payment-step.jsx │ │ ├── select-field.jsx │ │ ├── shipping-method.jsx │ │ ├── shipping-step.jsx │ │ └── step-overview.jsx │ └── layout │ │ ├── blur.jsx │ │ ├── layout.jsx │ │ └── nav-bar.jsx ├── context │ ├── display-context.js │ └── store-context.js ├── images │ ├── icon.png │ ├── medusa-logo.jpg │ └── medusa-logo.svg ├── pages │ ├── 404.js │ ├── checkout.js │ ├── index.js │ ├── payment.js │ └── product │ │ └── [id].js ├── styles │ ├── blur.module.css │ ├── cart-view.module.css │ ├── checkout-step.module.css │ ├── checkout-summary.module.css │ ├── globals.css │ ├── home.module.css │ ├── information-step.module.css │ ├── injectable-payment-card.module.css │ ├── input-field.module.css │ ├── layout.module.css │ ├── nav-bar.module.css │ ├── payment.module.css │ ├── product.module.css │ ├── shipping-method.module.css │ ├── shipping-step.module.css │ └── step-overview.module.css └── utils │ ├── client.js │ ├── format-price.js │ ├── helper-functions.js │ └── stripe.js └── yarn.lock /.env.template: -------------------------------------------------------------------------------- 1 | #See https://stripe.com/docs/stripe-js for reference 2 | GATSBY_STRIPE_KEY=pk_test_something 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | /node_modules 4 | /public 5 | .env.development 6 | .env 7 | .env.production 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Medusa 4 | 5 |

6 |

7 | Medusa Gatsby Starter 8 |

9 |

10 | Medusa is an open-source headless commerce engine that enables developers to create amazing digital commerce experiences. 11 |

12 |

13 | 14 | Medusa is released under the MIT license. 15 | 16 | 17 | PRs welcome! 18 | 19 | 20 | Discord Chat 21 | 22 | 23 | Follow @medusajs 24 | 25 |

26 | 27 | > **Prerequisites**: To use the starter you should have a Medusa server running locally on port 9000. Check out [medusa-starter-default](https://github.com/medusajs/medusa-starter-default) for a quick setup. 28 | 29 | ## Quick start 30 | 31 | 1. **Setting up the environment variables** 32 | 33 | Navigate into your projects directory and get your environment variables ready: 34 | 35 | ```shell 36 | cd gatsby-starter-medusa/ 37 | mv .env.template .env.development 38 | ``` 39 | 40 | If using Stripe add your Stripe API key to your `.env.development` 41 | 42 | ``` 43 | GATSBY_STRIPE_KEY=pk_test_something 44 | ``` 45 | 46 | 2. **Install dependencies** 47 | 48 | Use Yarn to install all dependencies. 49 | 50 | ```shell 51 | yarn 52 | ``` 53 | 54 | 3. **Start developing.** 55 | 56 | Start up the local server. 57 | 58 | ```shell 59 | yarn start 60 | ``` 61 | 62 | 4. **Open the code and start customizing!** 63 | 64 | Your site is now running at http://localhost:8000! 65 | 66 | Edit `src/pages/index.js` to see your site update in real-time! 67 | 68 | 5. **Learn more about Medusa** 69 | 70 | - [Website](https://www.medusa-commerce.com/) 71 | - [GitHub](https://github.com/medusajs) 72 | - [Documentation](https://docs.medusa-commerce.com/) 73 | 74 | 6. **Learn more about Gatsby** 75 | 76 | - [Documentation](https://www.gatsbyjs.com/docs/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter) 77 | 78 | - [Tutorials](https://www.gatsbyjs.com/tutorial/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter) 79 | 80 | - [Guides](https://www.gatsbyjs.com/tutorial/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter) 81 | 82 | - [API Reference](https://www.gatsbyjs.com/docs/api-reference/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter) 83 | 84 | - [Plugin Library](https://www.gatsbyjs.com/plugins?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter) 85 | 86 | - [Cheat Sheet](https://www.gatsbyjs.com/docs/cheat-sheet/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter) 87 | -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DisplayProvider } from "./src/context/display-context"; 3 | import { StoreProvider } from "./src/context/store-context"; 4 | import Layout from "./src/components/layout/layout"; 5 | import { Location } from "@reach/router"; 6 | 7 | export const wrapRootElement = ({ element }) => { 8 | return ( 9 | 10 | 11 | 12 | {(location) => {element}} 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteMetadata: { 3 | title: "gatsby-starter-medusa", 4 | version: "1.0.0", 5 | }, 6 | plugins: [ 7 | "gatsby-plugin-image", 8 | "gatsby-plugin-react-helmet", 9 | "gatsby-plugin-sharp", 10 | "gatsby-transformer-sharp", 11 | { 12 | resolve: "gatsby-source-filesystem", 13 | options: { 14 | name: "images", 15 | path: "./src/images/", 16 | }, 17 | __key: "images", 18 | }, 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-starter-medusa", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "gatsby-starter-medusa", 6 | "author": "Kasper Fabricius Kristensen (https://www.medusa-commerce.com)", 7 | "keywords": [ 8 | "gatsby", 9 | "medusajs", 10 | "e-commerce" 11 | ], 12 | "scripts": { 13 | "develop": "gatsby develop", 14 | "start": "gatsby develop", 15 | "build": "gatsby build", 16 | "serve": "gatsby serve", 17 | "clean": "gatsby clean" 18 | }, 19 | "dependencies": { 20 | "@medusajs/medusa-js": "^1.0.3", 21 | "@stripe/react-stripe-js": "^1.4.1", 22 | "@stripe/stripe-js": "^1.15.0", 23 | "axios": "^0.21.1", 24 | "formik": "^2.2.9", 25 | "gatsby": "^3.6.2", 26 | "gatsby-plugin-image": "^1.6.0", 27 | "gatsby-plugin-react-helmet": "^4.6.0", 28 | "gatsby-plugin-sharp": "^3.6.0", 29 | "gatsby-source-filesystem": "^3.6.0", 30 | "gatsby-transformer-sharp": "^3.6.0", 31 | "lodash": "^4.17.21", 32 | "react": "^17.0.1", 33 | "react-dom": "^17.0.1", 34 | "react-helmet": "^6.1.0", 35 | "react-icons": "^4.2.0", 36 | "react-spinners": "^0.11.0", 37 | "yup": "^0.32.9" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/cart-view/cart-view.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import DisplayContext from "../../context/display-context"; 3 | import StoreContext from "../../context/store-context"; 4 | import { Link, navigate } from "gatsby"; 5 | import * as styles from "../../styles/cart-view.module.css"; 6 | import { quantity, sum, formatPrice } from "../../utils/helper-functions"; 7 | 8 | const CartView = () => { 9 | const { cartView, updateCartViewDisplay, updateCheckoutStep } = 10 | useContext(DisplayContext); 11 | const { cart, currencyCode, updateLineItem, removeLineItem } = 12 | useContext(StoreContext); 13 | 14 | return ( 15 |
16 |
17 |

Bag

18 |

19 | {cart.items.length > 0 ? cart.items.map(quantity).reduce(sum) : 0}{" "} 20 | {cart.items.length > 0 && cart.items.map(quantity).reduce(sum) === 1 21 | ? "item" 22 | : "items"} 23 |

24 | 30 |
31 |
32 | {cart.items 33 | .sort((a, b) => { 34 | const createdAtA = new Date(a.created_at), 35 | createdAtB = new Date(b.created_at); 36 | 37 | if (createdAtA < createdAtB) return -1; 38 | if (createdAtA > createdAtB) return 1; 39 | return 0; 40 | }) 41 | .map((i) => { 42 | return ( 43 |
44 |
updateCartViewDisplay()}> 45 | 46 | {/* Replace with a product thumbnail/image */} 47 |
48 | {`${i.title}`} 57 |
58 | 59 |
60 |
61 |
62 |
63 | 64 | {i.title} 65 | 66 |

Size: {i.variant.title}

67 |

68 | Price:{" "} 69 | {formatPrice(i.unit_price, cart.region.currency_code)} 70 |

71 |
72 |
73 |
74 |
75 | 86 |

{i.quantity}

87 | 98 |
99 |
100 |

{}

101 |
102 |
103 | 109 |
110 |
111 | ); 112 | })} 113 |
114 |
115 |

Subtotal (incl. taxes)

116 | 117 | {cart.region ? formatPrice(cart.subtotal, currencyCode) : 0} 118 | 119 |
120 |
121 | 132 |
133 |
134 | ); 135 | }; 136 | 137 | export default CartView; 138 | -------------------------------------------------------------------------------- /src/components/checkout/checkout-step.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | import DisplayContext from "../../context/display-context"; 3 | import StoreContext from "../../context/store-context"; 4 | import * as styles from "../../styles/checkout-step.module.css"; 5 | import CheckoutSummary from "./checkout-summary"; 6 | import InformationStep from "./information-step"; 7 | import PaymentStep from "./payment-step"; 8 | import ShippingStep from "./shipping-step"; 9 | import StepOverview from "./step-overview"; 10 | 11 | const CheckoutStep = () => { 12 | const { checkoutStep, updateCheckoutStep, updateOrderSummaryDisplay } = 13 | useContext(DisplayContext); 14 | const { cart, updateAddress, setShippingMethod } = useContext(StoreContext); 15 | 16 | const [isProcessingInfo, setIsProcessingInfo] = useState(false); 17 | const [isProcessingShipping, setIsProcessingShipping] = useState(false); 18 | 19 | const handleShippingSubmit = async (address, email) => { 20 | setIsProcessingInfo(true); 21 | 22 | await updateAddress(address, email); 23 | 24 | setIsProcessingInfo(false); 25 | updateCheckoutStep(2); 26 | }; 27 | 28 | const handleDeliverySubmit = async (option) => { 29 | setIsProcessingShipping(true); 30 | await setShippingMethod(option.id) 31 | .then(() => { 32 | updateCheckoutStep(3); 33 | }) 34 | .finally(() => { 35 | setIsProcessingShipping(false); 36 | }); 37 | }; 38 | 39 | const handleStep = () => { 40 | switch (checkoutStep) { 41 | case 1: 42 | return ( 43 | 51 | handleShippingSubmit(submittedAddr, submittedEmail) 52 | } 53 | /> 54 | ); 55 | case 2: 56 | return ( 57 | 63 | ); 64 | case 3: 65 | return ; 66 | default: 67 | return null; 68 | } 69 | }; 70 | 71 | return ( 72 |
73 |
74 |
75 |

76 | Information 77 |

78 |

/

79 |

80 | Delivery 81 |

82 |

/

83 |

Payment

84 |
85 | {checkoutStep !== 1 ? : null} 86 | {handleStep()} 87 | 93 |
94 |
95 | 96 |
97 |
98 | ); 99 | }; 100 | 101 | export default CheckoutStep; 102 | -------------------------------------------------------------------------------- /src/components/checkout/checkout-summary.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { PuffLoader } from "react-spinners"; 3 | import * as styles from "../../styles/checkout-summary.module.css"; 4 | import * as itemStyles from "../../styles/cart-view.module.css"; 5 | import { Link } from "gatsby"; 6 | import { formatPrice } from "../../utils/helper-functions"; 7 | import { sum, quantity } from "../../utils/helper-functions"; 8 | import DisplayContext from "../../context/display-context"; 9 | 10 | const CheckoutSummary = ({ cart }) => { 11 | const { orderSummary, updateOrderSummaryDisplay } = 12 | useContext(DisplayContext); 13 | return cart ? ( 14 |
15 |
16 |

17 | Order Summary 18 |

19 |

20 | {cart.items.length > 0 ? cart.items.map(quantity).reduce(sum) : 0}{" "} 21 | {cart.items.length > 0 && cart.items.map(quantity).reduce(sum) === 1 22 | ? "item" 23 | : "items"} 24 |

25 | 31 |
32 |
33 | {cart.items 34 | .sort((a, b) => { 35 | const createdAtA = new Date(a.created_at), 36 | createdAtB = new Date(b.created_at); 37 | 38 | if (createdAtA < createdAtB) return -1; 39 | if (createdAtA > createdAtB) return 1; 40 | return 0; 41 | }) 42 | .map((i) => { 43 | return ( 44 |
45 |
46 | 47 | {/* Replace with a product thumbnail/image */} 48 |
49 | {`${i.title}`} 58 |
59 | 60 |
61 |
62 |
63 |
64 | 65 | {i.title} 66 | 67 |

Size: {i.variant.title}

68 |

69 | Price:{" "} 70 | {formatPrice(i.unit_price, cart.region.currency_code)} 71 |

72 |

Quantity: {i.quantity}

73 |
74 |
75 |
76 |
77 | ); 78 | })} 79 |
80 |
81 |

Subtotal (incl. taxes)

82 | 83 | {cart.region 84 | ? formatPrice(cart.subtotal, cart.region.currency_code) 85 | : 0} 86 | 87 |
88 |
89 |

Shipping

90 | 91 | {cart.region 92 | ? formatPrice(cart.shipping_total, cart.region.currency_code) 93 | : 0} 94 | 95 |
96 |
97 |

Total

98 | 99 | {cart.region ? formatPrice(cart.total, cart.region.currency_code) : 0} 100 | 101 |
102 |
103 | ) : ( 104 |
105 | 106 |
107 | ); 108 | }; 109 | 110 | export default CheckoutSummary; 111 | -------------------------------------------------------------------------------- /src/components/checkout/information-step.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { Form, Formik } from "formik"; 3 | import * as Yup from "yup"; 4 | import PuffLoader from "react-spinners/PuffLoader"; 5 | import * as styles from "../../styles/information-step.module.css"; 6 | import InputField from "./input-field"; 7 | import StoreContext from "../../context/store-context"; 8 | import SelectField from "./select-field"; 9 | 10 | const InformationStep = ({ handleSubmit, savedValues, isProcessing }) => { 11 | const { cart } = useContext(StoreContext); 12 | let Schema = Yup.object().shape({ 13 | first_name: Yup.string() 14 | .min(2, "Too short") 15 | .max(50, "Too long") 16 | .required("Required"), 17 | last_name: Yup.string() 18 | .min(2, "Too short") 19 | .max(50, "Too long") 20 | .required("Required"), 21 | email: Yup.string().email("Invalid email").required("Required"), 22 | address_1: Yup.string() 23 | .required("Required") 24 | .max(45, "Limit on 45 characters"), 25 | address_2: Yup.string().nullable(true).max(45, "Limit on 45 characters"), 26 | country_code: Yup.string().required("Required"), 27 | city: Yup.string().required("Required"), 28 | postal_code: Yup.string().required("Required"), 29 | province: Yup.string().nullable(true), 30 | phone: Yup.string().required("Required"), 31 | }); 32 | 33 | return ( 34 |
35 |

Address

36 | { 53 | const { email, ...rest } = values; 54 | handleSubmit(rest, email); 55 | }} 56 | > 57 | {({ errors, touched, values, setFieldValue }) => ( 58 |
59 | {isProcessing || !cart ? ( 60 |
61 | 62 |
63 | ) : ( 64 | <> 65 |
66 | 73 | 81 |
82 | 89 | 96 | 103 | 109 |
110 | 117 | 124 |
125 | 132 |
133 | 136 |
137 | 138 | )} 139 | 140 | )} 141 |
142 |
143 | ); 144 | }; 145 | 146 | export default InformationStep; 147 | -------------------------------------------------------------------------------- /src/components/checkout/injectable-payment-card.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | import { CardElement, useStripe, useElements } from "@stripe/react-stripe-js"; 3 | import { navigate } from "gatsby"; 4 | import DisplayContext from "../../context/display-context"; 5 | import * as styles from "../../styles/injectable-payment-card.module.css"; 6 | import { BiLeftArrowAlt } from "react-icons/bi"; 7 | 8 | const InjectablePaymentCard = ({ session, onSetPaymentSession }) => { 9 | const stripe = useStripe(); 10 | const elements = useElements(); 11 | const [succeeded, setSucceeded] = useState(false); 12 | const [error, setError] = useState(null); 13 | const [processing, setProcessing] = useState(""); 14 | const [disabled, setDisabled] = useState(true); 15 | const { updateCheckoutStep } = useContext(DisplayContext); 16 | 17 | const handleChange = async (event) => { 18 | setDisabled(event.empty); 19 | setError(event.error ? event.error.message : ""); 20 | }; 21 | 22 | const handleSubmit = async (ev) => { 23 | ev.preventDefault(); 24 | setProcessing(true); 25 | 26 | await onSetPaymentSession(); 27 | 28 | const payload = await stripe.confirmCardPayment( 29 | session.data.client_secret, 30 | { 31 | payment_method: { 32 | card: elements.getElement(CardElement), 33 | }, 34 | } 35 | ); 36 | if (payload.error) { 37 | setError(`${payload.error.message}`); 38 | setProcessing(false); 39 | } else { 40 | setError(null); 41 | setProcessing(false); 42 | setSucceeded(true); 43 | navigate(`/payment`); 44 | } 45 | }; 46 | 47 | return ( 48 |
49 | 54 | {/* Show any error that happens when processing the payment */} 55 | {error && ( 56 |
57 | {error} 58 |
59 | )} 60 |
61 | 67 | 74 |
75 | 76 | ); 77 | }; 78 | 79 | export default InjectablePaymentCard; 80 | -------------------------------------------------------------------------------- /src/components/checkout/input-field.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Field } from "formik"; 3 | import * as styles from "../../styles/input-field.module.css"; 4 | import { MdError } from "react-icons/md"; 5 | 6 | const InputField = ({ id, placeholder, error, errorMsg, type, disabled }) => { 7 | return ( 8 |
9 | {error ? ( 10 |

{errorMsg}

11 | ) : ( 12 | 15 | )} 16 |
19 | 27 | {error && } 28 |
29 |
30 | ); 31 | }; 32 | 33 | export default InputField; 34 | -------------------------------------------------------------------------------- /src/components/checkout/payment-step.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext } from "react"; 2 | import { navigate } from "gatsby"; 3 | import { Elements } from "@stripe/react-stripe-js"; 4 | import StoreContext from "../../context/store-context"; 5 | import InjectablePaymentCard from "./injectable-payment-card"; 6 | import * as styles from "../../styles/injectable-payment-card.module.css"; 7 | import getStripe from "../../utils/stripe"; 8 | 9 | const PaymentStep = () => { 10 | const { cart, createPaymentSession, setPaymentSession } = 11 | useContext(StoreContext); 12 | 13 | useEffect(() => { 14 | createPaymentSession(); 15 | }, []); 16 | 17 | const handlePayment = async () => { 18 | await setPaymentSession("manual").then(() => { 19 | navigate(`/payment`); 20 | }); 21 | }; 22 | 23 | return ( 24 |
25 | {cart && 26 | cart.payment_sessions && 27 | cart.payment_sessions.map((ps) => { 28 | switch (ps.provider_id) { 29 | case "stripe": 30 | return ( 31 | 32 |

Stripe Payment

33 | setPaymentSession("stripe")} 36 | /> 37 |
38 | ); 39 | case "manual": 40 | return ( 41 |
42 |

Test Payment

43 | 50 |
51 | ); 52 | default: 53 | return null; 54 | } 55 | })} 56 |
57 | ); 58 | }; 59 | 60 | export default PaymentStep; 61 | -------------------------------------------------------------------------------- /src/components/checkout/select-field.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Field } from "formik"; 3 | import * as styles from "../../styles/input-field.module.css"; 4 | import { MdError } from "react-icons/md"; 5 | 6 | const SelectField = ({ id, error, errorMsg, type, disabled, options }) => { 7 | return options ? ( 8 |
9 | {error ? ( 10 |

{errorMsg}

11 | ) : ( 12 | 15 | )} 16 |
19 | 27 | {options.map((o) => { 28 | return ( 29 | 32 | ); 33 | })} 34 | 35 | {error && } 36 |
37 |
38 | ) : ( 39 |
40 | ); 41 | }; 42 | 43 | export default SelectField; 44 | -------------------------------------------------------------------------------- /src/components/checkout/shipping-method.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as styles from "../../styles/shipping-method.module.css"; 3 | import { formatPrice } from "../../utils/helper-functions"; 4 | 5 | const ShippingMethod = ({ handleOption, option, chosen }) => { 6 | return ( 7 |
handleOption(option)} 12 | role="button" 13 | tabIndex="0" 14 | > 15 |

{option.name}

16 |

{formatPrice(option.amount, "EUR")}

17 |
18 | ); 19 | }; 20 | 21 | export default ShippingMethod; 22 | -------------------------------------------------------------------------------- /src/components/checkout/shipping-step.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext } from "react"; 2 | import * as styles from "../../styles/shipping-step.module.css"; 3 | import ShippingMethod from "./shipping-method"; 4 | import { BiLeftArrowAlt } from "react-icons/bi"; 5 | import DisplayContext from "../../context/display-context"; 6 | import { isEmpty } from "lodash"; 7 | import StoreContext from "../../context/store-context"; 8 | import { MdError } from "react-icons/md"; 9 | 10 | const ShippingStep = ({ handleDeliverySubmit, isProcessing, cart }) => { 11 | const [shippingOptions, setShippingOptions] = useState([]); 12 | const [selectedOption, setSelectedOption] = useState(); 13 | const [error, setError] = useState(false); 14 | 15 | const { getShippingOptions } = useContext(StoreContext); 16 | const { updateCheckoutStep } = useContext(DisplayContext); 17 | 18 | useEffect(() => { 19 | // Wait until the customer has entered their address information 20 | if (!cart.shipping_address?.country_code) { 21 | return; 22 | } 23 | 24 | getShippingOptions().then((partitioned) => { 25 | setShippingOptions(partitioned); 26 | }); 27 | 28 | //if method is already selected, then preselect 29 | if (cart.shipping_methods.length > 0) { 30 | setSelectedOption(cart.shipping_methods[0].shipping_option); 31 | } 32 | }, [cart, setSelectedOption, getShippingOptions]); 33 | 34 | const handleSelectOption = (o) => { 35 | setSelectedOption(o); 36 | 37 | if (error) { 38 | setError(false); 39 | } 40 | }; 41 | 42 | const handleSubmit = () => { 43 | if (!selectedOption) { 44 | setError(true); 45 | } else { 46 | handleDeliverySubmit(selectedOption); 47 | } 48 | }; 49 | 50 | return ( 51 |
52 |

Delivery

53 | {isEmpty(shippingOptions) || isProcessing ? ( 54 |
loading...
55 | ) : ( 56 |
57 | {shippingOptions.map((so) => { 58 | return ( 59 |
60 | 65 |
66 | ); 67 | })} 68 |
69 | )} 70 |
71 | 72 |

Select a shipping method

73 |
74 |
75 | 81 | 84 |
85 |
86 | ); 87 | }; 88 | 89 | export default ShippingStep; 90 | -------------------------------------------------------------------------------- /src/components/checkout/step-overview.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import DisplayContext from "../../context/display-context"; 3 | import StoreContext from "../../context/store-context"; 4 | import * as styles from "../../styles/step-overview.module.css"; 5 | 6 | const StepOverview = () => { 7 | const { cart } = useContext(StoreContext); 8 | const { checkoutStep, updateCheckoutStep } = useContext(DisplayContext); 9 | return ( 10 |
11 |

Steps

12 |
13 | {cart?.shipping_address ? ( 14 | <> 15 |
16 | Contact 17 |
18 | {cart.shipping_address.first_name}{" "} 19 | {cart.shipping_address.last_name} 20 |
21 | 27 |
28 |
29 | Address 30 |
31 | {cart.shipping_address.address_1}, {cart.shipping_address.city} 32 |
33 | 39 |
40 | 41 | ) : null} 42 | {cart?.shipping_methods[0] && checkoutStep !== 2 ? ( 43 |
44 | Shipping 45 |
46 | {cart.shipping_methods[0].shipping_option.name} 47 |
48 | 54 |
55 | ) : null} 56 |
57 |
58 | ); 59 | }; 60 | 61 | export default StepOverview; 62 | -------------------------------------------------------------------------------- /src/components/layout/blur.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import DisplayContext from "../../context/display-context"; 3 | import * as styles from "../../styles/blur.module.css"; 4 | 5 | const Blur = () => { 6 | const { cartView, updateCartViewDisplay } = useContext(DisplayContext); 7 | return ( 8 |
updateCartViewDisplay()} 11 | onKeyDown={() => updateCartViewDisplay()} 12 | role="button" 13 | tabIndex="-1" 14 | aria-label="Close cart view" 15 | /> 16 | ); 17 | }; 18 | 19 | export default Blur; 20 | -------------------------------------------------------------------------------- /src/components/layout/layout.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from "react"; 2 | import NavBar from "./nav-bar"; 3 | import Blur from "./blur"; 4 | import CartView from "../cart-view/cart-view"; 5 | import DisplayContext from "../../context/display-context"; 6 | import * as styles from "../../styles/layout.module.css"; 7 | import "../../styles/globals.css"; 8 | 9 | const Layout = ({ location, children }) => { 10 | const { cartView } = useContext(DisplayContext); 11 | const [isCheckout, setIsCheckout] = useState(false); 12 | 13 | useEffect(() => { 14 | if ( 15 | location.location.pathname === "/checkout" || 16 | location.location.pathname === "/payment" 17 | ) { 18 | setIsCheckout(true); 19 | } else { 20 | setIsCheckout(false); 21 | } 22 | }, [location]); 23 | 24 | return ( 25 |
26 |
27 | 28 | 29 | 30 | {children} 31 |
32 |
33 | ); 34 | }; 35 | 36 | export default Layout; 37 | -------------------------------------------------------------------------------- /src/components/layout/nav-bar.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "gatsby"; 2 | import React, { useContext } from "react"; 3 | import DisplayContext from "../../context/display-context"; 4 | import StoreContext from "../../context/store-context"; 5 | import { quantity, sum } from "../../utils/helper-functions"; 6 | import { BiShoppingBag } from "react-icons/bi"; 7 | import * as styles from "../../styles/nav-bar.module.css"; 8 | import MedusaLogo from "../../images/medusa-logo.svg"; 9 | 10 | const NavBar = ({ isCheckout }) => { 11 | const { updateCartViewDisplay } = useContext(DisplayContext); 12 | const { cart } = useContext(StoreContext); 13 | 14 | return ( 15 |
16 | 17 | logo 18 | 19 | {!isCheckout ? ( 20 | 27 | ) : null} 28 |
29 | ); 30 | }; 31 | 32 | export default NavBar; 33 | -------------------------------------------------------------------------------- /src/context/display-context.js: -------------------------------------------------------------------------------- 1 | import React, { useReducer } from "react"; 2 | 3 | export const defaultDisplayContext = { 4 | cartView: false, 5 | orderSummary: false, 6 | checkoutStep: 1, 7 | updateCartViewDisplay: () => {}, 8 | updateOrderSummaryDisplay: () => {}, 9 | updateCheckoutStep: () => {}, 10 | dispatch: () => {}, 11 | }; 12 | 13 | const DisplayContext = React.createContext(defaultDisplayContext); 14 | export default DisplayContext; 15 | 16 | const reducer = (state, action) => { 17 | switch (action.type) { 18 | case "updateCartViewDisplay": 19 | return { ...state, cartView: !state.cartView }; 20 | case "updateOrderSummaryDisplay": 21 | return { ...state, orderSummary: !state.orderSummary }; 22 | case "updateCheckoutStep": 23 | return { ...state, checkoutStep: action.payload }; 24 | default: 25 | return state; 26 | } 27 | }; 28 | 29 | export const DisplayProvider = ({ children }) => { 30 | const [state, dispatch] = useReducer(reducer, defaultDisplayContext); 31 | 32 | const updateCartViewDisplay = () => { 33 | dispatch({ type: "updateCartViewDisplay" }); 34 | }; 35 | 36 | const updateOrderSummaryDisplay = () => { 37 | dispatch({ type: "updateOrderSummaryDisplay" }); 38 | }; 39 | 40 | const updateCheckoutStep = (step) => { 41 | dispatch({ type: "updateCheckoutStep", payload: step }); 42 | }; 43 | 44 | return ( 45 | 54 | {children} 55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /src/context/store-context.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useReducer, useRef } from "react"; 2 | import { createClient } from "../utils/client"; 3 | 4 | export const defaultStoreContext = { 5 | adding: false, 6 | cart: { 7 | items: [], 8 | }, 9 | order: {}, 10 | products: [], 11 | currencyCode: "eur", 12 | /** 13 | * 14 | * @param {*} variantId 15 | * @param {*} quantity 16 | * @returns 17 | */ 18 | addVariantToCart: async () => {}, 19 | createCart: async () => {}, 20 | removeLineItem: async () => {}, 21 | updateLineItem: async () => {}, 22 | setShippingMethod: async () => {}, 23 | updateAddress: async () => {}, 24 | createPaymentSession: async () => {}, 25 | completeCart: async () => {}, 26 | retrieveOrder: async () => {}, 27 | dispatch: () => {}, 28 | }; 29 | 30 | const StoreContext = React.createContext(defaultStoreContext); 31 | export default StoreContext; 32 | 33 | const reducer = (state, action) => { 34 | switch (action.type) { 35 | case "setCart": 36 | return { 37 | ...state, 38 | cart: action.payload, 39 | currencyCode: action.payload.region.currency_code, 40 | }; 41 | case "setOrder": 42 | return { 43 | ...state, 44 | order: action.payload, 45 | }; 46 | case "setProducts": 47 | return { 48 | ...state, 49 | products: action.payload, 50 | }; 51 | default: 52 | return state; 53 | } 54 | }; 55 | 56 | const client = createClient(); 57 | 58 | export const StoreProvider = ({ children }) => { 59 | const [state, dispatch] = useReducer(reducer, defaultStoreContext); 60 | const stateCartId = useRef(); 61 | 62 | useEffect(() => { 63 | stateCartId.current = state.cart.id; 64 | }, [state.cart]); 65 | 66 | useEffect(() => { 67 | let cartId; 68 | if (localStorage) { 69 | cartId = localStorage.getItem("cart_id"); 70 | } 71 | 72 | if (cartId) { 73 | client.carts.retrieve(cartId).then((data) => { 74 | dispatch({ type: "setCart", payload: data.cart }); 75 | }); 76 | } else { 77 | client.carts.create(cartId).then((data) => { 78 | dispatch({ type: "setCart", payload: data.cart }); 79 | if (localStorage) { 80 | localStorage.setItem("cart_id", data.cart.id); 81 | } 82 | }); 83 | } 84 | 85 | client.products.list().then((data) => { 86 | dispatch({ type: "setProducts", payload: data.products }); 87 | }); 88 | }, []); 89 | 90 | const createCart = () => { 91 | if (localStorage) { 92 | localStorage.removeItem("cart_id"); 93 | } 94 | client.carts.create().then((data) => { 95 | dispatch({ type: "setCart", payload: data.cart }); 96 | }); 97 | }; 98 | 99 | const setPaymentSession = async (provider) => { 100 | client.carts 101 | .setPaymentSession(state.cart.id, { 102 | provider_id: provider, 103 | }) 104 | .then((data) => { 105 | dispatch({ type: "setCart", payload: data.cart }); 106 | return data; 107 | }); 108 | }; 109 | 110 | const addVariantToCart = async ({ variantId, quantity }) => { 111 | client.carts.lineItems 112 | .create(state.cart.id, { 113 | variant_id: variantId, 114 | quantity: quantity, 115 | }) 116 | .then((data) => { 117 | dispatch({ type: "setCart", payload: data.cart }); 118 | }); 119 | }; 120 | 121 | const removeLineItem = async (lineId) => { 122 | client.carts.lineItems.delete(state.cart.id, lineId).then((data) => { 123 | dispatch({ type: "setCart", payload: data.cart }); 124 | }); 125 | }; 126 | 127 | const updateLineItem = async ({ lineId, quantity }) => { 128 | client.carts.lineItems 129 | .update(state.cart.id, lineId, { quantity: quantity }) 130 | .then((data) => { 131 | dispatch({ type: "setCart", payload: data.cart }); 132 | }); 133 | }; 134 | 135 | const getShippingOptions = async () => { 136 | const data = await client.shippingOptions 137 | .listCartOptions(state.cart.id) 138 | .then((data) => data); 139 | 140 | if (data) { 141 | return data.shipping_options; 142 | } else { 143 | return undefined; 144 | } 145 | }; 146 | 147 | const setShippingMethod = async (id) => { 148 | return await client.carts 149 | .addShippingMethod(state.cart.id, { 150 | option_id: id, 151 | }) 152 | .then((data) => { 153 | dispatch({ type: "setCart", payload: data.cart }); 154 | return data; 155 | }); 156 | }; 157 | 158 | const createPaymentSession = async () => { 159 | return await client.carts 160 | .createPaymentSessions(state.cart.id) 161 | .then((data) => { 162 | dispatch({ type: "setCart", payload: data.cart }); 163 | return data; 164 | }); 165 | }; 166 | 167 | const completeCart = async () => { 168 | const data = await client.carts 169 | .complete(state.cart.id) 170 | .then((data) => data); 171 | 172 | if (data) { 173 | return data.data; 174 | } else { 175 | return undefined; 176 | } 177 | }; 178 | 179 | const retrieveOrder = async (orderId) => { 180 | const data = await client.orders.retrieve(orderId).then((data) => data); 181 | 182 | if (data) { 183 | return data.order; 184 | } else { 185 | return undefined; 186 | } 187 | }; 188 | 189 | const updateAddress = (address, email) => { 190 | client.carts 191 | .update(state.cart.id, { 192 | shipping_address: address, 193 | billing_address: address, 194 | email: email, 195 | }) 196 | .then((data) => { 197 | dispatch({ type: "setCart", payload: data.cart }); 198 | }); 199 | }; 200 | 201 | return ( 202 | 219 | {children} 220 | 221 | ); 222 | }; 223 | -------------------------------------------------------------------------------- /src/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medusajs/gatsby-starter-medusa-simple/796275c0612a63e2e136ae1a29ede7522ffbb2d0/src/images/icon.png -------------------------------------------------------------------------------- /src/images/medusa-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medusajs/gatsby-starter-medusa-simple/796275c0612a63e2e136ae1a29ede7522ffbb2d0/src/images/medusa-logo.jpg -------------------------------------------------------------------------------- /src/images/medusa-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | medusa-logo-full-colour-rgb 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/pages/404.js: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Link } from "gatsby" 3 | 4 | // styles 5 | const pageStyles = { 6 | color: "#232129", 7 | padding: "96px", 8 | fontFamily: "-apple-system, Roboto, sans-serif, serif", 9 | } 10 | const headingStyles = { 11 | marginTop: 0, 12 | marginBottom: 64, 13 | maxWidth: 320, 14 | } 15 | 16 | const paragraphStyles = { 17 | marginBottom: 48, 18 | } 19 | const codeStyles = { 20 | color: "#8A6534", 21 | padding: 4, 22 | backgroundColor: "#FFF4DB", 23 | fontSize: "1.25rem", 24 | borderRadius: 4, 25 | } 26 | 27 | // markup 28 | const NotFoundPage = () => { 29 | return ( 30 |
31 | Not found 32 |

Page not found

33 |

34 | Sorry{" "} 35 | 36 | 😔 37 | {" "} 38 | we couldn’t find what you were looking for. 39 |
40 | {process.env.NODE_ENV === "development" ? ( 41 | <> 42 |
43 | Try creating a page in src/pages/. 44 |
45 | 46 | ) : null} 47 |
48 | Go home. 49 |

50 |
51 | ) 52 | } 53 | 54 | export default NotFoundPage 55 | -------------------------------------------------------------------------------- /src/pages/checkout.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import CheckoutStep from "../components/checkout/checkout-step"; 3 | 4 | const Checkout = () => { 5 | return ; 6 | }; 7 | 8 | export default Checkout; 9 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { FaGithub } from "react-icons/fa"; 3 | import StoreContext from "../context/store-context"; 4 | import { graphql } from "gatsby"; 5 | import * as styles from "../styles/home.module.css"; 6 | import { Link } from "gatsby"; 7 | import { formatPrices } from "../utils/format-price"; 8 | 9 | // markup 10 | const IndexPage = ({ data }) => { 11 | const { cart, products } = useContext(StoreContext); 12 | 13 | return ( 14 |
15 |
16 |
17 |

18 | Medusa + Gatsby Starter{" "} 19 | 20 | 🚀 21 | 22 |

23 |

24 | Build blazing-fast client applications on top of a modular headless 25 | commerce engine. Integrate seamlessly with any 3rd party tools for a 26 | best-in-breed commerce stack. 27 |

28 |
29 |
30 | v{data.site.siteMetadata.version} 31 |
32 | 38 |
42 | Medusa 43 |
44 |
45 | 51 |
55 | Gatsby 56 |
57 |
58 | 64 |
68 | Stripe 69 |
70 |
71 |
72 | 104 |
105 |
106 |

Demo Products

107 |
108 | {products && 109 | products.map((p) => { 110 | return ( 111 |
112 | 113 |
114 |

{p.title}

115 |

{formatPrices(cart, p.variants[0])}

116 |
117 | 118 |
119 | ); 120 | })} 121 |
122 |
123 |
124 |
125 | ); 126 | }; 127 | 128 | export const query = graphql` 129 | query VersionQuery { 130 | site { 131 | siteMetadata { 132 | version 133 | } 134 | } 135 | } 136 | `; 137 | 138 | export default IndexPage; 139 | -------------------------------------------------------------------------------- /src/pages/payment.js: -------------------------------------------------------------------------------- 1 | import { Link } from "gatsby"; 2 | import React, { useContext, useEffect, useState } from "react"; 3 | import StoreContext from "../context/store-context"; 4 | import * as itemStyles from "../styles/cart-view.module.css"; 5 | import * as styles from "../styles/payment.module.css"; 6 | import { formatPrice } from "../utils/helper-functions"; 7 | 8 | const style = { 9 | height: "100vh", 10 | width: "100%", 11 | display: "flex", 12 | flexDirection: "column", 13 | justifyContent: "center", 14 | alignItems: "center", 15 | textAlign: "center", 16 | }; 17 | 18 | const Payment = () => { 19 | const [order, setOrder] = useState(); 20 | const { cart, completeCart, createCart } = useContext(StoreContext); 21 | 22 | useEffect(() => { 23 | if (cart.items.length > 0) { 24 | completeCart().then((order) => { 25 | setOrder(order); 26 | createCart(); 27 | }); 28 | } 29 | }, []); 30 | 31 | return !order ? ( 32 |
33 |

Hang on while we validate your payment...

34 |
35 | ) : ( 36 |
37 |
38 |

Order Summary

39 |

Thank you for your order!

40 |
41 |
42 | {order.items 43 | .sort((a, b) => { 44 | const createdAtA = new Date(a.created_at), 45 | createdAtB = new Date(b.created_at); 46 | 47 | if (createdAtA < createdAtB) return -1; 48 | if (createdAtA > createdAtB) return 1; 49 | return 0; 50 | }) 51 | .map((i) => { 52 | return ( 53 |
54 |
55 |
56 | 57 | {/* Replace with a product thumbnail/image */} 58 |
59 | {`${i.title}`} 68 |
69 | 70 |
71 |
72 |
73 |
74 | 75 | {i.title} 76 | 77 |

78 | Size: {i.variant.title} 79 |

80 |

81 | Price:{" "} 82 | {formatPrice(i.unit_price, order.currency_code)} 83 |

84 |

85 | Quantity: {i.quantity} 86 |

87 |
88 |
89 |
90 |
91 |
92 | ); 93 | })} 94 |
95 |
96 |
97 |
Subtotal
98 |
{formatPrice(order.subtotal, order.region.currency_code)}
99 |
100 |
101 |
Shipping
102 |
103 | {formatPrice(order.shipping_total, order.region.currency_code)} 104 |
105 |
106 |
107 |
Total
108 |
{formatPrice(order.total, order.region.currency_code)}
109 |
110 |
111 |
112 |

An order comfirmation will be sent to you at {order.email}

113 |
114 |
115 | ); 116 | }; 117 | 118 | export default Payment; 119 | -------------------------------------------------------------------------------- /src/pages/product/[id].js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from "react"; 2 | import { BiShoppingBag } from "react-icons/bi"; 3 | import StoreContext from "../../context/store-context"; 4 | import * as styles from "../../styles/product.module.css"; 5 | import { createClient } from "../../utils/client"; 6 | import { formatPrices } from "../../utils/format-price"; 7 | import { getSlug, resetOptions } from "../../utils/helper-functions"; 8 | 9 | const Product = ({ location }) => { 10 | const { cart, addVariantToCart } = useContext(StoreContext); 11 | const [options, setOptions] = useState({ 12 | variantId: "", 13 | quantity: 0, 14 | size: "", 15 | }); 16 | 17 | const [product, setProduct] = useState(undefined); 18 | const client = createClient(); 19 | 20 | useEffect(() => { 21 | const getProduct = async () => { 22 | const slug = getSlug(location.pathname); 23 | const response = await client.products.retrieve(slug); 24 | setProduct(response.product); 25 | }; 26 | 27 | getProduct(); 28 | }, [location.pathname]); 29 | 30 | useEffect(() => { 31 | if (product) { 32 | setOptions(resetOptions(product)); 33 | } 34 | }, [product]); 35 | 36 | const handleQtyChange = (action) => { 37 | if (action === "inc") { 38 | if ( 39 | options.quantity < 40 | product.variants.find(({ id }) => id === options.variantId) 41 | .inventory_quantity 42 | ) 43 | setOptions({ 44 | variantId: options.variantId, 45 | quantity: options.quantity + 1, 46 | size: options.size, 47 | }); 48 | } 49 | if (action === "dec") { 50 | if (options.quantity > 1) 51 | setOptions({ 52 | variantId: options.variantId, 53 | quantity: options.quantity - 1, 54 | size: options.size, 55 | }); 56 | } 57 | }; 58 | 59 | const handleAddToBag = () => { 60 | addVariantToCart({ 61 | variantId: options.variantId, 62 | quantity: options.quantity, 63 | }); 64 | if (product) setOptions(resetOptions(product)); 65 | }; 66 | 67 | return product && cart.id ? ( 68 |
69 |
70 |
71 | {`${product.title}`} 76 |
77 |
78 |
79 | 80 |
81 |
82 |

{product.title}

83 |
84 |

{formatPrices(cart, product.variants[0])}

85 |
86 |

Select Size

87 |
88 | {product.variants 89 | .slice(0) 90 | .reverse() 91 | .map((v) => { 92 | return ( 93 | 108 | ); 109 | })} 110 |
111 |
112 |
113 |

Select Quantity

114 |
115 | 121 | {options.quantity} 122 | 128 |
129 |
130 | 134 |
135 |
136 | 137 |
138 |
139 |

{product.description}

140 |
141 |
142 |
143 |
144 |
145 | ) : null; 146 | }; 147 | 148 | export default Product; 149 | -------------------------------------------------------------------------------- /src/styles/blur.module.css: -------------------------------------------------------------------------------- 1 | .blur { 2 | position: absolute; 3 | top: 0; 4 | bottom: 0; 5 | right: 0; 6 | width: 100vw; 7 | height: 100vh; 8 | background: rgba(0, 0, 0, 0.5); 9 | opacity: 0; 10 | visibility: hidden; 11 | cursor: pointer; 12 | transition: all 0.2s ease-in-out; 13 | z-index: 10; 14 | } 15 | 16 | .active { 17 | opacity: 1; 18 | visibility: visible; 19 | } -------------------------------------------------------------------------------- /src/styles/cart-view.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | --py: 35px; 3 | 4 | position: fixed; 5 | min-width: 460px; 6 | height: 100vh; 7 | max-height: 100vh; 8 | -webkit-box-shadow: var(--shade); 9 | box-shadow: var(--shade); 10 | background: white; 11 | z-index: 11; 12 | display: flex; 13 | flex-direction: column; 14 | overflow: hidden; 15 | justify-content: space-between; 16 | right: -460px; 17 | top: 0; 18 | transition: -webkit-transform 0.5s ease; 19 | transition: transform 0.5s ease; 20 | -webkit-transform: translateX(110%); 21 | -ms-transform: translateX(110%); 22 | transform: translateX(110%); 23 | } 24 | 25 | .active { 26 | -webkit-transform: translateX(-460px); 27 | -ms-transform: translateX(-460px); 28 | transform: translateX(-460px); 29 | } 30 | 31 | .top { 32 | display: flex; 33 | align-items: center; 34 | justify-content: space-between; 35 | padding: 15px var(--py); 36 | } 37 | 38 | .closebtn { 39 | background: transparent; 40 | border: none; 41 | cursor: pointer; 42 | } 43 | 44 | .subtotal { 45 | display: flex; 46 | align-items: center; 47 | justify-content: space-between; 48 | padding: 15px var(--py); 49 | } 50 | 51 | .bottom { 52 | padding: 15px var(--py); 53 | } 54 | 55 | .overview { 56 | flex-grow: 1; 57 | overflow-y: scroll; 58 | scrollbar-width: thin; 59 | scrollbar-color: var(--logo-color-400) transparent; 60 | } 61 | 62 | .overview::-webkit-scrollbar { 63 | width: 12px; 64 | border-radius: 12px; 65 | } 66 | 67 | .overview::-webkit-scrollbar-track { 68 | background: transparent; 69 | border-radius: 12px; 70 | } 71 | 72 | .overview::-webkit-scrollbar-thumb { 73 | background-color: var(--logo-color-400); 74 | border-radius: 20px; 75 | border: 1px solid var(--bg); 76 | } 77 | 78 | .product { 79 | padding: 24px var(--py) 0; 80 | margin-top: 0; 81 | position: relative; 82 | min-height: 120px; 83 | display: flex; 84 | } 85 | 86 | .mid { 87 | display: flex; 88 | flex-direction: column; 89 | } 90 | 91 | .price { 92 | margin: 0; 93 | } 94 | 95 | .selector { 96 | display: flex; 97 | align-items: center; 98 | } 99 | 100 | .product figure { 101 | background: var(--bg); 102 | width: 126px; 103 | height: 189px; 104 | margin: 0; 105 | margin-right: 1rem; 106 | } 107 | 108 | .placeholder { 109 | width: 100%; 110 | height: 100%; 111 | cursor: pointer; 112 | } 113 | 114 | .controls { 115 | display: flex; 116 | flex-direction: column; 117 | justify-content: space-around; 118 | } 119 | 120 | .remove { 121 | background: transparent; 122 | border: none; 123 | cursor: pointer; 124 | padding: 0; 125 | text-align: left; 126 | text-decoration: underline; 127 | color: lightgrey; 128 | transition: color 0.1s ease-in; 129 | } 130 | 131 | .remove:hover { 132 | color: var(--logo-color-900); 133 | } 134 | 135 | .size { 136 | font-size: var(--fz-sm); 137 | color: grey; 138 | } 139 | 140 | .ticker { 141 | width: 25px; 142 | text-align: center; 143 | user-select: none; 144 | } 145 | 146 | .qtybtn { 147 | background: transparent; 148 | border: transparent; 149 | color: grey; 150 | transition: color 0.1s ease-in; 151 | cursor: pointer; 152 | } 153 | 154 | .qtybtn:hover { 155 | color: var(--logo-color-900); 156 | } 157 | 158 | .checkoutbtn { 159 | width: 100%; 160 | font-size: 1.125rem; 161 | min-height: 3rem; 162 | padding: 0.5rem 0; 163 | align-self: center; 164 | display: inline-flex; 165 | align-items: center; 166 | justify-content: center; 167 | background: var(--logo-color-900); 168 | color: white; 169 | border-radius: 8px; 170 | transition: background 0.2s ease-in; 171 | font-weight: 500; 172 | cursor: pointer; 173 | border: none; 174 | } 175 | 176 | .checkoutbtn:hover { 177 | background: var(--logo-color-1000); 178 | } 179 | 180 | @media (max-width: 876px) { 181 | 182 | .container { 183 | width: 100%; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/styles/checkout-step.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | height: 100vh; 4 | } 5 | 6 | .steps { 7 | width: 60%; 8 | height: 100vh; 9 | padding: 110px 88px 28px; 10 | display: flex; 11 | flex-direction: column; 12 | overflow-y: scroll; 13 | scrollbar-width: thin; 14 | scrollbar-color: var(--logo-color-400) transparent; 15 | } 16 | 17 | .summary { 18 | width: 40%; 19 | display: flex; 20 | flex-direction: column; 21 | } 22 | 23 | .orderBtn { 24 | width: 100%; 25 | font-size: 1rem; 26 | min-height: 3rem; 27 | padding: 0.5rem 0; 28 | align-self: center; 29 | display: inline-flex; 30 | align-items: center; 31 | justify-content: center; 32 | border-radius: 8px; 33 | transition: background 0.2s ease-in; 34 | font-weight: 500; 35 | cursor: pointer; 36 | border: none; 37 | display: none; 38 | justify-self: flex-end; 39 | background: transparent; 40 | } 41 | 42 | .orderBtn:hover { 43 | background: var(--logo-color-100); 44 | } 45 | 46 | .breadcrumbs { 47 | display: flex; 48 | } 49 | 50 | .breadcrumbs p { 51 | margin-right: 0.5rem; 52 | color: grey; 53 | transition: color 0.2s ease-in; 54 | } 55 | 56 | .breadcrumbs p:last-child { 57 | margin-right: 0; 58 | } 59 | 60 | .breadcrumbs p.activeStep { 61 | color: black; 62 | } 63 | 64 | @media (max-width: 876px) { 65 | .container { 66 | flex-direction: column; 67 | } 68 | .steps { 69 | padding: 0px 22px; 70 | width: 100%; 71 | height: 100%; 72 | } 73 | .breadcrumbs { 74 | margin-top: 6rem; 75 | } 76 | .orderBtn { 77 | margin-bottom: 2rem; 78 | display: block; 79 | } 80 | } -------------------------------------------------------------------------------- /src/styles/checkout-summary.module.css: -------------------------------------------------------------------------------- 1 | .spinnerContainer { 2 | width: 100%; 3 | height: 100vh; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | } 8 | 9 | .container { 10 | --py: 35px; 11 | 12 | position: fixed; 13 | width: 40%; 14 | height: 100vh; 15 | max-height: 100vh; 16 | -webkit-box-shadow: var(--shade); 17 | box-shadow: var(--shade); 18 | background: white; 19 | z-index: 11; 20 | display: flex; 21 | flex-direction: column; 22 | overflow: hidden; 23 | justify-content: space-between; 24 | right: 0; 25 | position: fixed; 26 | top: 0; 27 | } 28 | 29 | .closeBtn { 30 | background: transparent; 31 | border: none; 32 | cursor: pointer; 33 | display: none; 34 | } 35 | 36 | .breakdown { 37 | display: flex; 38 | align-items: center; 39 | justify-content: space-between; 40 | padding: 10px var(--py); 41 | } 42 | 43 | .total { 44 | display: flex; 45 | align-items: center; 46 | justify-content: space-between; 47 | padding: 10px var(--py) 20px; 48 | font-weight: 700; 49 | } 50 | 51 | .total p { 52 | margin: 0; 53 | } 54 | 55 | .breakdown p { 56 | margin: 0; 57 | } 58 | 59 | @media (max-width: 876px) { 60 | .container { 61 | --py: 35px; 62 | 63 | position: fixed; 64 | height: calc(100vh - 20px); 65 | max-height: calc(100vh - 20px); 66 | -webkit-box-shadow: var(--shade); 67 | box-shadow: var(--shade); 68 | background: white; 69 | z-index: 11; 70 | display: flex; 71 | flex-direction: column; 72 | overflow: hidden; 73 | justify-content: space-between; 74 | top: 20px; 75 | bottom: 0; 76 | border-radius: 8px; 77 | transition: -webkit-transform 0.5s ease; 78 | transition: transform 0.5s ease; 79 | -webkit-transform: translateY(110%); 80 | -ms-transform: translateY(110%); 81 | transform: translateY(110%); 82 | width: 100%; 83 | transition: -webkit-transform 0.5s ease; 84 | transition: transform 0.5s ease; 85 | } 86 | 87 | .active { 88 | -webkit-transform: translateY(0px); 89 | -ms-transform: translateY(0px); 90 | transform: translateY(0px); 91 | } 92 | 93 | .closeBtn { 94 | display: block; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --logo-color-1000: #30363d; 3 | --logo-color-900: #454b54; 4 | --logo-color-400: #a3b1c7; 5 | --logo-color-100: #454b5411; 6 | --bg: #f7f9f9; 7 | 8 | /* Font sizes */ 9 | --fz-s: 12px; 10 | --fz-sm: 14px; 11 | --fz-m: 16px; 12 | --fz-ml: 18px; 13 | --fz-l: 22px; 14 | --fz-xl: 24px; 15 | 16 | /* box-shadow */ 17 | --shade: 0 1px 5px rgba(0, 0, 0, 0.2); 18 | 19 | /* Nav */ 20 | --nav-height: 68px; 21 | } 22 | 23 | * { 24 | box-sizing: border-box; 25 | } 26 | 27 | html, 28 | body { 29 | padding: 0; 30 | margin: 0; 31 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 32 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 33 | } 34 | 35 | a { 36 | color: inherit; 37 | text-decoration: none; 38 | } 39 | 40 | button { 41 | cursor: pointer; 42 | } 43 | 44 | button:disabled, 45 | button:disabled:hover { 46 | cursor: not-allowed; 47 | background: lightgrey; 48 | color: black; 49 | } 50 | 51 | * { 52 | box-sizing: border-box; 53 | } 54 | -------------------------------------------------------------------------------- /src/styles/home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 88px; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | 10 | .main { 11 | padding: 5rem 0; 12 | flex: 1; 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: center; 16 | align-items: center; 17 | } 18 | 19 | .title { 20 | margin: 0; 21 | line-height: 1.15; 22 | font-size: clamp(2rem, 8vw, 4rem); 23 | } 24 | 25 | .description { 26 | line-height: 1.5; 27 | font-size: clamp(1rem, 2vw, 1.5rem); 28 | } 29 | 30 | .tag { 31 | border-radius: 5px; 32 | padding: 0.75rem; 33 | font-size: var(--fz-s); 34 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 35 | Bitstream Vera Sans Mono, Courier New, monospace; 36 | margin-right: 1rem; 37 | } 38 | 39 | .grid { 40 | display: flex; 41 | align-items: center; 42 | justify-content: center; 43 | flex-wrap: wrap; 44 | } 45 | 46 | .products { 47 | display: flex; 48 | flex-direction: column; 49 | align-items: flex-start; 50 | width: 100%; 51 | margin-top: 3rem; 52 | } 53 | 54 | .card { 55 | margin-right: 1rem; 56 | padding: 1.5rem; 57 | text-align: left; 58 | color: inherit; 59 | text-decoration: none; 60 | border: 1px solid #eaeaea; 61 | border-radius: 10px; 62 | transition: color 0.15s ease, border-color 0.15s ease; 63 | } 64 | 65 | .card:hover, 66 | .card:focus, 67 | .card:active { 68 | color: var(--logo-color-900); 69 | border-color: var(--logo-color-900); 70 | } 71 | 72 | .card h2 { 73 | margin: 0 0 .5rem 0; 74 | font-size: 1.25rem; 75 | } 76 | 77 | .card p { 78 | margin: 0; 79 | font-size: 1rem; 80 | line-height: 1.5; 81 | } 82 | 83 | .logo { 84 | height: 1em; 85 | margin-left: 0.5rem; 86 | } 87 | 88 | .hero { 89 | text-align: left; 90 | max-width: 800px; 91 | } 92 | 93 | .links { 94 | display: flex; 95 | align-items: center; 96 | } 97 | 98 | .btn { 99 | font-size: 1.125rem; 100 | min-height: 3rem; 101 | min-width: 3rem; 102 | padding: 0.5rem 1.25rem; 103 | align-self: center; 104 | display: inline-flex; 105 | align-items: center; 106 | background: var(--logo-color-900); 107 | color: white; 108 | border-radius: 8px; 109 | transition: background .2s ease-in; 110 | font-weight: 500; 111 | border: none; 112 | } 113 | 114 | .btn:hover { 115 | background: var(--logo-color-1000); 116 | } 117 | 118 | .btn svg { 119 | margin-left: 0.5rem; 120 | font-size: var(--fz-l); 121 | } 122 | 123 | .btn:hover svg { 124 | transform: scale(1.1); 125 | transform-origin: center; 126 | -webkit-animation: heartbeat 1.5s infinite both; 127 | animation: heartbeat 1.5s infinite both; 128 | } 129 | 130 | .links .btn:first-child { 131 | margin-right: 1rem; 132 | } 133 | 134 | .links .btn:last-child { 135 | background: transparent; 136 | color: black; 137 | font-weight: 400; 138 | } 139 | 140 | .links .btn:last-child:hover { 141 | background: var(--logo-color-100); 142 | } 143 | 144 | .tags { 145 | display: flex; 146 | align-items: center; 147 | flex-wrap: nowrap; 148 | margin-bottom: 3rem; 149 | } 150 | 151 | @media (max-width: 876px) { 152 | .grid { 153 | width: 100%; 154 | flex-direction: column; 155 | } 156 | 157 | .container { 158 | padding: 0 22px; 159 | } 160 | 161 | .card { 162 | width: 100%; 163 | margin-right: 0; 164 | margin-bottom: 1rem; 165 | } 166 | } 167 | 168 | @-webkit-keyframes heartbeat { 169 | from { 170 | -webkit-transform: scale(1.0); 171 | } 172 | 20% { 173 | -webkit-transform: scale(1.0); 174 | } 175 | 50% { 176 | -webkit-transform: scale(.90); 177 | } 178 | 100% { 179 | -webkit-transform: scale(1.0); 180 | } 181 | } 182 | @keyframes heartbeat { 183 | from { 184 | -webkit-transform: scale(1.0); 185 | transform: scale(1.0); 186 | } 187 | 20% { 188 | -webkit-transform: scale(1.0); 189 | transform: scale(1.0); 190 | } 191 | 50% { 192 | -webkit-transform: scale(.90); 193 | transform: scale(.90); 194 | } 195 | 100% { 196 | -webkit-transform: scale(1.0); 197 | transform: scale(1.0); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/styles/information-step.module.css: -------------------------------------------------------------------------------- 1 | .styledform { 2 | width: 100%; 3 | } 4 | 5 | .sharedrow { 6 | display: flex; 7 | } 8 | 9 | .sharedrow div:first-of-type { 10 | margin-left: 0; 11 | margin-right: 10px; 12 | } 13 | 14 | .sharedrow div { 15 | margin-left: 10px; 16 | margin-right: 10px; 17 | } 18 | 19 | .sharedrow div:last-of-type { 20 | margin-right: 0; 21 | } 22 | 23 | .fieldcontainer { 24 | width: 100%; 25 | background: white; 26 | border-radius: 8px; 27 | border: 1px solid lightgrey; 28 | margin: 5px 0; 29 | display: flex; 30 | align-items: center; 31 | } 32 | 33 | .styledfield { 34 | height: 40px; 35 | width: 100%; 36 | padding: 10px; 37 | border-radius: 8px; 38 | background: transparent; 39 | border: none; 40 | } 41 | 42 | .formbtn { 43 | font-size: 1.125rem; 44 | min-height: 3rem; 45 | min-width: 3rem; 46 | padding: 0.5rem 1.25rem; 47 | align-self: center; 48 | display: inline-flex; 49 | align-items: center; 50 | background: var(--logo-color-900); 51 | color: white; 52 | border-radius: 8px; 53 | transition: background .2s ease-in; 54 | font-weight: 500; 55 | border: none; 56 | } 57 | 58 | .formbtn:hover { 59 | background: var(--logo-color-1000); 60 | } 61 | 62 | .spinner { 63 | display: flex; 64 | align-items: center; 65 | justify-content: center; 66 | width: 100%; 67 | padding: 4rem; 68 | } 69 | 70 | .btncontainer { 71 | margin-top: 1.5rem; 72 | display: flex; 73 | justify-content: flex-end; 74 | align-items: center; 75 | } -------------------------------------------------------------------------------- /src/styles/injectable-payment-card.module.css: -------------------------------------------------------------------------------- 1 | .cardForm { 2 | background: white; 3 | box-shadow: var(--shade); 4 | padding: 2rem 2rem; 5 | border-radius: 8px; 6 | } 7 | 8 | .stepBack { 9 | background: transparent; 10 | border: none; 11 | display: flex; 12 | align-items: center; 13 | padding: 0; 14 | font-size: var(--fz-m); 15 | } 16 | 17 | .stepBack svg { 18 | margin-right: .5rem; 19 | } 20 | 21 | .controls { 22 | display: flex; 23 | align-items: center; 24 | justify-content: space-between; 25 | margin-top: 2rem; 26 | } 27 | 28 | .payBtn { 29 | font-size: 1.125rem; 30 | min-height: 3rem; 31 | min-width: 3rem; 32 | padding: 0.5rem 1.25rem; 33 | align-self: center; 34 | display: inline-flex; 35 | align-items: center; 36 | background: var(--logo-color-900); 37 | color: white; 38 | border-radius: 8px; 39 | transition: background .2s ease-in; 40 | font-weight: 500; 41 | border: none; 42 | } 43 | 44 | .payBtn:hover { 45 | background: var(--logo-color-1000); 46 | } -------------------------------------------------------------------------------- /src/styles/input-field.module.css: -------------------------------------------------------------------------------- 1 | .colors { 2 | --error-900: #DB5461; 3 | --error-400: #d67b84; 4 | } 5 | 6 | .container { 7 | width: 100%; 8 | display: flex; 9 | align-items: baseline; 10 | flex-direction: column; 11 | margin: 5px 0; 12 | } 13 | 14 | .fieldcontainer { 15 | width: 100%; 16 | background: white; 17 | border-radius: 8px; 18 | border: 1px solid lightgrey; 19 | display: flex; 20 | align-items: center; 21 | margin-top: .1rem; 22 | position: relative; 23 | transition: all .1s ease-in; 24 | } 25 | 26 | .errorfield { 27 | --error-900: #f0ada6; 28 | --error-400: #fef1f2; 29 | border-color: var(--error-900); 30 | background: var(--error-400); 31 | } 32 | 33 | .errortext { 34 | margin: 0; 35 | align-self: flex-end; 36 | font-size: var(--fz-s); 37 | color: #e07367; 38 | } 39 | 40 | .erroricon { 41 | color: #eb948b; 42 | font-size: 18px; 43 | margin-right: 10px; 44 | position: absolute; 45 | right: 0; 46 | } 47 | 48 | .fill { 49 | color: transparent; 50 | margin: 0; 51 | font-size: var(--fz-s); 52 | } 53 | 54 | .styledfield { 55 | height: 40px; 56 | width: 100%; 57 | border-radius: 8px; 58 | background: transparent; 59 | border: none; 60 | padding: 10px; 61 | outline: none; 62 | } 63 | 64 | .styledselect { 65 | height: 40px; 66 | width: 100%; 67 | border-radius: 8px; 68 | background: transparent; 69 | border: none; 70 | padding: 10px; 71 | outline: none; 72 | -webkit-appearance: none; 73 | } 74 | 75 | .fetching { 76 | height: 40px; 77 | width: 100%; 78 | border-radius: 8px; 79 | background: transparent; 80 | border: none; 81 | padding: 10px; 82 | outline: none; 83 | background: var(--logo-color-100); 84 | } -------------------------------------------------------------------------------- /src/styles/layout.module.css: -------------------------------------------------------------------------------- 1 | .noscroll { 2 | overflow: hidden; 3 | height: 100vh; 4 | } 5 | -------------------------------------------------------------------------------- /src/styles/nav-bar.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | padding: 20px 88px; 6 | position: absolute; 7 | width: 100%; 8 | } 9 | 10 | .container h1 { 11 | margin: 0; 12 | } 13 | 14 | .logo { 15 | font-size: var(--fz-xl); 16 | color: var(--logo-color-900); 17 | } 18 | 19 | .btn { 20 | border: none; 21 | background: transparent; 22 | display: flex; 23 | align-items: center; 24 | font-size: var(--fz-sm); 25 | cursor: pointer; 26 | } 27 | 28 | .btn span { 29 | margin-left: 0.75rem; 30 | margin-right: 0.75rem; 31 | } 32 | 33 | .btn svg { 34 | font-size: var(--fz-ml); 35 | } 36 | 37 | @media (max-width: 876px) { 38 | .container { 39 | padding: 20px 22px; 40 | } 41 | } -------------------------------------------------------------------------------- /src/styles/payment.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | margin: 0 auto; 6 | padding: 0 88px; 7 | max-width: 560px; 8 | min-height: 100vh; 9 | margin-bottom: 2rem; 10 | } 11 | 12 | .header { 13 | border-bottom: 1px solid var(--logo-color-100); 14 | margin-top: 4rem; 15 | } 16 | 17 | .items { 18 | padding: 22px 0; 19 | border-bottom: 1px solid var(--logo-color-100); 20 | margin-bottom: .5rem; 21 | } 22 | 23 | .item { 24 | margin-bottom: .5rem; 25 | } 26 | 27 | .price { 28 | display: flex; 29 | justify-content: space-between; 30 | padding: .5rem 0; 31 | } 32 | 33 | .total { 34 | font-weight: 700; 35 | border-bottom: 1px solid var(--logo-color-100); 36 | } 37 | 38 | @media (max-width: 876px) { 39 | .container { 40 | padding: 0 22px; 41 | width: 100%; 42 | max-width: 100%; 43 | } 44 | } -------------------------------------------------------------------------------- /src/styles/product.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | min-height: 100vh; 4 | display: flex; 5 | } 6 | 7 | .image { 8 | width: 60%; 9 | height: 100vh; 10 | margin: 0; 11 | } 12 | 13 | .placeholder { 14 | height: 100%; 15 | width: 100%; 16 | background: var(--bg); 17 | } 18 | 19 | .info { 20 | display: flex; 21 | flex-direction: column; 22 | justify-content: center; 23 | padding: 1rem 3rem; 24 | } 25 | 26 | .sizebtn { 27 | padding: 0.7rem; 28 | border: none; 29 | background: lightgrey; 30 | border-radius: 2px; 31 | height: 38px; 32 | width: 38px; 33 | margin-right: 0.5rem; 34 | transition: all 0.2s ease-in; 35 | } 36 | 37 | .sizebtn:hover { 38 | filter: brightness(0.9); 39 | } 40 | 41 | .selected { 42 | background: var(--logo-color-900); 43 | color: white; 44 | } 45 | 46 | .ticker { 47 | width: 35px; 48 | height: 35px; 49 | text-align: center; 50 | display: flex; 51 | align-items: center; 52 | justify-content: center; 53 | } 54 | 55 | .qty { 56 | display: flex; 57 | align-items: center; 58 | } 59 | 60 | .selection { 61 | margin: 2rem 0; 62 | } 63 | 64 | .selection p { 65 | margin: 0; 66 | margin-bottom: 0.7rem; 67 | } 68 | 69 | .qtybtn { 70 | height: 38px; 71 | width: 38px; 72 | border: none; 73 | background: none; 74 | transition: all 0.2s ease-in; 75 | border-radius: 2px; 76 | } 77 | 78 | .qtybtn:hover, 79 | .qtybtn:focus { 80 | background: var(--logo-color-100); 81 | } 82 | 83 | .addbtn { 84 | font-size: 1.125rem; 85 | min-height: 3rem; 86 | min-width: 3rem; 87 | padding: 0.5rem 1.25rem; 88 | align-self: center; 89 | display: inline-flex; 90 | align-items: center; 91 | background: var(--logo-color-900); 92 | color: white; 93 | border-radius: 4px; 94 | transition: background 0.2s ease-in; 95 | font-weight: 500; 96 | border: none; 97 | } 98 | 99 | .addbtn svg { 100 | margin-left: 0.7rem; 101 | } 102 | 103 | .addbtn:hover { 104 | background: var(--logo-color-1000); 105 | } 106 | 107 | .tabs { 108 | margin-top: 2rem; 109 | max-width: 500px; 110 | } 111 | 112 | .tabtitle { 113 | background: transparent; 114 | border: none; 115 | padding: .5rem 0; 116 | font-size: var(--fz-m); 117 | border-bottom: 1px solid var(--logo-color-400); 118 | } 119 | 120 | @media (max-width: 876px) { 121 | .container { 122 | flex-direction: column; 123 | } 124 | 125 | .image, 126 | .info { 127 | width: 100% 128 | } 129 | 130 | .image { 131 | height: 50vh; 132 | } 133 | 134 | .info { 135 | margin-top: 1rem; 136 | padding: 0 22px; 137 | } 138 | } -------------------------------------------------------------------------------- /src/styles/shipping-method.module.css: -------------------------------------------------------------------------------- 1 | .shippingOption { 2 | display: flex; 3 | align-items: center; 4 | box-shadow: var(--shade); 5 | justify-content: space-between; 6 | background: white; 7 | border-radius: 8px; 8 | padding: 1rem 2rem; 9 | cursor: pointer; 10 | } 11 | 12 | .shippingOption div { 13 | display: flex; 14 | align-items: center; 15 | } 16 | 17 | .chosen { 18 | border: 1px solid var(--logo-color-400) 19 | } 20 | -------------------------------------------------------------------------------- /src/styles/shipping-step.module.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | width: 100%; 6 | padding: 4rem; 7 | height: 530px; 8 | } 9 | 10 | .container { 11 | margin-top: 20px; 12 | flex-grow: 1; 13 | } 14 | 15 | .shippingOption { 16 | display: flex; 17 | align-items: center; 18 | box-shadow: var(--shade); 19 | justify-content: space-between; 20 | background: white; 21 | border-radius: 8px; 22 | padding: 1rem; 23 | } 24 | 25 | .shippingOption div { 26 | display: flex; 27 | align-items: center; 28 | } 29 | 30 | .stepBack { 31 | background: transparent; 32 | border: none; 33 | display: flex; 34 | align-items: center; 35 | padding: 0; 36 | font-size: var(--fz-m); 37 | } 38 | 39 | .stepBack svg { 40 | margin-right: 0.5rem; 41 | } 42 | 43 | .controls { 44 | display: flex; 45 | align-items: center; 46 | justify-content: space-between; 47 | 48 | } 49 | 50 | .nextBtn { 51 | font-size: 1.125rem; 52 | min-height: 3rem; 53 | min-width: 3rem; 54 | padding: 0.5rem 1.25rem; 55 | align-self: center; 56 | display: inline-flex; 57 | align-items: center; 58 | background: var(--logo-color-900); 59 | color: white; 60 | border-radius: 8px; 61 | transition: background 0.2s ease-in; 62 | font-weight: 500; 63 | border: none; 64 | } 65 | 66 | .nextBtn:hover { 67 | background: var(--logo-color-1000); 68 | } 69 | 70 | .error { 71 | display: flex; 72 | opacity: 0; 73 | visibility: hidden; 74 | align-items: center; 75 | transition: all 0.1s ease-in; 76 | color: #db5461; 77 | } 78 | 79 | .error p { 80 | margin-left: 0.5rem; 81 | } 82 | 83 | .error.active { 84 | opacity: 1; 85 | visibility: visible; 86 | } 87 | -------------------------------------------------------------------------------- /src/styles/step-overview.module.css: -------------------------------------------------------------------------------- 1 | .step { 2 | display: flex; 3 | background: white; 4 | border-radius: 8px; 5 | box-shadow: var(--shade); 6 | padding: 2rem 2rem; 7 | align-items: center; 8 | font-size: var(--fz-s); 9 | margin-bottom: 0.5em; 10 | color: grey; 11 | } 12 | 13 | .stepInfo { 14 | margin-right: 0.75em; 15 | min-width: 0px; 16 | flex: 1 1 0%; 17 | white-space: nowrap; 18 | overflow: hidden; 19 | text-overflow: ellipsis; 20 | } 21 | 22 | .detail { 23 | width: 80px; 24 | font-weight: 600; 25 | } 26 | 27 | .edit { 28 | background: transparent; 29 | border: none; 30 | transition: background .2s ease-in; 31 | padding: .5rem 1rem; 32 | border-radius: 4px; 33 | } 34 | 35 | .edit:hover { 36 | background: var(--logo-color-100); 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/client.js: -------------------------------------------------------------------------------- 1 | import Medusa from "@medusajs/medusa-js"; 2 | 3 | const BACKEND_URL = process.env.GATSBY_STORE_URL || "http://localhost:9000"; 4 | 5 | export const createClient = () => new Medusa({ baseUrl: BACKEND_URL }); 6 | -------------------------------------------------------------------------------- /src/utils/format-price.js: -------------------------------------------------------------------------------- 1 | function getTaxRate(cart) { 2 | if ("tax_rate" in cart) { 3 | return cart.tax_rate / 100; 4 | } else if (cart.region) { 5 | return cart.region && cart.region.tax_rate / 100; 6 | } 7 | return 0; 8 | } 9 | 10 | export function formatMoneyAmount(moneyAmount, digits, taxRate = 0) { 11 | let locale = "en-US"; 12 | 13 | return new Intl.NumberFormat(locale, { 14 | style: "currency", 15 | currency: moneyAmount.currencyCode, 16 | minimumFractionDigits: digits, 17 | }).format(moneyAmount.amount * (1 + taxRate / 100)); 18 | } 19 | 20 | export function getVariantPrice(cart, variant) { 21 | let taxRate = getTaxRate(cart); 22 | 23 | let moneyAmount = variant.prices.find( 24 | (p) => 25 | p.currency_code.toLowerCase() === cart.region.currency_code.toLowerCase() 26 | ); 27 | 28 | if (moneyAmount && moneyAmount.amount) { 29 | return (moneyAmount.amount * (1 + taxRate)) / 100; 30 | } 31 | 32 | return undefined; 33 | } 34 | 35 | export function formatPrices(cart, variant, digits = 2) { 36 | if (!cart || !cart.region || !variant) return; 37 | if (!variant.prices) return `15.00 EUR`; 38 | return formatMoneyAmount( 39 | { 40 | currencyCode: cart.region.currency_code, 41 | amount: getVariantPrice(cart, variant), 42 | }, 43 | digits 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/helper-functions.js: -------------------------------------------------------------------------------- 1 | export const quantity = (item) => { 2 | return item.quantity; 3 | }; 4 | 5 | export const sum = (prev, next) => { 6 | return prev + next; 7 | }; 8 | 9 | export const formatPrice = (price, currency) => { 10 | return `${(price / 100).toFixed(2)} ${currency.toUpperCase()}`; 11 | }; 12 | 13 | export const getSlug = (path) => { 14 | const tmp = path.split("/"); 15 | return tmp[tmp.length - 1]; 16 | }; 17 | 18 | export const resetOptions = (product) => { 19 | const variantId = product.variants.slice(0).reverse()[0].id; 20 | const size = product.variants.slice(0).reverse()[0].title; 21 | return { 22 | variantId: variantId, 23 | quantity: 1, 24 | size: size, 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /src/utils/stripe.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a singleton to ensure we only instantiate Stripe once. 3 | */ 4 | import { loadStripe } from "@stripe/stripe-js"; 5 | const STRIPE_API_KEY = process.env.GATSBY_STRIPE_KEY || null; 6 | let stripePromise; 7 | const getStripe = () => { 8 | if (!stripePromise) { 9 | stripePromise = loadStripe(STRIPE_API_KEY); 10 | } 11 | return stripePromise; 12 | }; 13 | export default getStripe; 14 | --------------------------------------------------------------------------------