├── .env.template ├── .gitignore ├── .nvmrc ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-ssr.js ├── package.json ├── resources └── shopify+gatsby.png ├── src ├── components │ ├── Cart │ │ ├── LineItem │ │ │ ├── index.js │ │ │ └── styles.js │ │ └── index.js │ ├── Navigation │ │ ├── index.js │ │ └── styles.js │ ├── ProductForm │ │ └── index.js │ ├── ProductGrid │ │ ├── index.js │ │ └── styles.js │ └── seo.js ├── context │ └── StoreContext.js ├── images │ ├── gatsby-astronaut.png │ └── gatsby-icon.png ├── layouts │ └── index.js ├── pages │ ├── 404.js │ ├── cart.js │ ├── index.js │ └── page-2.js ├── provider │ └── ContextProvider.js ├── templates │ └── ProductPage │ │ ├── index.js │ │ └── styles.js └── utils │ └── styles.js └── yarn.lock /.env.template: -------------------------------------------------------------------------------- 1 | SHOP_NAME= 2 | SHOPIFY_ACCESS_TOKEN= 3 | SHOPIFY_SHOP_PASSWORD= 4 | GATSBY_SHOPIFY_STORE_URL= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variables file 55 | .env 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | 61 | # Mac files 62 | .DS_Store 63 | 64 | # Yarn 65 | yarn-error.log 66 | .pnp/ 67 | .pnp.js 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | .env.development 72 | .env.production -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "arrowParens": "avoid" 6 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Alexander Hörl 4 | Copyright (c) 2015 gatsbyjs 5 | 6 | Copyright for portions of gatsby-shopify-starter are held by gatsbyjs, 2015 as 7 | part of project gatsby-starter-default. 8 | All other copyright for project gatsby-shopify-starter are held by Alexander Hörl, 2019. 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | Gatsby 4 |
5 | Gatsby Shopify starter 6 |

7 | 8 | [![JamStackBox Status](https://jamstackbox.alexanderhoerl.de/badge/gatsby-shopify-starter)](https://github.com/AlexanderProd/jam-stack-box) 9 | 10 | Kick off your next eCommerce experience with this Gatsby starter. It is based on the default Gatsby starter to be easily modifiable. [Demo](https://gatsby-shopify-starter.alexanderhoerl.de) 11 | 12 | Unfortunately Shopify prohibits to share access tokens in public repositories, therefore you have to to create your own Shopify Shop and put those credentials in the `template.env` file and rename it to `.env.development` as well as `.env.production`. There are two files if you want to use a different store for you development. 13 | 14 | To obtain your own credentials you need the create a custom app in the Shopify frontend and enable the Storefront as well as the Admin API. 15 | 16 | If you have questions feel free to message me on [Twitter](https://twitter.com/alexanderhorl) 🤙🏻 17 | 18 | Checkout [nureineburg.alexanderhoerl.de](https://nureineburg.alexanderhoerl.de) for a real public shop built with this starter, the code is also [public](https://github.com/AlexanderProd/nureineburg.de/). 19 | 20 | ## 💎 Features 21 | 22 | - Cart 23 | - Product grid 24 | - Product page 25 | - Dynamic Inventory Checking 26 | - Image optimization with Gatsby Image 27 | - Styled Components with Emotion 28 | - Google Analytics 29 | - SEO 30 | 31 | ### 📦 Dynamic Inventory Checking 32 | 33 | The Shopify product inventory is being checked in realtime, therefore no rebuilding and redeploy is needed when a product goes out of stock. This avoids problems where products could still be available even though they're out of stock due to redeploy delay. 34 | 35 | ### 🖌 Styling 36 | 37 | I'm using [Emotion](https://emotion.sh/docs/introduction) as styled components library, but the starter is purposely only sparsely styled so you don't have to remove unecessary code but can instead add your own styling immediately. 38 | 39 | ## ⚠️ Common problems 40 | 41 | - You need to use the Shopify Storefront API credentials not the regular Shopify API. 42 | - You need to have at least one published product on Shopify. 43 | 44 | ## 🚀 Quick start 45 | 46 | 1. **Create a Gatsby site.** 47 | 48 | Use the Gatsby CLI to create a new site, specifying this starter. 49 | 50 | ```sh 51 | # create a new Gatsby site using this starter 52 | gatsby new my-shopify-store https://github.com/AlexanderProd/gatsby-shopify-starter 53 | ``` 54 | 55 | 1. **Start developing.** 56 | 57 | Navigate into your new site’s directory and start it up. 58 | 59 | ```sh 60 | cd my-shopify-store/ 61 | gatsby develop 62 | ``` 63 | 64 | 1. **Open the source code and start editing!** 65 | 66 | Your site is now running at `http://localhost:8000`! 67 | 68 | _Note: You'll also see a second link: _`http://localhost:8000/___graphql`_. This is a tool you can use to experiment with querying your data. Learn more about using this tool in the [Gatsby tutorial](https://www.gatsbyjs.org/tutorial/part-five/#introducing-graphiql)._ 69 | 70 | Open the `my-shopify-store` directory in your code editor of choice and edit `src/pages/index.js`. Save your changes and the browser will update in real time! 71 | 72 | 1. **Connect your own Shopify store.** 73 | 74 | Open both `.env` files located in the root directory of your page end replace the credentials with your own. Don't forget to restart Gatsby for your store to be loaded! 75 | 76 | ⚠️ Make sure to use the Shopify storefront API credentials, not the regular Shopify API! 77 | 78 | ## Deploy 79 | 80 | Checkout my other open-source project [JAMStackBox](https://github.com/AlexanderProd/jam-stack-box) to continuously deploy your Gatsby site on your own server. 81 | 82 | ## 🎓 Learning Gatsby 83 | 84 | Looking for more guidance? Full documentation for Gatsby lives [on the website](https://www.gatsbyjs.org/). Here are some places to start: 85 | 86 | - **For most developers, we recommend starting with our [in-depth tutorial for creating a site with Gatsby](https://www.gatsbyjs.org/tutorial/).** It starts with zero assumptions about your level of ability and walks through every step of the process. 87 | 88 | - **To dive straight into code samples, head [to our documentation](https://www.gatsbyjs.org/docs/).** In particular, check out the _Guides_, _API Reference_, and _Advanced Tutorials_ sections in the sidebar. 89 | 90 | ## 📌 ToDo 91 | 92 | I'll happily merge any pull request to improve the starter. 🙂 93 | 94 | - [x] Convert Layout to function component. 95 | - [x] Add dynamic inventory checking to avoid re-building after every purchase. 96 | - [x] Add better styling. 97 | - [x] Add image optimization using Gatsby sharp plugin. 98 | - [x] Convert ProductForm to function component. 99 | 100 | -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's Browser APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/browser-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | require('dotenv').config({ 4 | path: `.env.${process.env.NODE_ENV}`, 5 | }) 6 | 7 | module.exports = { 8 | siteMetadata: { 9 | title: `Gatsby Shopify Starter`, 10 | description: `Kick off your next, ecommerce experience with this Gatsby starter. This starter ships with credentials to a shopify demo store so you can try it out immediately.`, 11 | author: `@alexanderhorl`, 12 | }, 13 | plugins: [ 14 | `gatsby-plugin-react-helmet`, 15 | { 16 | resolve: `gatsby-source-filesystem`, 17 | options: { 18 | name: `images`, 19 | path: `${__dirname}/src/images`, 20 | }, 21 | }, 22 | `gatsby-transformer-sharp`, 23 | `gatsby-plugin-sharp`, 24 | `gatsby-plugin-image`, 25 | `gatsby-plugin-layout`, 26 | { 27 | resolve: `gatsby-plugin-manifest`, 28 | options: { 29 | name: `gatsby-starter-default`, 30 | short_name: `starter`, 31 | start_url: `/`, 32 | background_color: `#663399`, 33 | theme_color: `#663399`, 34 | display: `minimal-ui`, 35 | icon: `src/images/gatsby-icon.png`, // This path is relative to the root of the site. 36 | }, 37 | }, 38 | { 39 | resolve: 'gatsby-source-shopify', 40 | options: { 41 | password: process.env.SHOPIFY_SHOP_PASSWORD, 42 | storeUrl: process.env.GATSBY_SHOPIFY_STORE_URL, 43 | downloadImages: true, 44 | shopifyConnections: ['collections'], 45 | }, 46 | }, 47 | { 48 | resolve: 'gatsby-plugin-root-import', 49 | options: { 50 | '~': path.join(__dirname, 'src/'), 51 | }, 52 | }, 53 | { 54 | resolve: `gatsby-plugin-google-analytics`, 55 | options: { 56 | trackingId: 'UA-134421805-1', 57 | anonymize: true, 58 | respectDNT: true, 59 | }, 60 | }, 61 | // this (optional) plugin enables Progressive Web App + Offline functionality 62 | // To learn more, visit: https://gatsby.app/offline 63 | // 'gatsby-plugin-offline', 64 | ], 65 | } 66 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require(`path`) 2 | 3 | exports.createPages = ({ graphql, actions }) => { 4 | const { createPage } = actions 5 | return graphql(` 6 | { 7 | allShopifyProduct { 8 | edges { 9 | node { 10 | handle 11 | } 12 | } 13 | } 14 | } 15 | `).then(result => { 16 | result.data.allShopifyProduct.edges.forEach(({ node }) => { 17 | createPage({ 18 | path: `/product/${node.handle}/`, 19 | component: path.resolve(`./src/templates/ProductPage/index.js`), 20 | context: { 21 | // Data passed to context is available 22 | // in page queries as GraphQL variables. 23 | handle: node.handle, 24 | }, 25 | }) 26 | }) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's SSR (Server Side Rendering) APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/ssr-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-shopify-starter", 3 | "private": false, 4 | "description": "A simple starter to build a blazing fast Shopify store quickly with Gatsby", 5 | "version": "0.1.0", 6 | "author": "Alexander Hörl ", 7 | "engines": { 8 | "node": "=> 10.13 < 14.9" 9 | }, 10 | "dependencies": { 11 | "@emotion/core": "^11.0.0", 12 | "@emotion/react": "^11.9.3", 13 | "@emotion/styled": "^11.9.3", 14 | "gatsby": "^4.19.2", 15 | "gatsby-plugin-google-analytics": "^4.19.0", 16 | "gatsby-plugin-image": "^2.19.0", 17 | "gatsby-plugin-layout": "^3.19.0", 18 | "gatsby-plugin-manifest": "^4.19.0", 19 | "gatsby-plugin-offline": "^5.19.0", 20 | "gatsby-plugin-react-helmet": "^5.19.0", 21 | "gatsby-plugin-root-import": "^2.0.8", 22 | "gatsby-plugin-sharp": "^4.19.0", 23 | "gatsby-source-filesystem": "^4.19.0", 24 | "gatsby-source-shopify": "6.10.2", 25 | "gatsby-transformer-sharp": "^4.19.0", 26 | "isomorphic-fetch": "^3.0.0", 27 | "js-yaml": "^4.1.0", 28 | "lodash": "^4.17.21", 29 | "prop-types": "^15.7.2", 30 | "querystringify": "^2.0.0", 31 | "react": "^18.2.0", 32 | "react-dom": "^18.2.0", 33 | "react-helmet": "^6.1.0", 34 | "set-value": "^4.1.0", 35 | "shopify-buy": "2.16.1" 36 | }, 37 | "keywords": [ 38 | "gatsby", 39 | "shopify", 40 | "ecommerce", 41 | "store", 42 | "shop" 43 | ], 44 | "license": "MIT", 45 | "scripts": { 46 | "build": "gatsby build", 47 | "dev": "gatsby develop", 48 | "start": "npm run dev", 49 | "clean": "gatsby clean", 50 | "format": "prettier --write \"src/**/*.js\"", 51 | "test": "echo \"Write tests! -> https://gatsby.app/unit-testing\"" 52 | }, 53 | "devDependencies": { 54 | "prettier": "^2.2.1" 55 | }, 56 | "repository": { 57 | "type": "git", 58 | "url": "https://github.com/gatsbyjs/gatsby-starter-default" 59 | }, 60 | "bugs": { 61 | "url": "https://github.com/gatsbyjs/gatsby/issues" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /resources/shopify+gatsby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexanderProd/gatsby-shopify-starter/b26ba5c76c357eed55a7e05a7c19f53dd33da10c/resources/shopify+gatsby.png -------------------------------------------------------------------------------- /src/components/Cart/LineItem/index.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import { Link } from 'gatsby' 3 | 4 | import StoreContext from '~/context/StoreContext' 5 | import { Wrapper } from './styles' 6 | 7 | const LineItem = props => { 8 | const { item } = props 9 | const { 10 | removeLineItem, 11 | store: { client, checkout }, 12 | } = useContext(StoreContext) 13 | 14 | const variantImage = item.variant.image ? ( 15 | {`${item.title} 20 | ) : null 21 | 22 | const selectedOptions = item.variant.selectedOptions 23 | ? item.variant.selectedOptions.map( 24 | option => `${option.name}: ${option.value} ` 25 | ) 26 | : null 27 | 28 | const handleRemove = () => { 29 | removeLineItem(client, checkout.id, item.id) 30 | } 31 | 32 | return ( 33 | 34 | 35 | {variantImage} 36 | 37 |

38 | {item.title} 39 | {` `} 40 | {item.variant.title === !'Default Title' ? item.variant.title : ''} 41 |

42 | {selectedOptions} 43 | {item.quantity} 44 | 45 |
46 | ) 47 | } 48 | 49 | export default LineItem 50 | -------------------------------------------------------------------------------- /src/components/Cart/LineItem/styles.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled' 2 | 3 | export const Wrapper = styled.div` 4 | display: flex; 5 | justify-content: space-between; 6 | align-items: center; 7 | flex-wrap: wrap; 8 | padding: 2rem 0 2rem 0; 9 | ` 10 | -------------------------------------------------------------------------------- /src/components/Cart/index.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | 3 | import StoreContext from '~/context/StoreContext' 4 | import LineItem from './LineItem' 5 | 6 | const Cart = () => { 7 | const { 8 | store: { checkout }, 9 | } = useContext(StoreContext) 10 | 11 | const handleCheckout = () => { 12 | window.open(checkout.webUrl) 13 | } 14 | 15 | const lineItems = checkout.lineItems.map(item => ( 16 | 17 | )) 18 | 19 | return ( 20 |
21 | {lineItems} 22 |

Subtotal

23 |

$ {checkout.subtotalPrice}

24 |
25 |

Taxes

26 |

$ {checkout.totalTax}

27 |
28 |

Total

29 |

$ {checkout.totalPrice}

30 |
31 | 37 |
38 | ) 39 | } 40 | 41 | export default Cart 42 | -------------------------------------------------------------------------------- /src/components/Navigation/index.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import reduce from 'lodash/reduce' 3 | import PropTypes from 'prop-types' 4 | 5 | import StoreContext from '~/context/StoreContext' 6 | import { CartCounter, Container, InfoBanner, MenuLink, Wrapper } from './styles' 7 | 8 | const useQuantity = () => { 9 | const { 10 | store: { checkout }, 11 | } = useContext(StoreContext) 12 | const items = checkout ? checkout.lineItems : [] 13 | const total = reduce(items, (acc, item) => acc + item.quantity, 0) 14 | return [total !== 0, total] 15 | } 16 | 17 | const Navigation = ({ siteTitle }) => { 18 | const [hasItems, quantity] = useQuantity() 19 | 20 | return ( 21 | <> 22 | 23 | Check out my open source Project{' '} 24 | 29 | JAMStackBox 30 | {' '} 31 | to continuosly deploy Gatsby sites on your own. 32 | 33 | 34 | 35 | {siteTitle} 36 | 37 | {hasItems && {quantity}} 38 | Cart 🛍 39 | 40 | 41 | 42 | 43 | ) 44 | } 45 | 46 | Navigation.propTypes = { 47 | siteTitle: PropTypes.string, 48 | } 49 | 50 | Navigation.defaultProps = { 51 | siteTitle: ``, 52 | } 53 | 54 | export default Navigation 55 | -------------------------------------------------------------------------------- /src/components/Navigation/styles.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled' 2 | import { Link } from 'gatsby' 3 | 4 | import { breakpoints } from '../../utils/styles' 5 | 6 | export const Wrapper = styled.div` 7 | background: rebeccapurple; 8 | margin-bottom: 1.45rem; 9 | ` 10 | 11 | export const Container = styled.div` 12 | display: flex; 13 | justify-content: space-between; 14 | align-items: baseline; 15 | padding: 1.45rem; 16 | margin: 0 auto; 17 | max-width: 960px; 18 | ` 19 | 20 | export const MenuLink = styled(Link)` 21 | color: white; 22 | text-decoration: none; 23 | font-size: 2rem; 24 | font-weight: bold; 25 | 26 | @media (max-width: ${breakpoints.s}px) { 27 | font-size: 1.4rem; 28 | } 29 | ` 30 | 31 | export const CartCounter = styled.span` 32 | background-color: white; 33 | color: #663399; 34 | border-radius: 20px; 35 | padding: 0 10px; 36 | font-size: 1.2rem; 37 | float: right; 38 | margin: -10px; 39 | z-index: 20; 40 | ` 41 | 42 | export const InfoBanner = styled.div` 43 | align-items: center; 44 | justify-content: space-between; 45 | padding-top: 0.5rem; 46 | padding-bottom: 0.5rem; 47 | padding-left: 2.5rem; 48 | padding-right: 2.5rem; 49 | background-color: #eb5ebf; 50 | text-align: center; 51 | color: white; 52 | 53 | a { 54 | color: white; 55 | } 56 | 57 | svg { 58 | cursor: pointer; 59 | } 60 | 61 | @media (max-width: ${breakpoints.s}px) { 62 | & > :first-of-type { 63 | margin-right: 0.5rem; 64 | } 65 | } 66 | ` 67 | -------------------------------------------------------------------------------- /src/components/ProductForm/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useEffect, useCallback } from 'react' 2 | import find from 'lodash/find' 3 | import isEqual from 'lodash/isEqual' 4 | import PropTypes from 'prop-types' 5 | 6 | import StoreContext from '~/context/StoreContext' 7 | 8 | const ProductForm = ({ product }) => { 9 | const { 10 | options, 11 | variants, 12 | variants: [initialVariant], 13 | priceRangeV2: { minVariantPrice }, 14 | } = product 15 | const [variant, setVariant] = useState({ ...initialVariant }) 16 | const [quantity, setQuantity] = useState(1) 17 | const [loading, setLoading] = useState(false) 18 | const { 19 | addVariantToCart, 20 | store: { client, adding }, 21 | } = useContext(StoreContext) 22 | 23 | const productVariant = 24 | client.product.helpers.variantForOptions(product, variant) || variant 25 | const [available, setAvailable] = useState(productVariant.availableForSale) 26 | 27 | const checkAvailability = useCallback( 28 | async productId => { 29 | setLoading(true) 30 | const fetchedProduct = await client.product.fetch(productId) 31 | const result = fetchedProduct.variants.filter( 32 | variant => variant.id === productVariant.shopifyId 33 | ) 34 | setAvailable(result[0]?.available ?? fetchedProduct.variants[0].available) 35 | setLoading(false) 36 | }, 37 | [client.product, productVariant.shopifyId] 38 | ) 39 | 40 | useEffect(() => { 41 | checkAvailability(product.shopifyId) 42 | }, [checkAvailability, product.shopifyId]) 43 | 44 | const handleQuantityChange = ({ target }) => { 45 | setQuantity(target.value) 46 | } 47 | 48 | const handleOptionChange = (optionIndex, { target }) => { 49 | const { value } = target 50 | const currentOptions = [...variant.selectedOptions] 51 | 52 | currentOptions[optionIndex] = { 53 | ...currentOptions[optionIndex], 54 | value, 55 | } 56 | 57 | const selectedVariant = find(variants, ({ selectedOptions }) => 58 | isEqual(currentOptions, selectedOptions) 59 | ) 60 | 61 | setVariant({ ...selectedVariant }) 62 | } 63 | 64 | const handleAddToCart = async () => { 65 | await addVariantToCart(productVariant.shopifyId, quantity) 66 | } 67 | 68 | /* 69 | Using this in conjunction with a select input for variants 70 | can cause a bug where the buy button is disabled, this 71 | happens when only one variant is available and it's not the 72 | first one in the dropdown list. I didn't feel like putting 73 | in time to fix this since its an edge case and most people 74 | wouldn't want to use dropdown styled selector anyways - 75 | at least if the have a sense for good design lol. 76 | */ 77 | const checkDisabled = (name, value) => { 78 | const match = find(variants, { 79 | selectedOptions: [ 80 | { 81 | name: name, 82 | value: value, 83 | }, 84 | ], 85 | }) 86 | if (match === undefined) return true 87 | if (match.availableForSale === true) return false 88 | return true 89 | } 90 | 91 | const price = Intl.NumberFormat(undefined, { 92 | currency: minVariantPrice.currencyCode, 93 | minimumFractionDigits: 2, 94 | style: 'currency', 95 | }).format(variant.price) 96 | 97 | return ( 98 | <> 99 |

{price}

100 | {options.map(({ id, name, values }, index) => ( 101 | 102 | 103 | 118 |
119 |
120 | ))} 121 | 122 | 131 |
132 | 139 | {!available &&

This Product is out of Stock!

} 140 | 141 | ) 142 | } 143 | 144 | ProductForm.propTypes = { 145 | product: PropTypes.shape({ 146 | descriptionHtml: PropTypes.string, 147 | handle: PropTypes.string, 148 | id: PropTypes.string, 149 | shopifyId: PropTypes.string, 150 | images: PropTypes.arrayOf( 151 | PropTypes.shape({ 152 | id: PropTypes.string, 153 | originalSrc: PropTypes.string, 154 | }) 155 | ), 156 | options: PropTypes.arrayOf( 157 | PropTypes.shape({ 158 | id: PropTypes.string, 159 | name: PropTypes.string, 160 | values: PropTypes.arrayOf(PropTypes.string), 161 | }) 162 | ), 163 | productType: PropTypes.string, 164 | title: PropTypes.string, 165 | variants: PropTypes.arrayOf( 166 | PropTypes.shape({ 167 | availableForSale: PropTypes.bool, 168 | id: PropTypes.string, 169 | price: PropTypes.string, 170 | title: PropTypes.string, 171 | shopifyId: PropTypes.string, 172 | selectedOptions: PropTypes.arrayOf( 173 | PropTypes.shape({ 174 | name: PropTypes.string, 175 | value: PropTypes.string, 176 | }) 177 | ), 178 | }) 179 | ), 180 | }), 181 | addVariantToCart: PropTypes.func, 182 | } 183 | 184 | export default ProductForm 185 | -------------------------------------------------------------------------------- /src/components/ProductGrid/index.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import { GatsbyImage } from 'gatsby-plugin-image' 3 | import { useStaticQuery, graphql, Link } from 'gatsby' 4 | 5 | import StoreContext from '~/context/StoreContext' 6 | import { Grid, Product, Title, PriceTag } from './styles' 7 | 8 | const ProductGrid = () => { 9 | const { 10 | store: { checkout }, 11 | } = useContext(StoreContext) 12 | const { allShopifyProduct } = useStaticQuery( 13 | graphql` 14 | query { 15 | allShopifyProduct(sort: { fields: [createdAt], order: DESC }) { 16 | edges { 17 | node { 18 | id 19 | title 20 | handle 21 | createdAt 22 | images { 23 | id 24 | originalSrc 25 | altText 26 | localFile { 27 | childImageSharp { 28 | gatsbyImageData(layout: FULL_WIDTH) 29 | } 30 | } 31 | } 32 | featuredImage { 33 | altText 34 | id 35 | localFile { 36 | childImageSharp { 37 | gatsbyImageData(layout: FULL_WIDTH) 38 | } 39 | } 40 | } 41 | variants { 42 | price 43 | } 44 | } 45 | } 46 | } 47 | } 48 | ` 49 | ) 50 | 51 | const getPrice = price => 52 | Intl.NumberFormat(undefined, { 53 | currency: checkout.currencyCode ? checkout.currencyCode : 'EUR', 54 | minimumFractionDigits: 2, 55 | style: 'currency', 56 | }).format(parseFloat(price ? price : 0)) 57 | 58 | return ( 59 | 60 | {allShopifyProduct.edges ? ( 61 | allShopifyProduct.edges.map( 62 | ({ 63 | node: { 64 | id, 65 | handle, 66 | title, 67 | featuredImage, 68 | variants: [firstVariant], 69 | }, 70 | }) => ( 71 | 72 | 73 | {featuredImage && ( 74 | 82 | )} 83 | 84 | {title} 85 | {getPrice(firstVariant.price)} 86 | 87 | ) 88 | ) 89 | ) : ( 90 |

No Products found!

91 | )} 92 |
93 | ) 94 | } 95 | 96 | export default ProductGrid 97 | -------------------------------------------------------------------------------- /src/components/ProductGrid/styles.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled' 2 | 3 | import { breakpoints } from '../../utils/styles' 4 | 5 | export const Grid = styled.div` 6 | display: grid; 7 | grid-template-columns: repeat(3, 1fr); 8 | gap: 2.5rem; 9 | 10 | @media (max-width: ${breakpoints.s}px) { 11 | grid-template-columns: repeat(1, 1fr); 12 | } 13 | ` 14 | 15 | export const Product = styled.div` 16 | display: flex; 17 | min-height: 100%; 18 | flex-direction: column; 19 | ` 20 | 21 | export const Title = styled.span` 22 | font-weight: 300; 23 | font-size: 1.2rem; 24 | text-align: center; 25 | ` 26 | 27 | export const PriceTag = styled.span` 28 | font-weight: 300; 29 | font-size: 1rem; 30 | text-align: center; 31 | margin-top: 15px; 32 | 33 | :before { 34 | content: '- '; 35 | } 36 | ` 37 | -------------------------------------------------------------------------------- /src/components/seo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Helmet from 'react-helmet' 4 | import { StaticQuery, graphql } from 'gatsby' 5 | 6 | function SEO({ description, lang, meta, keywords, title }) { 7 | return ( 8 | { 11 | const metaDescription = 12 | description || data.site.siteMetadata.description 13 | return ( 14 | 0 56 | ? { 57 | name: `keywords`, 58 | content: keywords.join(`, `), 59 | } 60 | : [] 61 | ) 62 | .concat(meta)} 63 | /> 64 | ) 65 | }} 66 | /> 67 | ) 68 | } 69 | 70 | SEO.defaultProps = { 71 | lang: `en`, 72 | meta: [], 73 | keywords: [], 74 | } 75 | 76 | SEO.propTypes = { 77 | description: PropTypes.string, 78 | lang: PropTypes.string, 79 | meta: PropTypes.array, 80 | keywords: PropTypes.arrayOf(PropTypes.string), 81 | title: PropTypes.string.isRequired, 82 | } 83 | 84 | export default SEO 85 | 86 | const detailsQuery = graphql` 87 | query DefaultSEOQuery { 88 | site { 89 | siteMetadata { 90 | title 91 | description 92 | author 93 | } 94 | } 95 | } 96 | ` 97 | -------------------------------------------------------------------------------- /src/context/StoreContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const StoreContext = React.createContext() 4 | 5 | export default StoreContext 6 | -------------------------------------------------------------------------------- /src/images/gatsby-astronaut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexanderProd/gatsby-shopify-starter/b26ba5c76c357eed55a7e05a7c19f53dd33da10c/src/images/gatsby-astronaut.png -------------------------------------------------------------------------------- /src/images/gatsby-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexanderProd/gatsby-shopify-starter/b26ba5c76c357eed55a7e05a7c19f53dd33da10c/src/images/gatsby-icon.png -------------------------------------------------------------------------------- /src/layouts/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { StaticQuery, graphql } from 'gatsby' 4 | import styled from '@emotion/styled' 5 | 6 | import ContextProvider from '~/provider/ContextProvider' 7 | 8 | import { GlobalStyle } from '~/utils/styles' 9 | import Navigation from '~/components/Navigation' 10 | 11 | const Wrapper = styled.div` 12 | margin: 0 auto; 13 | max-width: 960px; 14 | padding: 0px 1.0875rem 1.45rem; 15 | ` 16 | 17 | const Layout = ({ children }) => { 18 | return ( 19 | 20 | 21 | ( 32 | <> 33 | 34 | 35 | {children} 36 |
37 | © {new Date().getFullYear()}, Built with 38 | {` `} 39 | Gatsby 40 |
41 |
42 | 43 | )} 44 | /> 45 |
46 | ) 47 | } 48 | 49 | Layout.propTypes = { 50 | children: PropTypes.node.isRequired, 51 | } 52 | 53 | export default Layout 54 | -------------------------------------------------------------------------------- /src/pages/404.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Seo from '~/components/seo' 4 | 5 | const NotFoundPage = () => ( 6 | <> 7 | 8 |

NOT FOUND

9 |

You just hit a route that doesn't exist... the sadness.

10 | 11 | ) 12 | 13 | export default NotFoundPage 14 | -------------------------------------------------------------------------------- /src/pages/cart.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Cart from '~/components/Cart' 4 | import { Container } from '~/utils/styles' 5 | 6 | const CartPage = () => ( 7 | 8 |

Cart

9 | 10 |
11 | ) 12 | 13 | export default CartPage 14 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'gatsby' 3 | 4 | import Seo from '~/components/seo' 5 | import ProductGrid from '~/components/ProductGrid' 6 | 7 | const IndexPage = () => ( 8 | <> 9 | 10 |

Hi people

11 |

Welcome to your new Shop powered by Gatsby and Shopify.

12 | 13 | Go to page 2 14 | 15 | ) 16 | 17 | export default IndexPage 18 | -------------------------------------------------------------------------------- /src/pages/page-2.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'gatsby' 3 | 4 | import Seo from '~/components/seo' 5 | 6 | const SecondPage = () => ( 7 | <> 8 | 9 |

Hi from the second page

10 |

Welcome to page 2

11 | Go back to the homepage 12 | 13 | ) 14 | 15 | export default SecondPage 16 | -------------------------------------------------------------------------------- /src/provider/ContextProvider.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch' 2 | import React, { useState, useEffect, useRef } from 'react' 3 | import Client from 'shopify-buy' 4 | 5 | import Context from '~/context/StoreContext' 6 | 7 | const client = Client.buildClient( 8 | { 9 | storefrontAccessToken: process.env.SHOPIFY_ACCESS_TOKEN, 10 | domain: `${process.env.SHOP_NAME}.myshopify.com`, 11 | }, 12 | fetch 13 | ) 14 | 15 | const ContextProvider = ({ children }) => { 16 | let initialStoreState = { 17 | client, 18 | adding: false, 19 | checkout: { lineItems: [] }, 20 | products: [], 21 | shop: {}, 22 | } 23 | 24 | const [store, updateStore] = useState(initialStoreState) 25 | const isRemoved = useRef(false) 26 | 27 | useEffect(() => { 28 | const initializeCheckout = async () => { 29 | // Check for an existing cart. 30 | const isBrowser = typeof window !== 'undefined' 31 | const existingCheckoutID = isBrowser 32 | ? localStorage.getItem('shopify_checkout_id') 33 | : null 34 | 35 | const setCheckoutInState = checkout => { 36 | if (isBrowser) { 37 | localStorage.setItem('shopify_checkout_id', checkout.id) 38 | } 39 | 40 | updateStore(prevState => { 41 | return { ...prevState, checkout } 42 | }) 43 | } 44 | 45 | const createNewCheckout = () => store.client.checkout.create() 46 | const fetchCheckout = id => store.client.checkout.fetch(id) 47 | 48 | if (existingCheckoutID) { 49 | try { 50 | const checkout = await fetchCheckout(existingCheckoutID) 51 | // Make sure this cart hasn’t already been purchased. 52 | if (!isRemoved.current && !checkout.completedAt) { 53 | setCheckoutInState(checkout) 54 | return 55 | } 56 | } catch (e) { 57 | localStorage.setItem('shopify_checkout_id', null) 58 | } 59 | } 60 | 61 | const newCheckout = await createNewCheckout() 62 | if (!isRemoved.current) { 63 | setCheckoutInState(newCheckout) 64 | } 65 | } 66 | 67 | initializeCheckout() 68 | }, [store.client.checkout]) 69 | 70 | useEffect(() => () => { 71 | isRemoved.current = true 72 | }) 73 | 74 | return ( 75 | { 79 | if (variantId === '' || !quantity) { 80 | console.error('Both a size and quantity are required.') 81 | return 82 | } 83 | 84 | updateStore(prevState => { 85 | return { ...prevState, adding: true } 86 | }) 87 | 88 | const { checkout, client } = store 89 | 90 | const checkoutId = checkout.id 91 | const lineItemsToUpdate = [ 92 | { variantId, quantity: parseInt(quantity, 10) }, 93 | ] 94 | 95 | return client.checkout 96 | .addLineItems(checkoutId, lineItemsToUpdate) 97 | .then(checkout => { 98 | updateStore(prevState => { 99 | return { ...prevState, checkout, adding: false } 100 | }) 101 | }) 102 | }, 103 | removeLineItem: (client, checkoutID, lineItemID) => { 104 | return client.checkout 105 | .removeLineItems(checkoutID, [lineItemID]) 106 | .then(res => { 107 | updateStore(prevState => { 108 | return { ...prevState, checkout: res } 109 | }) 110 | }) 111 | }, 112 | updateLineItem: (client, checkoutID, lineItemID, quantity) => { 113 | const lineItemsToUpdate = [ 114 | { id: lineItemID, quantity: parseInt(quantity, 10) }, 115 | ] 116 | 117 | return client.checkout 118 | .updateLineItems(checkoutID, lineItemsToUpdate) 119 | .then(res => { 120 | updateStore(prevState => { 121 | return { ...prevState, checkout: res } 122 | }) 123 | }) 124 | }, 125 | }} 126 | > 127 | {children} 128 | 129 | ) 130 | } 131 | export default ContextProvider 132 | -------------------------------------------------------------------------------- /src/templates/ProductPage/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'gatsby' 3 | import { GatsbyImage } from 'gatsby-plugin-image' 4 | 5 | import Seo from '~/components/seo' 6 | import ProductForm from '~/components/ProductForm' 7 | import { Container, TwoColumnGrid, GridLeft, GridRight } from '~/utils/styles' 8 | import { ProductTitle, ProductDescription } from './styles' 9 | 10 | const ProductPage = ({ data }) => { 11 | const product = data.shopifyProduct 12 | 13 | return ( 14 | <> 15 | 16 | 17 | 18 | 19 | {product.images.map(image => ( 20 | 25 | ))} 26 | 27 | 28 | {product.title} 29 | 32 | 33 | 34 | 35 | 36 | 37 | ) 38 | } 39 | 40 | export const query = graphql` 41 | query($handle: String!) { 42 | shopifyProduct(handle: { eq: $handle }) { 43 | id 44 | title 45 | handle 46 | productType 47 | description 48 | descriptionHtml 49 | shopifyId 50 | options { 51 | id 52 | name 53 | values 54 | } 55 | variants { 56 | id 57 | title 58 | price 59 | availableForSale 60 | shopifyId: storefrontId 61 | selectedOptions { 62 | name 63 | value 64 | } 65 | } 66 | priceRangeV2 { 67 | minVariantPrice { 68 | amount 69 | currencyCode 70 | } 71 | maxVariantPrice { 72 | amount 73 | currencyCode 74 | } 75 | } 76 | images { 77 | originalSrc 78 | id 79 | localFile { 80 | childImageSharp { 81 | gatsbyImageData( 82 | width: 910 83 | placeholder: TRACED_SVG 84 | layout: CONSTRAINED 85 | ) 86 | } 87 | } 88 | } 89 | } 90 | } 91 | ` 92 | 93 | export default ProductPage 94 | -------------------------------------------------------------------------------- /src/templates/ProductPage/styles.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled' 2 | 3 | export const ProductTitle = styled.h1` 4 | font-size: 2.25rem; 5 | margin-bottom: 15px; 6 | word-wrap: break-word; 7 | font-family: 'Helvetica', 'Helvetica', sans-serif; 8 | font-weight: 400; 9 | margin: 0 0 0.5rem; 10 | line-height: 1.4; 11 | ` 12 | 13 | export const ProductDescription = styled.div` 14 | margin-top: 40px; 15 | font-family: 'Helvetica', 'Helvetica', sans-serif; 16 | font-weight: 300; 17 | ` 18 | -------------------------------------------------------------------------------- /src/utils/styles.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from '@emotion/styled' 3 | import { Global, css } from '@emotion/react' 4 | import { GatsbyImage } from 'gatsby-plugin-image' 5 | 6 | export const breakpoints = { 7 | s: 576, 8 | m: 768, 9 | l: 992, 10 | xl: 1200, 11 | } 12 | 13 | export const GlobalStyle = props => ( 14 | 27 | ) 28 | 29 | export const Img = styled(GatsbyImage)` 30 | max-width: 100 %; 31 | margin-left: 0; 32 | margin-right: 0; 33 | margin-top: 0; 34 | padding-bottom: 0; 35 | padding-left: 0; 36 | padding-right: 0; 37 | padding-top: 0; 38 | margin-bottom: 1.45rem; 39 | ` 40 | 41 | export const Container = styled.div` 42 | margin: 0 auto; 43 | max-width: 960px; 44 | ` 45 | 46 | export const TwoColumnGrid = styled.div` 47 | display: grid; 48 | grid-template-columns: 1fr 2rem 1fr; 49 | grid-template-rows: 1auto; 50 | grid-template-areas: 'left . right'; 51 | 52 | @media (max-width: ${breakpoints.l}px) { 53 | display: block; 54 | } 55 | ` 56 | 57 | export const GridLeft = styled.div` 58 | grid-area: left; 59 | ` 60 | 61 | export const GridRight = styled.div` 62 | grid-area: right; 63 | ` 64 | 65 | export const MainContent = styled.main` 66 | margin-top: 80px; 67 | margin-bottom: 40px; 68 | 69 | @media (max-width: ${breakpoints.l}px) { 70 | margin-top: 40px; 71 | margin-bottom: 20px; 72 | } 73 | ` 74 | --------------------------------------------------------------------------------