├── .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 |
4 |
5 | Gatsby Shopify starter
6 |
7 |
8 | [](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 |
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 |
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 |
--------------------------------------------------------------------------------