├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-ssr.js ├── netlify.toml ├── package.json ├── src ├── components │ ├── ArticleView.js │ ├── CartItems.js │ ├── CartSteps.js │ ├── CheckoutForm.js │ ├── CheckoutProgress.js │ ├── Content.js │ ├── CouponForm.js │ ├── CouponItem.js │ ├── Footer.js │ ├── Header.js │ ├── Heading.js │ ├── HomeAbout.js │ ├── HomeBanner.js │ ├── Layout.js │ ├── NewsItem.js │ ├── PageView.js │ ├── PaymentConfirmed.js │ ├── PaymentForm.js │ ├── ProductGallery.js │ ├── ProductInfo.js │ ├── ProductItem.js │ ├── ProductView.js │ ├── ProductsList.js │ ├── ScrollButton.js │ ├── Seo.js │ ├── SocialIcons.js │ └── SubscribeForm.js ├── html.js ├── pages │ ├── 404.js │ ├── blog.js │ ├── cart.js │ ├── contact.js │ ├── coupons.js │ └── index.js └── utils │ ├── apolloClient.js │ ├── config.js │ ├── helpers.js │ ├── localState.js │ ├── theme.js │ └── wrapRootElement.js ├── static ├── images │ ├── contact.svg │ ├── home-bg-3.jpg │ ├── logo-1024.png │ └── payment-strip.png ├── js │ └── scripts.js └── robots.txt └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | /.cache 2 | /node_modules 3 | /.vscode 4 | /public 5 | /static 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "extends": ["airbnb", "prettier"], 5 | "plugins": ["prettier"], 6 | "env": { 7 | "browser": true 8 | }, 9 | "globals": {}, 10 | "settings": { 11 | "import/core-modules": ["prop-types", "react", "graphql"] 12 | }, 13 | "rules": { 14 | "prettier/prettier": [ 15 | "error", 16 | { 17 | "singleQuote": true, 18 | "trailingComma": "all", 19 | "bracketSpacing": true, 20 | "jsxBracketSameLine": true 21 | } 22 | ], 23 | "jsx-a11y/anchor-is-valid": 0, 24 | "no-underscore-dangle": 0, 25 | "class-methods-use-this": 0, 26 | "react/jsx-filename-extension": 0, 27 | "react/prop-types": 0, 28 | "react/no-danger": 0, 29 | "react/prefer-stateless-function": 0, 30 | "react/forbid-prop-types": 0, 31 | "react/jsx-one-expression-per-line": 0, 32 | "react/jsx-props-no-spreading": 0, 33 | "react/jsx-closing-bracket-location": 0 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build output 2 | public/ 3 | .cache/ 4 | 5 | # Dependencies 6 | node_modules/ 7 | 8 | # misc 9 | .DS_Store 10 | *.log* 11 | /.env 12 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "EditorConfig.EditorConfig", 5 | "esbenp.prettier-vscode" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "prettier.eslintIntegration": true, 4 | "javascript.format.enable": false, 5 | "json.format.enable": false 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Parminder Klair 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GatsbyJs Ecommerce 2 | 3 | ## THIS REPOSITORY HAS BEEN MOVED TO [https://github.com/gatsbyjs-ecommerce/web](https://github.com/gatsbyjs-ecommerce/web) 4 | 5 | A minimalist static E-commerce site built using GatsbyJs. 6 | 7 | It use headless CMS called Contentful, so no need to manage database for APIs hosting. 8 | 9 | [![Netlify Status](https://api.netlify.com/api/v1/badges/73b567fe-9c0f-4ba1-b2e9-6d612b4c15b2/deploy-status)](https://app.netlify.com/sites/gatsbyjs-ecommerce/deploys) 10 | 11 | [Live Demo](https://gatsbyjs-ecommerce.netlify.com/) 12 | 13 | Admin panel can be found in [Admin repository](https://github.com/gatsbyjs-ecommerce/admin) 14 | 15 | Required API for mutations can be found in [API repository](https://github.com/gatsbyjs-ecommerce/api) 16 | 17 | More info about this written here for better understanding [Creating Static E-commerce site with GatsbyJs](https://medium.com/@pinku1/creating-static-e-commerce-site-with-gatsbyjs-a349d7e022a) 18 | 19 | ## Stack 20 | 21 | - [GatsbyJs](https://www.gatsbyjs.org/) 22 | - [React.js](https://reactjs.org/) 23 | - [Apollo GraphQL](https://www.apollographql.com/) 24 | - [Sanity](https://www.sanity.io/) 25 | 26 | ## To use 27 | 28 | - Fork or download this repository 29 | - Ready! 30 | 31 | To change site config `./src/utils/config.js` 32 | 33 | also add `.env` file in the root, with content for example: 34 | 35 | ``` 36 | SANITY_TOKEN=YOUR_KEY_HERE 37 | ``` 38 | 39 | ## Setup 40 | 41 | Run: 42 | 43 | ``` 44 | yarn install 45 | ``` 46 | 47 | ## Development 48 | 49 | To start development server 50 | 51 | ``` 52 | yarn start 53 | ``` 54 | 55 | ## Deployment 56 | 57 | ``` 58 | yarn run build 59 | yarn serve 60 | ``` 61 | -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | import wrapRoot from './src/utils/wrapRootElement'; 2 | 3 | export const wrapRootElement = wrapRoot; 4 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | const config = require('./src/utils/config.js'); 2 | 3 | module.exports = { 4 | siteMetadata: { 5 | title: config.siteName, 6 | author: config.author, 7 | description: config.description, 8 | siteUrl: config.siteUrl, 9 | }, 10 | plugins: [ 11 | { 12 | resolve: 'gatsby-source-sanity', 13 | options: { 14 | projectId: '2jkk6tlv', 15 | dataset: 'production', 16 | // a token with read permissions is required 17 | // if you have a private dataset 18 | token: process.env.SANITY_TOKEN, 19 | }, 20 | }, 21 | // { 22 | // resolve: `gatsby-source-contentful`, 23 | // options: { 24 | // spaceId: `o6uhtcakujse`, 25 | // accessToken: `42627fbeb9475a7867204b28243ff40aa2aec93995ecac371eea9957dda734b2`, 26 | // downloadLocal: false, 27 | // }, 28 | // }, 29 | `gatsby-plugin-styled-components`, 30 | `gatsby-plugin-react-helmet`, 31 | { 32 | resolve: `gatsby-plugin-google-analytics`, 33 | options: { 34 | trackingId: config.googleAnalytics, 35 | }, 36 | }, 37 | { 38 | resolve: `gatsby-plugin-manifest`, 39 | options: { 40 | name: config.siteName, 41 | short_name: config.siteName, 42 | start_url: config.siteUrl, 43 | background_color: config.backgroundColor, 44 | theme_color: config.themeColor, 45 | display: `minimal-ui`, 46 | icon: `./static/images/logo-1024.png`, 47 | }, 48 | }, 49 | `gatsby-plugin-offline`, 50 | ], 51 | }; 52 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | exports.createPages = async ({ graphql, actions, reporter }) => { 4 | const { createPage } = actions; 5 | 6 | const result = await graphql(` 7 | query { 8 | allSanityProduct { 9 | edges { 10 | node { 11 | _id 12 | slug { 13 | current 14 | } 15 | } 16 | } 17 | } 18 | allSanityPage { 19 | edges { 20 | node { 21 | id 22 | slug { 23 | current 24 | } 25 | } 26 | } 27 | } 28 | allSanityArticle { 29 | edges { 30 | node { 31 | id 32 | slug { 33 | current 34 | } 35 | } 36 | } 37 | } 38 | } 39 | `); 40 | if (result.errors) { 41 | return reporter.panicOnBuild('🚨 ERROR: Loading "createPages" query'); 42 | } 43 | 44 | const products = result.data.allSanityProduct.edges || []; 45 | const pages = result.data.allSanityPage.edges || []; 46 | const articles = result.data.allSanityArticle.edges || []; 47 | 48 | products.forEach(({ node }) => { 49 | createPage({ 50 | path: `product/${node.slug.current}`, 51 | component: path.resolve(`src/components/ProductView.js`), 52 | // additional data can be passed via context 53 | context: { 54 | slug: node.slug.current, 55 | }, 56 | }); 57 | }); 58 | 59 | articles.forEach(({ node }) => { 60 | createPage({ 61 | path: `article/${node.slug.current}`, 62 | component: path.resolve(`src/components/ArticleView.js`), 63 | // additional data can be passed via context 64 | context: { 65 | slug: node.slug.current, 66 | }, 67 | }); 68 | }); 69 | 70 | pages.forEach(({ node }) => { 71 | createPage({ 72 | path: `page/${node.slug.current}`, 73 | component: path.resolve(`src/components/PageView.js`), 74 | // additional data can be passed via context 75 | context: { 76 | slug: node.slug.current, 77 | }, 78 | }); 79 | }); 80 | }; 81 | -------------------------------------------------------------------------------- /gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | import wrapRoot from './src/utils/wrapRootElement'; 2 | 3 | export const wrapRootElement = wrapRoot; 4 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "public" 3 | command = "npm run build" 4 | [build.environment] 5 | YARN_VERSION = "1.3.2" 6 | YARN_FLAGS = "--no-ignore-optional" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsbyjs-ecommerce", 3 | "description": "Static E-commerce site built using GatsbyJs", 4 | "version": "3.0.1", 5 | "author": "Parminder Klair", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/perminder-klair/kickoff-gatsbyjs.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/perminder-klair/kickoff-gatsbyjs/issues" 13 | }, 14 | "homepage": "https://github.com/perminder-klair/kickoff-gatsbyjs#readme", 15 | "scripts": { 16 | "start": "npm run develop", 17 | "develop": "gatsby develop", 18 | "serve": "gatsby serve", 19 | "build": "gatsby build", 20 | "lint": "eslint --ignore-path .gitignore \"src/**/*.{js,jsx}\"" 21 | }, 22 | "dependencies": { 23 | "@apollo/react-hooks": "^3.1.0", 24 | "@sanity/block-content-to-react": "^2.0.6", 25 | "apollo-cache-inmemory": "^1.6.3", 26 | "apollo-cache-persist": "^0.1.1", 27 | "apollo-client": "^2.6.4", 28 | "apollo-link-context": "^1.0.19", 29 | "apollo-link-http": "^1.5.16", 30 | "cleave.js": "^1.5.3", 31 | "currency.js": "^1.2.2", 32 | "dayjs": "^1.8.16", 33 | "formik": "^1.5.8", 34 | "gatsby": "^2.15.9", 35 | "gatsby-image": "^2.2.17", 36 | "gatsby-plugin-google-analytics": "^2.1.14", 37 | "gatsby-plugin-manifest": "^2.2.14", 38 | "gatsby-plugin-offline": "^3.0.3", 39 | "gatsby-plugin-react-helmet": "^3.1.6", 40 | "gatsby-plugin-styled-components": "^3.1.4", 41 | "gatsby-remark-images": "^3.1.20", 42 | "gatsby-source-sanity": "^5.0.2", 43 | "graphql-tag": "^2.10.1", 44 | "isomorphic-fetch": "^2.2.1", 45 | "lodash": "^4.17.15", 46 | "polished": "^3.4.1", 47 | "prop-types": "^15.7.2", 48 | "randomstring": "^1.1.5", 49 | "react": "^16.9.0", 50 | "react-accessible-accordion": "^2.4.2", 51 | "react-dom": "^16.9.0", 52 | "react-helmet": "^5.2.1", 53 | "react-image-gallery": "^0.9.1", 54 | "react-share": "^3.0.1", 55 | "react-spring": "^5.3.8", 56 | "styled-components": "^4.3.2", 57 | "styled-reset-advanced": "^1.0.1", 58 | "sweetalert": "^2.1.2", 59 | "yup": "^0.27.0" 60 | }, 61 | "devDependencies": { 62 | "babel-eslint": "10.0.3", 63 | "babel-plugin-styled-components": "^1.10.6", 64 | "eslint-config-airbnb": "^18.0.1", 65 | "eslint-config-prettier": "^6.2.0", 66 | "eslint-plugin-import": "^2.18.2", 67 | "eslint-plugin-prettier": "^3.1.0", 68 | "eslint-plugin-react": "^7.14.3", 69 | "prettier": "^1.18.2", 70 | "pretty-quick": "^1.11.1" 71 | }, 72 | "keywords": [ 73 | "npm", 74 | "node", 75 | "gatsbyjs", 76 | "javascript", 77 | "graphql", 78 | "graphql", 79 | "reactjs", 80 | "boilerplate" 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /src/components/ArticleView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { graphql } from 'gatsby'; 3 | 4 | import config from '../utils/config'; 5 | import Seo from './Seo'; 6 | import Layout from './Layout'; 7 | import Heading from './Heading'; 8 | 9 | export const articleQuery = graphql` 10 | query ArticleByPath($slug: String!) { 11 | sanityArticle(slug: { current: { eq: $slug } }) { 12 | id 13 | title 14 | slug { 15 | current 16 | } 17 | description 18 | } 19 | } 20 | `; 21 | 22 | export default class ArticleView extends React.Component { 23 | render() { 24 | const { data } = this.props; 25 | const page = data.sanityArticle; 26 | 27 | return ( 28 | 29 | 34 |
35 |
36 | {page.title} 37 | {page.description} 38 |
39 |
40 |
41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/CartItems.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | import { useApolloClient } from '@apollo/react-hooks'; 4 | 5 | import { formatCurrency } from '../utils/helpers'; 6 | import CouponForm from './CouponForm'; 7 | 8 | const Item = styled.article` 9 | min-height: 200px; 10 | .image { 11 | height: auto; 12 | } 13 | img.cart-item-image { 14 | object-fit: cover; 15 | width: 128px; 16 | height: auto; 17 | } 18 | .remove { 19 | color: ${props => props.theme.primaryColor}; 20 | text-transform: uppercase; 21 | font-size: 0.8rem; 22 | margin-left: 1rem; 23 | } 24 | `; 25 | 26 | const BuyBtn = styled.button` 27 | width: 100%; 28 | margin-top: 3rem; 29 | `; 30 | 31 | const CartItems = ({ showCheckoutBtn, handlePayment, cartItems }) => { 32 | const [total, setTotal] = useState(0); 33 | const [discount, setDiscount] = useState(0); 34 | const [couponCode, setCouponCode] = useState(null); 35 | const client = useApolloClient(); 36 | // console.log('cartItems', cartItems); 37 | 38 | if (cartItems.length === 0) { 39 | return

No items in your cart.

; 40 | } 41 | 42 | const handleRemoveItem = index => { 43 | cartItems.splice(index, 1); 44 | client.writeData({ data: { cartItems } }); 45 | }; 46 | 47 | const handleApplyDiscount = ({ discountPercentage, code }) => { 48 | const discountNew = (discountPercentage / 100) * total; 49 | setDiscount(discountNew); 50 | setCouponCode(code); 51 | }; 52 | 53 | const calculateTotal = () => { 54 | let newTotal = 0; 55 | cartItems.forEach(item => { 56 | newTotal += item.price; 57 | }); 58 | if (total !== newTotal) { 59 | setTimeout(() => { 60 | setTotal(newTotal); 61 | }, 300); 62 | } 63 | }; 64 | 65 | // run everytime cart item updates 66 | useEffect(() => { 67 | calculateTotal(); 68 | }, [cartItems]); 69 | 70 | return ( 71 | <> 72 | {cartItems.map((item, index) => ( 73 | 74 | {item.image && ( 75 |
76 |
77 | {item.title} 82 | {/* {item.image.title} */} 88 |
89 |
90 | )} 91 |
92 |
93 |

94 | {item.title}{' '} 95 | 96 | {item.sku} 97 | 98 |
99 | 100 | {formatCurrency(item.price)} 101 | 102 | handleRemoveItem(index)}> 103 | remove 104 | 105 |

106 |
107 |
108 |
109 | ))} 110 |
111 |
112 |
113 | {!showCheckoutBtn && ( 114 | <> 115 | handleApplyDiscount(values)} 117 | /> 118 |
119 | 120 | )} 121 |
122 |
123 |

124 | Shipping:{' '} 125 | {formatCurrency(0)} 126 |

127 | {discount > 0 && ( 128 |

129 | Discount:{' '} 130 | 131 | -{formatCurrency(discount)} 132 | 133 |

134 | )} 135 |

136 | Total:{' '} 137 | 138 | {formatCurrency(total - discount)} 139 | 140 |

141 |
142 |
143 | {showCheckoutBtn && ( 144 | { 147 | handlePayment({ 148 | items: cartItems, 149 | total, 150 | discount, 151 | couponCode, 152 | }); 153 | }}> 154 | Checkout 155 | 156 | )} 157 | 158 | ); 159 | }; 160 | 161 | export default CartItems; 162 | -------------------------------------------------------------------------------- /src/components/CartSteps.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Spring, animated } from 'react-spring'; 3 | import randomstring from 'randomstring'; 4 | import gql from 'graphql-tag'; 5 | import { useQuery, useMutation, useApolloClient } from '@apollo/react-hooks'; 6 | import { isEmpty } from 'lodash'; 7 | 8 | import Heading from './Heading'; 9 | import CheckoutProgress from './CheckoutProgress'; 10 | import CartItems from './CartItems'; 11 | import CheckoutForm from './CheckoutForm'; 12 | import PaymentForm from './PaymentForm'; 13 | import PaymentConfirmed from './PaymentConfirmed'; 14 | 15 | const cartQuery = gql` 16 | query CartItems { 17 | cartItems @client { 18 | id 19 | title 20 | sku 21 | quantity 22 | price 23 | image 24 | } 25 | } 26 | `; 27 | 28 | const createOrderMutation = gql` 29 | mutation createOrder($input: OrderInput!) { 30 | createOrder(input: $input) { 31 | id 32 | orderId 33 | } 34 | } 35 | `; 36 | 37 | const verifyCardMutation = gql` 38 | mutation verifyCard($input: VerifyCardInput!) { 39 | verifyCard(input: $input) { 40 | id 41 | } 42 | } 43 | `; 44 | 45 | const CartSteps = () => { 46 | const client = useApolloClient(); 47 | const [activeStep, setActiveStep] = useState(1); 48 | const [userData, setUserData] = useState({}); 49 | const [paymentData, setPaymentData] = useState({}); 50 | const [orderData, setOrderData] = useState({}); 51 | const [createOrder, { data: createOrderResult }] = useMutation( 52 | createOrderMutation, 53 | ); 54 | const [verifyCard, { data: verifyCardResult }] = useMutation( 55 | verifyCardMutation, 56 | ); 57 | const { data } = useQuery(cartQuery); 58 | const cartItems = data ? data.cartItems || [] : []; 59 | console.log('data', data, verifyCardResult, createOrderResult); 60 | 61 | useEffect(() => { 62 | // make verifyCard mutation to generate token 63 | if (!isEmpty(paymentData)) { 64 | verifyCard({ variables: { input: paymentData } }); 65 | } 66 | }, [paymentData]); 67 | 68 | useEffect(() => { 69 | console.log('now create order', verifyCardResult); 70 | if (!verifyCardResult) { 71 | return; 72 | } 73 | const tokenId = verifyCardResult.verifyCard.id; 74 | const orderId = randomstring.generate(6).toUpperCase(); 75 | const { email, fullName, ...address } = userData; 76 | const productIds = cartItems.map(item => { 77 | return item.id; 78 | }); 79 | createOrder({ 80 | variables: { 81 | input: { 82 | tokenId, 83 | orderId, 84 | customer: { email, fullName, address: { ...address } }, 85 | productIds, 86 | }, 87 | }, 88 | }); 89 | }, [verifyCardResult]); 90 | 91 | useEffect(() => { 92 | console.log('now show success', createOrderResult); 93 | if (!createOrderResult) { 94 | return; 95 | } 96 | setOrderData(createOrderResult.createOrder); 97 | setActiveStep(4); 98 | 99 | // empty cart 100 | client.writeData({ data: { cartItems: [] } }); 101 | }, [createOrderResult]); 102 | 103 | return ( 104 |
105 |
106 | Cart 107 | 113 | {styles => ( 114 | 115 | 116 | 117 | )} 118 | 119 |
120 | 124 | {stylesProps => ( 125 | 128 | { 132 | setActiveStep(2); 133 | }} 134 | /> 135 | 136 | )} 137 | 138 |
139 | { 143 | setActiveStep(2); 144 | }} 145 | /> 146 |
147 |
148 | {activeStep === 2 && ( 149 | { 151 | setActiveStep(3); 152 | setUserData(data2); 153 | }} 154 | /> 155 | )} 156 | {activeStep === 3 && ( 157 | { 159 | setPaymentData(data2); 160 | }} 161 | /> 162 | )} 163 | {activeStep === 4 && } 164 |
165 |
166 |
167 |
168 | ); 169 | }; 170 | 171 | export default CartSteps; 172 | -------------------------------------------------------------------------------- /src/components/CheckoutForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Spring, animated } from 'react-spring'; 4 | import { isUndefined } from 'lodash'; 5 | import { withFormik } from 'formik'; 6 | import * as Yup from 'yup'; 7 | 8 | const BuyBtn = styled.button` 9 | width: 100%; 10 | margin-top: 3rem; 11 | `; 12 | 13 | class CheckoutForm extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | 17 | this.state = { isVisible: false }; 18 | } 19 | 20 | componentDidMount() { 21 | const isMobile = !isUndefined(global.window) 22 | ? global.window.innerWidth < 768 23 | : false; 24 | setTimeout(() => { 25 | this.setState({ isVisible: true }); 26 | 27 | // const scroll = new SmoothScroll(); 28 | // scroll.animateScroll(isMobile ? 1100 : 450); 29 | }, 200); 30 | } 31 | 32 | render() { 33 | const { isVisible } = this.state; 34 | const { 35 | values, 36 | touched, 37 | errors, 38 | isSubmitting, 39 | handleSubmit, 40 | handleChange, 41 | handleBlur, 42 | } = this.props; 43 | 44 | return ( 45 | <> 46 | 50 | {stylesProps => ( 51 | 52 |
53 |
54 | 55 |
56 | 64 | {errors.fullName && touched.fullName && ( 65 |

{errors.fullName}

66 | )} 67 |
68 |
69 |
70 | 71 |
72 | 79 | {errors.addressLine1 && touched.addressLine1 && ( 80 |

{errors.addressLine1}

81 | )} 82 |
83 |
84 |
85 | 86 |
87 | 94 | {errors.addressLine2 && touched.addressLine2 && ( 95 |

{errors.addressLine2}

96 | )} 97 |
98 |
99 |
100 |
101 |
102 | 103 |
104 | 111 | {errors.city && touched.city && ( 112 |

{errors.city}

113 | )} 114 |
115 |
116 |
117 | 118 |
119 | 126 | {errors.postcode && touched.postcode && ( 127 |

{errors.postcode}

128 | )} 129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 | 137 |
138 | 145 | {errors.state && touched.state && ( 146 |

{errors.state}

147 | )} 148 |
149 |
150 |
151 | 152 |
153 | 160 | {errors.country && touched.country && ( 161 |

{errors.country}

162 | )} 163 |
164 |
165 |
166 |
167 |
168 | 169 |
170 | 177 | {errors.email && touched.email && ( 178 |

{errors.email}

179 | )} 180 |
181 |
182 |
183 | 184 |
185 | 192 | {errors.telephone && touched.telephone && ( 193 |

{errors.telephone}

194 | )} 195 |
196 |
197 | 201 | 202 | 203 | 204 | Make payment 205 | 206 | 207 |
208 | )} 209 |
210 | 211 | ); 212 | } 213 | } 214 | 215 | export default withFormik({ 216 | mapPropsToValues: () => ({ 217 | fullName: '', 218 | addressLine1: '', 219 | addressLine2: '', 220 | city: '', 221 | postcode: '', 222 | state: '', 223 | country: '', 224 | email: '', 225 | telephone: '', 226 | }), 227 | validationSchema: Yup.object().shape({ 228 | fullName: Yup.string().required('Full name is required.'), 229 | addressLine1: Yup.string().required('Address Line 1 is required.'), 230 | city: Yup.string().required('City is required.'), 231 | postcode: Yup.string().required('Postcode is required.'), 232 | state: Yup.string().required('State is required.'), 233 | country: Yup.string().required('Country is required.'), 234 | email: Yup.string() 235 | .email('Invalid email address') 236 | .required('Email is required!'), 237 | telephone: Yup.string().required('Telephone is required!'), 238 | }), 239 | handleSubmit: (values, { setSubmitting, props }) => { 240 | // console.log('handle submit', values, props); 241 | // $('.checkout-form-btn').addClass('is-loading'); 242 | setSubmitting(false); 243 | setTimeout(() => props.handlePayment(values), 350); 244 | }, 245 | displayName: 'CheckoutForm', // helps with React DevTools 246 | })(CheckoutForm); 247 | -------------------------------------------------------------------------------- /src/components/CheckoutProgress.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | 5 | const Progress = styled.div` 6 | border-top: 1px solid #979797; 7 | margin: 3rem 2rem; 8 | .active { 9 | font-weight: bold; 10 | .dot { 11 | background-color: #000 !important; 12 | } 13 | } 14 | .step { 15 | float: left; 16 | width: 33.3%; 17 | .dot { 18 | width: 15px; 19 | height: 15px; 20 | background-color: #797979; 21 | border-radius: 8px; 22 | margin: -9px auto 0 auto; 23 | } 24 | } 25 | .step.one { 26 | text-align: left; 27 | .dot { 28 | margin: -9px 0 0 0; 29 | } 30 | } 31 | .step.two { 32 | text-align: center; 33 | } 34 | .step.three { 35 | text-align: right; 36 | .dot { 37 | margin: -9px 0 0 97%; 38 | } 39 | } 40 | `; 41 | 42 | const CheckoutProgress = ({ activeStep }) => ( 43 | 44 |
45 |
46 | Shipping 47 |
48 |
49 |
50 | Payment 51 |
52 |
53 |
54 | Confirm 55 |
56 | 57 | ); 58 | 59 | CheckoutProgress.defaultProps = { 60 | activeStep: 1, 61 | }; 62 | 63 | CheckoutProgress.propTypes = { 64 | activeStep: PropTypes.number, 65 | }; 66 | 67 | export default CheckoutProgress; 68 | -------------------------------------------------------------------------------- /src/components/Content.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseBlockContent from '@sanity/block-content-to-react'; 3 | 4 | export default ({ content, className }) => ( 5 |
{content}
6 | ); 7 | 8 | export const HTMLContent = ({ content, className }) => ( 9 |
10 | ); 11 | 12 | export const BlockContent = ({ blocks }) => ( 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /src/components/CouponForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { withFormik } from 'formik'; 4 | import { graphql } from 'gatsby'; 5 | import swal from 'sweetalert'; 6 | 7 | // import apolloClient from '../utils/apolloClient'; 8 | 9 | const couponMutation = graphql` 10 | mutation validateCoupon($code: String!) { 11 | validateCoupon(code: $code) { 12 | code 13 | details 14 | discountPercentage 15 | } 16 | } 17 | `; 18 | 19 | class CouponForm extends React.Component { 20 | render() { 21 | const { 22 | values, 23 | isSubmitting, 24 | handleSubmit, 25 | handleChange, 26 | handleBlur, 27 | } = this.props; 28 | 29 | return ( 30 |
31 |
32 |
33 | 41 |
42 |
43 | 49 |
50 |
51 |
52 | ); 53 | } 54 | } 55 | 56 | CouponForm.propTypes = { 57 | handleSubmit: PropTypes.func.isRequired, 58 | }; 59 | 60 | export default withFormik({ 61 | mapPropsToValues: () => ({ 62 | couponCode: '', 63 | }), 64 | handleSubmit: (values, { setSubmitting, props }) => { 65 | // console.log('handle submit', values, props); 66 | // $('.coupon-form-btn').addClass('is-loading'); 67 | // apolloClient 68 | // .mutate({ 69 | // mutation: couponMutation, 70 | // variables: { code: values.couponCode }, 71 | // }) 72 | // .then(result => { 73 | // // console.log('result', result); 74 | // swal(`Applied: ${result.data.validateCoupon.details}`); 75 | // setSubmitting(false); 76 | // setTimeout(() => props.handleSubmit(result.data.validateCoupon), 200); 77 | // // $('.coupon-form-btn').removeClass('is-loading'); 78 | // }) 79 | // .catch(() => { 80 | // setSubmitting(false); 81 | // swal('Invalid coupon code.', 'error'); 82 | // // $('.coupon-form-btn').removeClass('is-loading'); 83 | // }); 84 | }, 85 | displayName: 'CouponForm', // helps with React DevTools 86 | })(CouponForm); 87 | -------------------------------------------------------------------------------- /src/components/CouponItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import dayjs from 'dayjs'; 3 | 4 | export default ({ data }) => ( 5 |
6 |
7 |

8 | {data.title} 9 |

10 |
11 |
12 |
{data.description}
13 |
14 | 30 |
31 | ); 32 | -------------------------------------------------------------------------------- /src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { Link } from 'gatsby'; 5 | 6 | import config from '../utils/config'; 7 | import SocialIcons from './SocialIcons'; 8 | import SubscribeForm from './SubscribeForm'; 9 | import ScrollButton from './ScrollButton'; 10 | 11 | const Container = styled.footer` 12 | padding-bottom: 80px; 13 | background-color: #2f2f2f; 14 | position: relative; 15 | margin-top: 6rem; 16 | p { 17 | color: #ffffff !important; 18 | } 19 | `; 20 | 21 | const Heading = styled.p` 22 | margin-bottom: 1rem; 23 | `; 24 | 25 | const Bottom = styled.div` 26 | background-color: #000000; 27 | width: 100%; 28 | position: absolute; 29 | bottom: 0; 30 | > .section { 31 | padding: 1.4rem 1.5rem; 32 | } 33 | `; 34 | 35 | const NavItems = [ 36 | { id: 2, name: 'Customer Care 24/7', url: '/contact' }, 37 | { id: 5, name: 'Delivery Information', url: '/page/delivery-information' }, 38 | { id: 6, name: 'Exchanges & Returns', url: '/page/return-policy' }, 39 | { id: 7, name: 'Gift Vouchers', url: '/coupons' }, 40 | { id: 1, name: 'About us', url: '/page/about' }, 41 | { id: 3, name: 'Terms and Conditions', url: '/page/terms-and-condition' }, 42 | { id: 4, name: 'Privacy Policy', url: '/page/privacy-policy' }, 43 | ]; 44 | 45 | const Footer = ({ home }) => ( 46 | 47 |
48 |
49 |
50 | Customer service 51 |
    52 | {NavItems.map(item => ( 53 |
  • 54 | 55 | {item.name} 56 | 57 |
  • 58 | ))} 59 |
60 |
61 |
62 | Subscribe 63 |

Receive special offers when you signup our mailing list

64 | 65 |
66 |
67 | Connect 68 | 69 |
70 |
71 |
72 | 73 |
74 |
75 |
76 |

Copyright © 2018 - {config.siteName}

77 |
78 |
79 | payments cards 84 |
85 |
86 |
87 |
88 | 89 |
90 | ); 91 | 92 | Footer.defaultProps = { 93 | home: {}, 94 | }; 95 | 96 | Footer.propTypes = { 97 | home: PropTypes.object, 98 | }; 99 | 100 | export default Footer; 101 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { Spring, animated } from 'react-spring'; 5 | import { Link } from 'gatsby'; 6 | import { useQuery } from '@apollo/react-hooks'; 7 | import gql from 'graphql-tag'; 8 | 9 | import config from '../utils/config'; 10 | import SocialIcons from './SocialIcons'; 11 | 12 | const cartQuery = gql` 13 | query CartItems { 14 | cartItems @client { 15 | id 16 | } 17 | } 18 | `; 19 | 20 | const Container = styled.div` 21 | margin-top: 0.6rem; 22 | a { 23 | color: #4a4a4a; 24 | } 25 | .navbar { 26 | margin-bottom: 0.6rem; 27 | } 28 | .navbar-menu { 29 | flex-grow: unset; 30 | margin: 0 auto; 31 | .navbar-item { 32 | font-size: 1.1rem; 33 | } 34 | .navbar-item:hover { 35 | color: #4a4a4a; 36 | } 37 | } 38 | img.logo { 39 | max-width: 150px; 40 | } 41 | `; 42 | 43 | const ContainerMobile = styled.div` 44 | position: relative; 45 | img { 46 | width: 100px; 47 | margin-top: 1rem; 48 | margin-left: 1rem; 49 | } 50 | .menu-trigger { 51 | position: absolute; 52 | top: 4rem; 53 | right: 1rem; 54 | font-size: 1.4rem; 55 | color: #4a4a4a; 56 | } 57 | `; 58 | 59 | const MobileMenu = styled(animated.div)` 60 | && { 61 | position: fixed; 62 | left: 0; 63 | top: 161px; 64 | height: 100%; 65 | width: 100%; 66 | background-color: #2f2f2f; 67 | z-index: 2; 68 | padding: 2rem; 69 | overflow: hidden; 70 | a { 71 | color: #fff; 72 | } 73 | .social { 74 | margin-left: 1.2rem; 75 | margin-top: 2rem; 76 | > section { 77 | width: 240px; 78 | .level-item { 79 | float: left; 80 | } 81 | } 82 | } 83 | } 84 | `; 85 | 86 | const Cart = styled.div` 87 | margin-top: 1rem; 88 | font-size: 1.2rem; 89 | width: 80px; 90 | float: right; 91 | position: relative; 92 | a { 93 | color: #4a4a4a !important; 94 | } 95 | span { 96 | font-weight: 700; 97 | padding: 0 0.1rem 0 0.5rem; 98 | } 99 | .count { 100 | background-color: ${config.primaryColor}; 101 | color: #fff; 102 | font-size: 0.6rem; 103 | width: 16px; 104 | height: 16px; 105 | text-align: center; 106 | border-radius: 8px; 107 | position: absolute; 108 | top: -3px; 109 | left: 22px; 110 | } 111 | `; 112 | 113 | const CartMobile = styled.div` 114 | width: 8rem; 115 | float: right; 116 | margin-top: 6rem; 117 | margin-right: 0.3rem; 118 | .count { 119 | left: 16px; 120 | } 121 | `; 122 | 123 | const NavItems = [ 124 | { id: 1, name: 'New In', url: '/' }, 125 | { id: 2, name: 'Coupons', url: '/coupons' }, 126 | { id: 3, name: 'Blog', url: '/blog' }, 127 | { id: 4, name: 'About', url: '/page/about' }, 128 | { id: 5, name: 'Contact', url: '/contact' }, 129 | ]; 130 | 131 | const Header = ({ home }) => { 132 | const [mobileMenuActive, setMobileMenuActive] = useState(false); 133 | const { data } = useQuery(cartQuery); 134 | const cartItems = data ? data.cartItems || [] : []; 135 | 136 | const cart = ( 137 | 138 | 139 | 140 | Cart{' '} 141 | {cartItems.length > 0 && ( 142 |
{cartItems.length}
143 | )} 144 | 145 |
146 | ); 147 | 148 | const toggleMobileMenu = () => { 149 | // if (mobileMenuActive) { 150 | // $('html').removeClass('disable-scroll'); 151 | // } else { 152 | // $('html').addClass('disable-scroll'); 153 | // } 154 | setMobileMenuActive(!mobileMenuActive); 155 | }; 156 | 157 | return ( 158 |
159 | 160 |
161 |
162 | 163 |
164 |
165 | 166 | {`${config.siteName} 171 | 172 |
173 |
174 |

175 | {home.email} |{' '} 176 | {home.telephone} 177 |

178 | {cart} 179 |
180 |
181 | 193 |
194 | 195 |
196 |
197 | 198 | {`${config.siteName} 199 | 200 |
201 |
202 | {mobileMenuActive ? ( 203 | 204 | 205 | 206 | 207 | 208 | ) : ( 209 | 210 | 211 | 212 | )} 213 | {cart} 214 |
215 |
216 | 224 | {styles => ( 225 | 226 | 238 | 239 | )} 240 | 241 |
242 |
243 | ); 244 | }; 245 | 246 | Header.defaultProps = { 247 | home: {}, 248 | }; 249 | 250 | Header.propTypes = { 251 | home: PropTypes.object, 252 | }; 253 | 254 | export default Header; 255 | -------------------------------------------------------------------------------- /src/components/Heading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | 5 | import config from '../utils/config'; 6 | 7 | const Line = styled.div` 8 | height: 3px; 9 | width: 50px; 10 | background-color: ${config.primaryColor}; 11 | margin: 0.6rem auto 3rem auto; 12 | `; 13 | 14 | const Heading = ({ children }) => ( 15 | <> 16 |

17 | {children} 18 |

19 | 20 | 21 | ); 22 | 23 | Heading.propTypes = { 24 | children: PropTypes.string.isRequired, 25 | }; 26 | 27 | export default Heading; 28 | -------------------------------------------------------------------------------- /src/components/HomeAbout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import { HTMLContent } from './Content'; 5 | import Heading from './Heading'; 6 | 7 | const Container = styled.section` 8 | position: relative; 9 | `; 10 | 11 | const HomeAbout = ({ data }) => ( 12 | 13 | Who we are 14 | 15 | 16 | ); 17 | 18 | export default HomeAbout; 19 | -------------------------------------------------------------------------------- /src/components/HomeBanner.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import config from '../utils/config'; 5 | 6 | const ContainerImage = styled.div` 7 | width: 100%; 8 | height: auto; 9 | img { 10 | width: 100%; 11 | height: auto; 12 | } 13 | `; 14 | 15 | const StripMobile = styled.div` 16 | padding: 0.3rem 0; 17 | background-color: #100b0b; 18 | width: 100%; 19 | opacity: 0.9; 20 | `; 21 | 22 | const HomeBanner = ({ data }) => ( 23 | <> 24 | 25 | home banner 26 | 27 | 28 |

29 | {data.homeSliderSubTitle} 30 |

31 |
32 | 33 | ); 34 | 35 | export default HomeBanner; 36 | -------------------------------------------------------------------------------- /src/components/Layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Helmet from 'react-helmet'; 3 | import styled, { ThemeProvider } from 'styled-components'; 4 | import { graphql, StaticQuery } from 'gatsby'; 5 | 6 | import GlobalStyle, { theme } from '../utils/theme'; 7 | import config from '../utils/config'; 8 | import Header from './Header'; 9 | import Footer from './Footer'; 10 | 11 | const Container = styled.div` 12 | min-height: 70vh; 13 | `; 14 | 15 | const query = graphql` 16 | query LayoutQuery { 17 | sanitySiteSettings { 18 | description 19 | telephone 20 | email 21 | address 22 | facebook 23 | twitter 24 | instagram 25 | pinterest 26 | } 27 | } 28 | `; 29 | 30 | const IndexLayout = ({ children, hideHeader }) => { 31 | return ( 32 | 33 | <> 34 | 35 | {config.siteName} 36 | 37 | 38 | 39 | 40 | 41 | { 44 | const home = data.sanitySiteSettings; 45 | return ( 46 | <> 47 | {!hideHeader &&
} 48 | {children} 49 |