├── .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 | [](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 |

82 | {/*
![{item.image.title} {item.image.title}]()
*/}
88 |
89 |
90 | )}
91 |
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 |
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 |
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 |
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 |

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 |

171 |
172 |
173 |
180 |
181 |
193 |
194 |
195 |
196 |
197 |
198 |

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 |
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 |
50 | >
51 | );
52 | }}
53 | />
54 | >
55 |
56 | );
57 | };
58 |
59 | export default IndexLayout;
60 |
--------------------------------------------------------------------------------
/src/components/NewsItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Link } from 'gatsby';
4 | import dayjs from 'dayjs';
5 |
6 | const Container = styled.article`
7 | && {
8 | border-top: none;
9 | margin-top: 2.3rem;
10 | .title {
11 | margin-top: 0.6rem;
12 | margin-bottom: 0.5rem;
13 | }
14 | a {
15 | color: ${props => props.theme.darkAccent};
16 | }
17 | .category {
18 | margin-left: 10px;
19 | }
20 | }
21 | `;
22 |
23 | const NewsItem = ({ post }) => (
24 |
25 |
26 |
27 |
28 | {dayjs(post._createdAt).format('MMMM YYYY')}
29 | {' '}
30 |
31 |
{post.title}
32 |
33 |
{post.description}
34 |
35 |
42 |
43 |
44 | );
45 |
46 | export default NewsItem;
47 |
--------------------------------------------------------------------------------
/src/components/PageView.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 pageQuery = graphql`
10 | query PageByPath($slug: String!) {
11 | sanityPage(slug: { current: { eq: $slug } }) {
12 | id
13 | title
14 | slug {
15 | current
16 | }
17 | description
18 | }
19 | }
20 | `;
21 |
22 | export default class PageView extends React.Component {
23 | render() {
24 | const { data } = this.props;
25 | const page = data.sanityPage;
26 |
27 | return (
28 |
29 |
34 |
35 |
36 | {page.title}
37 | {page.description}
38 |
39 |
40 |
41 | );
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/PaymentConfirmed.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'styled-components';
4 | import { Spring, animated } from 'react-spring';
5 | import { isUndefined } from 'lodash';
6 | import { Link } from 'gatsby';
7 |
8 | import config from '../utils/config';
9 |
10 | const Result = styled.div`
11 | text-align: center;
12 | svg {
13 | font-size: 3rem;
14 | }
15 | .info {
16 | margin: 3rem 1rem;
17 | }
18 | `;
19 |
20 | const OrderId = styled.span`
21 | font-weight: 700;
22 | color: ${config.primaryColor};
23 | `;
24 |
25 | const BuyBtn = styled(Link)`
26 | width: 100%;
27 | margin-top: 3rem;
28 | `;
29 |
30 | class PaymentConfirmed extends React.Component {
31 | constructor(props) {
32 | super(props);
33 |
34 | this.state = { isVisible: false };
35 | }
36 |
37 | componentDidMount() {
38 | const isMobile = !isUndefined(global.window)
39 | ? global.window.innerWidth < 768
40 | : false;
41 | setTimeout(() => {
42 | this.setState({ isVisible: true });
43 |
44 | // const scroll = new SmoothScroll();
45 | // scroll.animateScroll(isMobile ? 1100 : 450);
46 | }, 200);
47 | }
48 |
49 | render() {
50 | const { isVisible } = this.state;
51 | const { orderData } = this.props;
52 |
53 | return (
54 | <>
55 |
59 | {stylesProps => (
60 |
61 |
62 |
63 |
64 | Payment complete
65 |
66 |
67 | Order code is #{orderData.orderId}
68 |
69 | Please check your email
70 |
71 | for delivery updates.
72 |
73 |
74 |
77 | Continue Shopping
78 |
79 |
80 | )}
81 |
82 | >
83 | );
84 | }
85 | }
86 |
87 | PaymentConfirmed.propTypes = {
88 | orderData: PropTypes.object.isRequired,
89 | };
90 |
91 | export default PaymentConfirmed;
92 |
--------------------------------------------------------------------------------
/src/components/PaymentForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Spring, animated } from 'react-spring';
4 | import { withFormik } from 'formik';
5 | import * as Yup from 'yup';
6 | import Cleave from 'cleave.js/react';
7 |
8 | const Cards = styled.div`
9 | margin-bottom: 0rem;
10 | img {
11 | height: 45px;
12 | }
13 | `;
14 |
15 | const BuyBtn = styled.button`
16 | width: 100%;
17 | margin-top: 3rem;
18 | `;
19 |
20 | class PaymentForm extends React.Component {
21 | constructor(props) {
22 | super(props);
23 |
24 | this.state = { isVisible: false };
25 | }
26 |
27 | componentDidMount() {
28 | // const isMobile = !isUndefined(global.window)
29 | // ? global.window.innerWidth < 768
30 | // : false;
31 |
32 | setTimeout(() => {
33 | this.setState({ isVisible: true });
34 |
35 | // const scroll = new SmoothScroll();
36 | // scroll.animateScroll(isMobile ? 1100 : 450);
37 | }, 200);
38 | }
39 |
40 | render() {
41 | const { isVisible } = this.state;
42 | const {
43 | values,
44 | touched,
45 | errors,
46 | isSubmitting,
47 | handleSubmit,
48 | handleChange,
49 | handleBlur,
50 | } = this.props;
51 |
52 | return (
53 | <>
54 |
58 | {stylesProps => (
59 |
60 |
61 |
62 |
63 |
64 | All transactions are secure and encrypted. Credit card
65 | information is never stored.
66 |
67 |
68 |
157 |
158 | )}
159 |
160 | >
161 | );
162 | }
163 | }
164 |
165 | export default withFormik({
166 | mapPropsToValues: () => ({
167 | cardNumber: '',
168 | expMonth: '',
169 | expYear: '',
170 | cvc: '',
171 | }),
172 | validationSchema: Yup.object().shape({
173 | cardNumber: Yup.string().required('Card number is required.'),
174 | expMonth: Yup.string().required('Expiry month is required.'),
175 | expYear: Yup.string().required('Expiry year is required.'),
176 | cvc: Yup.string().required('Card CVC is required.'),
177 | }),
178 | handleSubmit: (values, { setSubmitting, props }) => {
179 | console.log('handle submit', values);
180 | setSubmitting(false);
181 | // $('.payment-form-btn').addClass('is-loading');
182 |
183 | props.handlePayment(values);
184 | },
185 | displayName: 'PaymentForm', // helps with React DevTools
186 | })(PaymentForm);
187 |
--------------------------------------------------------------------------------
/src/components/ProductGallery.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { isUndefined } from 'lodash';
4 | import { Spring, animated } from 'react-spring';
5 | import ImageGallery from 'react-image-gallery';
6 | import styled from 'styled-components';
7 |
8 | const Container = styled.div`
9 | .image-gallery-thumbnails-wrapper {
10 | margin-top: 10px;
11 | }
12 | .image-gallery-thumbnail-inner img {
13 | width: auto;
14 | height: 70px;
15 | }
16 | `;
17 |
18 | class ProductGallery extends React.Component {
19 | constructor(props) {
20 | super(props);
21 |
22 | this.state = { isVisible: false };
23 | }
24 |
25 | componentDidMount() {
26 | setTimeout(() => {
27 | this.setState({ isVisible: true });
28 | }, 400);
29 | }
30 |
31 | componentWillUnmount() {
32 | this.setState({ isVisible: false });
33 | }
34 |
35 | render() {
36 | const { isVisible } = this.state;
37 | const { product } = this.props;
38 |
39 | const isMobile = !isUndefined(global.window)
40 | ? global.window.innerWidth < 768
41 | : false;
42 |
43 | // console.log('images', product.variant.images);
44 | const images = product.variant.images
45 | ? product.variant.images.map(image => ({
46 | original: image.asset.fluid.src,
47 | thumbnail: image.asset.fluid.src,
48 | }))
49 | : [];
50 | // console.log('images 2', images);
51 |
52 | return (
53 |
54 |
61 | {styles => (
62 |
63 |
73 |
74 | )}
75 |
76 |
77 | );
78 | }
79 | }
80 |
81 | ProductGallery.propTypes = {
82 | product: PropTypes.object.isRequired,
83 | };
84 |
85 | export default ProductGallery;
86 |
--------------------------------------------------------------------------------
/src/components/ProductInfo.js:
--------------------------------------------------------------------------------
1 | /* eslint no-underscore-dangle: 0 */
2 |
3 | import React, { useState, useEffect } from 'react';
4 | import PropTypes from 'prop-types';
5 | import styled from 'styled-components';
6 | import { navigateTo } from 'gatsby';
7 | import { useQuery, useApolloClient } from '@apollo/react-hooks';
8 | import gql from 'graphql-tag';
9 | import {
10 | Accordion,
11 | AccordionItem,
12 | AccordionItemTitle,
13 | AccordionItemBody,
14 | } from 'react-accessible-accordion';
15 | import { Spring, animated } from 'react-spring';
16 | import {
17 | FacebookShareButton,
18 | TwitterShareButton,
19 | EmailShareButton,
20 | } from 'react-share';
21 |
22 | import config from '../utils/config';
23 | import { formatCurrency } from '../utils/helpers';
24 | import { BlockContent } from './Content';
25 | import Heading from './Heading';
26 |
27 | // const cartQuery = graphql`
28 | // query {
29 | // cart @client {
30 | // __typename
31 | // items
32 | // count
33 | // }
34 | // }
35 | // `;
36 |
37 | const Price = styled.div`
38 | color: ${config.primaryColor};
39 | font-size: 1.5rem;
40 | margin-top: -2rem;
41 | span {
42 | color: #4a4a4a;
43 | font-size: 1rem;
44 | text-decoration: line-through;
45 | font-weight: light;
46 | }
47 | `;
48 |
49 | const BuyBtn = styled.button`
50 | width: 100%;
51 | margin-top: 3rem;
52 | `;
53 |
54 | const AccordionStyled = styled(Accordion)`
55 | margin-top: 3rem;
56 | .accordion__title {
57 | border-bottom: 1px solid #979797;
58 | padding: 0.9rem 0;
59 | cursor: pointer;
60 | :focus {
61 | outline: none;
62 | }
63 | h3 {
64 | text-transform: uppercase;
65 | font-weight: 700;
66 | }
67 | }
68 | .accordion__body {
69 | display: block;
70 | padding: 1rem 0;
71 | }
72 | .accordion__body--hidden {
73 | display: none;
74 | }
75 | `;
76 |
77 | const ProductCode = styled.p`
78 | color: #b5b5b5 !important;
79 | `;
80 |
81 | const ShareContainer = styled.div`
82 | padding: 0.9rem 0;
83 | border-top: 1px solid #979797;
84 | h3 {
85 | text-transform: uppercase;
86 | font-weight: 700;
87 | float: left;
88 | }
89 | .share-icons {
90 | float: right;
91 | width: 110px;
92 | .level-item {
93 | margin-left: 0.3rem;
94 | }
95 | }
96 | svg {
97 | color: #4a4a4a;
98 | font-size: 1.5rem;
99 | cursor: pointer;
100 | }
101 | `;
102 |
103 | const cartQuery = gql`
104 | query CartItems {
105 | cartItems @client {
106 | id
107 | }
108 | }
109 | `;
110 |
111 | const ProductInfo = ({ product, home }) => {
112 | const [isVisible, setIsVisible] = useState(false);
113 | const client = useApolloClient();
114 | const { data } = useQuery(cartQuery);
115 | const { cartItems } = data || {};
116 | // console.log('product', product);
117 |
118 | useEffect(() => {
119 | setTimeout(() => {
120 | setIsVisible(true);
121 | }, 400);
122 | }, []);
123 |
124 | const metaUrl = `${config.siteUrl}/product/${product.slug.current}`;
125 | const metaTitle = `Checkout ${product.title} at SejalSuits`;
126 |
127 | const addToCart = () => {
128 | // console.log('cartItems', cartItems);
129 | const items = cartItems || [];
130 |
131 | const itemData = {
132 | id: product._id,
133 | sku: product.variant.sku,
134 | title: product.title,
135 | price: product.variant.price,
136 | image: product.variant.featuredImage.asset.fluid.src,
137 | quantity: 1,
138 | __typename: 'CartItem',
139 | };
140 | items.push(itemData);
141 |
142 | client.writeData({ data: { cartItems: items } });
143 |
144 | setTimeout(() => navigateTo('/cart'), 600);
145 | };
146 |
147 | return (
148 | <>
149 | {product.title}
150 |
151 | {formatCurrency(product.variant.discountPrice)}{' '}
152 | {product.variant.discountPrice < product.variant.price && (
153 | {formatCurrency(product.variant.price)}
154 | )}
155 |
156 |
157 | {stylesProps => (
158 |
159 | addToCart()}>
162 | Add to cart
163 |
164 |
165 |
166 |
167 | Product Details
168 |
169 |
170 | {product._rawBody && (
171 |
172 | )}
173 | {/* */}
176 | Color: {product.variant.color}
177 | Made in India
178 | All prices include sales taxes and free UK delivery.
179 | Product Code: {product.variant.sku}
180 |
181 |
182 |
183 |
184 | Delivery & Returns
185 |
186 |
187 | {home.productDeliveryInfo}
188 |
189 | {home.productShippingReturns}
190 |
191 |
192 |
193 |
194 | Share
195 |
196 |
197 |
198 |
202 |
203 |
204 |
205 |
206 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 | )}
223 |
224 | >
225 | );
226 | };
227 |
228 | ProductInfo.propTypes = {
229 | product: PropTypes.object.isRequired,
230 | home: PropTypes.object.isRequired,
231 | };
232 |
233 | export default ProductInfo;
234 |
--------------------------------------------------------------------------------
/src/components/ProductItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'styled-components';
4 | import { animated } from 'react-spring';
5 | import { Link } from 'gatsby';
6 | import Img from 'gatsby-image';
7 |
8 | import config from '../utils/config';
9 | import { formatCurrency } from '../utils/helpers';
10 |
11 | const Container = styled(animated.div)`
12 | .card {
13 | border: none;
14 | box-shadow: none;
15 | .image.is-4by5 {
16 | padding-top: 0;
17 | }
18 | .card-content {
19 | padding-left: 0;
20 | padding-top: 0.8rem;
21 | position: relative;
22 | a {
23 | color: #363636;
24 | }
25 | .price-container {
26 | width: 150px;
27 | position: absolute;
28 | right: 0;
29 | top: 0.5rem;
30 | }
31 | .price {
32 | color: ${config.primaryColor};
33 | }
34 | .old-price {
35 | text-decoration: line-through;
36 | }
37 | }
38 | }
39 | `;
40 |
41 | const Image = styled(Img)`
42 | object-fit: cover;
43 | height: 540px;
44 | width: 100%;
45 | `;
46 |
47 | const ProductItem = ({ item, styles }) => (
48 |
49 |
50 | {item.variant.featuredImage && (
51 |
52 |
53 |
54 |
55 | {/* */}
61 |
62 |
63 |
64 | )}
65 |
66 |
67 |
68 |
69 | {item.title}
70 |
71 | {item.variant && (
72 |
73 | {item.variant.color}
74 |
75 | )}
76 | {item.variant && (
77 |
78 |
79 | {formatCurrency(item.variant.discountPrice)}
80 |
81 | {item.variant.discountPrice < item.variant.price && (
82 |
83 | {formatCurrency(item.variant.price)}
84 |
85 | )}
86 |
87 | )}
88 |
89 |
90 |
91 |
92 |
93 | );
94 |
95 | ProductItem.propTypes = {
96 | styles: PropTypes.object.isRequired,
97 | };
98 |
99 | export default ProductItem;
100 |
--------------------------------------------------------------------------------
/src/components/ProductView.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { graphql, Link } from 'gatsby';
3 | import styled from 'styled-components';
4 |
5 | import config from '../utils/config';
6 | import Seo from './Seo';
7 | import Layout from './Layout';
8 | import ProductGallery from './ProductGallery';
9 | import ProductInfo from './ProductInfo';
10 | import ProductsList from './ProductsList';
11 |
12 | const Container = styled.div`
13 | &&& {
14 | margin-top: 3rem;
15 | }
16 | `;
17 |
18 | const ViewAllBtn = styled(Link)`
19 | padding-right: 2rem;
20 | padding-left: 2rem;
21 | `;
22 |
23 | export const query = graphql`
24 | query ProductViewQuery($slug: String!) {
25 | sanitySiteSettings {
26 | productDeliveryInfo
27 | productShippingReturns
28 | }
29 | sanityProduct(slug: { current: { eq: $slug } }) {
30 | _id
31 | title
32 | slug {
33 | current
34 | }
35 | _rawBody
36 | variant {
37 | color
38 | discountPrice
39 | price
40 | sku
41 | featuredImage {
42 | asset {
43 | fluid(maxWidth: 700) {
44 | ...GatsbySanityImageFluid
45 | }
46 | }
47 | }
48 | images {
49 | asset {
50 | fluid(maxWidth: 700) {
51 | ...GatsbySanityImageFluid
52 | }
53 | }
54 | }
55 | }
56 | }
57 | allSanityProduct(
58 | filter: { status: { eq: "active" }, slug: { current: { ne: $slug } } }
59 | limit: 9
60 | sort: { fields: [_createdAt], order: DESC }
61 | ) {
62 | edges {
63 | node {
64 | _id
65 | title
66 | slug {
67 | current
68 | }
69 | variant {
70 | color
71 | discountPrice
72 | price
73 | sku
74 | featuredImage {
75 | asset {
76 | fluid(maxWidth: 700) {
77 | ...GatsbySanityImageFluid
78 | }
79 | }
80 | }
81 | }
82 | }
83 | }
84 | }
85 | }
86 | `;
87 |
88 | const ProductView = ({ data }) => {
89 | const product = data.sanityProduct;
90 | const products = data.allSanityProduct.edges;
91 | const home = data.sanitySiteSettings;
92 | // console.log('products', products);
93 |
94 | // const metaImage = product.featuredImage
95 | // ? product.featuredImage.sizes.src
96 | // : `${config.url}${config.logo}`;
97 |
98 | return (
99 |
100 |
106 |
107 |
108 |
111 |
114 |
115 |
116 |
117 |
118 | View all
119 |
120 |
121 |
122 |
123 | );
124 | };
125 |
126 | export default ProductView;
127 |
--------------------------------------------------------------------------------
/src/components/ProductsList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'styled-components';
4 | import { Trail } from 'react-spring';
5 |
6 | import ProductItem from './ProductItem';
7 | import Heading from './Heading';
8 |
9 | const Container = styled.section`
10 | position: relative;
11 | `;
12 |
13 | class ProductsList extends React.Component {
14 | constructor(props) {
15 | super(props);
16 |
17 | this.state = { isOpen: false, activeCategory: null };
18 | }
19 |
20 | componentDidMount() {
21 | setTimeout(() => {
22 | this.setState({ isOpen: true });
23 | }, 200);
24 | }
25 |
26 | toggleCategory = category => this.setState({ activeCategory: category });
27 |
28 | render() {
29 | const { title, products } = this.props;
30 | const { isOpen, activeCategory } = this.state;
31 | const keys = products.map(item => item.node._id);
32 |
33 | return (
34 |
35 | {title}
36 |
37 |
42 | {products.map(({ node }) => styles => (
43 |
44 | ))}
45 |
46 |
47 |
48 | );
49 | }
50 | }
51 |
52 | ProductsList.defaultProps = {
53 | title: 'New arrivals',
54 | products: [],
55 | };
56 |
57 | ProductsList.propTypes = {
58 | title: PropTypes.string,
59 | products: PropTypes.array,
60 | };
61 |
62 | export default ProductsList;
63 |
--------------------------------------------------------------------------------
/src/components/ScrollButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const Button = styled.a`
5 | position: absolute;
6 | float: right;
7 | bottom: 8rem;
8 | right: 2rem;
9 | `;
10 |
11 | class ScrollButton extends React.Component {
12 | constructor() {
13 | super();
14 | this.state = {
15 | intervalId: 0,
16 | };
17 | }
18 |
19 | scrollStep = () => {
20 | if (global.window) {
21 | if (global.window.pageYOffset === 0) {
22 | clearInterval(this.state.intervalId);
23 | }
24 | global.window.scroll(
25 | 0,
26 | global.window.pageYOffset - this.props.scrollStepInPx,
27 | );
28 | }
29 | };
30 |
31 | scrollToTop = () => {
32 | const intervalId = setInterval(this.scrollStep, this.props.delayInMs);
33 | this.setState({ intervalId });
34 | };
35 |
36 | render() {
37 | return (
38 | <>
39 |
45 | >
46 | );
47 | }
48 | }
49 |
50 | export default ScrollButton;
51 |
--------------------------------------------------------------------------------
/src/components/Seo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Helmet from 'react-helmet';
3 | import PropTypes from 'prop-types';
4 |
5 | import config from '../utils/config';
6 |
7 | const getSchemaOrgJSONLD = ({ url, title }) => [
8 | {
9 | '@context': 'http://schema.org',
10 | '@type': 'WebSite',
11 | url,
12 | name: title,
13 | alternateName: config.siteName,
14 | },
15 | ];
16 |
17 | const Seo = ({ title, description, url, image }) => {
18 | const pageTitle = `${title} - ${config.siteName}`;
19 |
20 | const schemaOrgJSONLD = getSchemaOrgJSONLD({
21 | url,
22 | pageTitle,
23 | image,
24 | description,
25 | });
26 |
27 | return (
28 |
29 | {/* General tags */}
30 | {pageTitle}
31 |
32 |
33 |
34 | {/* Schema.org tags */}
35 |
38 |
39 | {/* OpenGraph tags */}
40 |
41 |
42 |
43 |
44 |
45 |
46 | {/* Twitter Card tags */}
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | };
55 |
56 | Seo.defaultProps = {
57 | url: config.siteUrl,
58 | image: `${config.siteUrl}/${config.logo}`,
59 | description: config.description,
60 | };
61 |
62 | Seo.propTypes = {
63 | title: PropTypes.string.isRequired,
64 | description: PropTypes.string,
65 | url: PropTypes.string,
66 | image: PropTypes.string,
67 | };
68 |
69 | export default Seo;
70 |
--------------------------------------------------------------------------------
/src/components/SocialIcons.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'styled-components';
4 |
5 | const Container = styled.section`
6 | width: 120px;
7 | min-height: 26px;
8 | .level-item {
9 | margin-right: 0.3rem;
10 | a {
11 | padding: 0 !important;
12 | }
13 | }
14 | svg {
15 | color: ${props => (!props.inverted ? '#000' : '#fff')};
16 | font-size: 1.6rem;
17 | }
18 | `;
19 |
20 | const SocialIcons = ({ inverted, data }) => (
21 |
22 |
27 |
32 |
37 |
42 |
43 | );
44 |
45 | SocialIcons.defaultProps = {
46 | inverted: false,
47 | data: {},
48 | };
49 |
50 | SocialIcons.propTypes = {
51 | inverted: PropTypes.bool,
52 | data: PropTypes.object,
53 | };
54 |
55 | export default SocialIcons;
56 |
--------------------------------------------------------------------------------
/src/components/SubscribeForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { withFormik } from 'formik';
4 | import * as Yup from 'yup';
5 | import { graphql } from 'gatsby';
6 | import swal from 'sweetalert';
7 |
8 | // import apolloClient from '../utils/apolloClient';
9 |
10 | const Container = styled.form`
11 | margin-top: 1rem;
12 | input {
13 | background-color: #3e3e3e;
14 | border: 1px solid #979797;
15 | border-radius: 0px;
16 | color: #fff;
17 | }
18 | `;
19 |
20 | const subscribeMutation = graphql`
21 | mutation subscribe($email: String!) {
22 | subscribe(email: $email) {
23 | email
24 | }
25 | }
26 | `;
27 |
28 | class SubscribeForm extends React.Component {
29 | render() {
30 | const {
31 | values,
32 | touched,
33 | errors,
34 | isSubmitting,
35 | handleSubmit,
36 | handleChange,
37 | handleBlur,
38 | } = this.props;
39 |
40 | return (
41 |
42 |
43 |
44 |
52 | {errors.email && touched.email && (
53 |
{errors.email}
54 | )}
55 |
56 |
57 |
58 |
59 |
65 |
66 |
67 |
68 | );
69 | }
70 | }
71 | export default withFormik({
72 | mapPropsToValues: () => ({
73 | email: '',
74 | }),
75 | validationSchema: Yup.object().shape({
76 | email: Yup.string()
77 | .email('Invalid email address')
78 | .required('Email is required!'),
79 | }),
80 | handleSubmit: (values, { setSubmitting }) => {
81 | // console.log('handle submit', values, props);
82 | // apolloClient
83 | // .mutate({
84 | // mutation: subscribeMutation,
85 | // variables: values,
86 | // })
87 | // .then(() => {
88 | // swal('Subscribed successfully, thank you!');
89 | // setSubmitting(false);
90 | // })
91 | // .catch(() => {
92 | // setSubmitting(false);
93 | // swal('Subscription failed, please try again.', 'error');
94 | // });
95 | },
96 | displayName: 'SubscribeForm', // helps with React DevTools
97 | })(SubscribeForm);
98 |
--------------------------------------------------------------------------------
/src/html.js:
--------------------------------------------------------------------------------
1 | /* eslint react/destructuring-assignment: 0 */
2 | /* eslint jsx-a11y/html-has-lang: 0 */
3 |
4 | import React from 'react';
5 | import PropTypes from 'prop-types';
6 |
7 | export default class HTML extends React.Component {
8 | render() {
9 | return (
10 |
11 |
12 |
13 |
14 |
18 |
19 | {/* Add custom css or scripts here */}
20 |
24 |
28 |
32 |
33 |
37 | {/* Add custom css or scripts here */}
38 |
39 | {this.props.headComponents}
40 |
41 |
42 | {this.props.preBodyComponents}
43 |
48 | {this.props.postBodyComponents}
49 |
50 |
51 | );
52 | }
53 | }
54 |
55 | HTML.propTypes = {
56 | htmlAttributes: PropTypes.object.isRequired,
57 | headComponents: PropTypes.array.isRequired,
58 | bodyAttributes: PropTypes.object.isRequired,
59 | preBodyComponents: PropTypes.array.isRequired,
60 | body: PropTypes.string.isRequired,
61 | postBodyComponents: PropTypes.array.isRequired,
62 | };
63 |
--------------------------------------------------------------------------------
/src/pages/404.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { graphql } from 'gatsby';
4 |
5 | import Layout from '../components/Layout';
6 | import Seo from '../components/Seo';
7 | import ProductsList from '../components/ProductsList';
8 |
9 | const Container = styled.div`
10 | margin-top: 2rem;
11 | margin-bottom: 4rem;
12 | text-align: center;
13 | `;
14 |
15 | // export const notFoundQuery = graphql`
16 | // query notFoundQuery {
17 | // allContentfulProduct(
18 | // filter: { status: { eq: "active" } }
19 | // limit: 6
20 | // sort: { fields: [createdAt], order: DESC }
21 | // ) {
22 | // edges {
23 | // node {
24 | // id
25 | // title
26 | // slug
27 | // color
28 | // originalPrice
29 | // discountPrice
30 | // featuredImage {
31 | // title
32 | // sizes(maxWidth: 550) {
33 | // ...GatsbyContentfulSizes
34 | // }
35 | // }
36 | // }
37 | // }
38 | // }
39 | // }
40 | // `;
41 |
42 | const NotFoundPage = ({ data }) => {
43 | // const { allContentfulProduct: products } = data;
44 | const products = [];
45 |
46 | return (
47 |
48 |
49 |
50 |
51 | NOT FOUND
52 | You just hit a route that doesn't exist... the sadness.
53 |
54 |
55 |
56 |
57 |
58 | );
59 | };
60 |
61 | export default NotFoundPage;
62 |
--------------------------------------------------------------------------------
/src/pages/blog.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { graphql } from 'gatsby';
4 |
5 | import Layout from '../components/Layout';
6 | import Seo from '../components/Seo';
7 | import NewsItem from '../components/NewsItem';
8 |
9 | export const pageQuery = graphql`
10 | query blog {
11 | allSanityArticle(sort: { fields: _createdAt, order: DESC }) {
12 | edges {
13 | node {
14 | id
15 | title
16 | slug {
17 | current
18 | }
19 | description
20 | _createdAt
21 | }
22 | }
23 | }
24 | }
25 | `;
26 |
27 | const Container = styled.div`
28 | margin-top: 4rem;
29 | margin-bottom: 6rem;
30 | `;
31 |
32 | const Blog = ({ data }) => {
33 | const { edges: posts } = data.allSanityArticle;
34 |
35 | return (
36 |
37 |
38 |
39 |
40 |
41 | News & Updates
42 |
43 |
44 |
45 | {posts.map(({ node: post }) => (
46 |
47 | ))}
48 |
49 |
50 |
51 |
52 |
53 | );
54 | };
55 |
56 | export default Blog;
57 |
--------------------------------------------------------------------------------
/src/pages/cart.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import config from '../utils/config';
4 | import Seo from '../components/Seo';
5 | import Layout from '../components/Layout';
6 | import CartSteps from '../components/CartSteps';
7 |
8 | const Cart = () => {
9 | return (
10 |
11 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default Cart;
22 |
--------------------------------------------------------------------------------
/src/pages/contact.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { graphql } from 'gatsby';
4 |
5 | import Seo from '../components/Seo';
6 | import Layout from '../components/Layout';
7 |
8 | export const query = graphql`
9 | query ContactQuery {
10 | sanitySiteSettings {
11 | telephone
12 | email
13 | address
14 | }
15 | }
16 | `;
17 |
18 | const Section = styled.div`
19 | .container {
20 | margin-top: 4rem;
21 | }
22 | p {
23 | margin-bottom: 1rem;
24 | }
25 | .image {
26 | width: 500px;
27 | height: auto;
28 | margin: 0 auto;
29 | object-position: center;
30 | }
31 | .button {
32 | margin-top: 2rem;
33 | }
34 | `;
35 |
36 | const Contact = ({ data }) => {
37 | const home = data.sanitySiteSettings;
38 |
39 | return (
40 |
41 |
42 |
43 |
44 |
45 |
46 |
Contact Us
47 |
48 | We’re as accessible as your good neighbour. Feel free
49 |
50 | to give us a shout.
51 |
52 |
53 |
54 | 📍
55 | {' '}
56 | {home.address}
57 |
58 |
59 |
60 | 📧
61 | {' '}
62 | {home.email}
63 |
64 |
65 |
66 | ☎️
67 | {' '}
68 | {home.telephone}
69 |
70 |
71 |
72 |
77 |
78 |
79 |
80 |
81 |

86 |
87 |
88 |
89 |
90 |
91 | );
92 | };
93 |
94 | export default Contact;
95 |
--------------------------------------------------------------------------------
/src/pages/coupons.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { graphql } from 'gatsby';
3 |
4 | import config from '../utils/config';
5 | import Seo from '../components/Seo';
6 | import Layout from '../components/Layout';
7 | import Heading from '../components/Heading';
8 | import CouponItem from '../components/CouponItem';
9 |
10 | export const couponsQuery = graphql`
11 | query Coupons {
12 | allSanityCoupon {
13 | edges {
14 | node {
15 | id
16 | title
17 | expiryDate
18 | discountPercentage
19 | description
20 | code
21 | }
22 | }
23 | }
24 | }
25 | `;
26 |
27 | export default class Coupons extends React.Component {
28 | render() {
29 | const { data } = this.props;
30 | const coupons = data.allSanityCoupon.edges;
31 |
32 | return (
33 |
34 |
39 |
40 |
41 |
Coupons
42 |
43 | {coupons.map(coupon => (
44 |
45 |
46 |
47 | ))}
48 |
49 |
50 |
51 |
52 | );
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { graphql } from 'gatsby';
3 |
4 | import config from '../utils/config';
5 | import Seo from '../components/Seo';
6 | import Layout from '../components/Layout';
7 | import HomeBanner from '../components/HomeBanner';
8 | import ProductsList from '../components/ProductsList';
9 | import HomeAbout from '../components/HomeAbout';
10 |
11 | export const query = graphql`
12 | query HomePageQuery {
13 | sanitySiteSettings {
14 | homeIntro
15 | homeSliderSubTitle
16 | description
17 | }
18 | allSanityProduct {
19 | edges {
20 | node {
21 | _id
22 | title
23 | slug {
24 | current
25 | }
26 | variant {
27 | color
28 | discountPrice
29 | price
30 | sku
31 | featuredImage {
32 | asset {
33 | fluid(maxWidth: 700) {
34 | ...GatsbySanityImageFluid
35 | }
36 | }
37 | }
38 | }
39 | }
40 | }
41 | }
42 | }
43 | `;
44 |
45 | const HomePage = ({ data }) => {
46 | const home = data.sanitySiteSettings;
47 | const products = data.allSanityProduct.edges;
48 |
49 | return (
50 |
51 |
56 |
61 |
62 | );
63 | };
64 |
65 | export default HomePage;
66 |
--------------------------------------------------------------------------------
/src/utils/apolloClient.js:
--------------------------------------------------------------------------------
1 | import ApolloClient from 'apollo-client';
2 | import { InMemoryCache } from 'apollo-cache-inmemory';
3 | import { createHttpLink } from 'apollo-link-http';
4 | import { setContext } from 'apollo-link-context';
5 | import { CachePersistor } from 'apollo-cache-persist';
6 | import fetch from 'isomorphic-fetch';
7 |
8 | import config from './config';
9 | import { resolvers, typeDefs } from './localState';
10 |
11 | const httpLink = createHttpLink({
12 | uri: config.debug ? config.graphQlUriDev : config.graphQlUri,
13 | fetch,
14 | });
15 |
16 | const cache = new InMemoryCache();
17 |
18 | if (process.browser) {
19 | const persistor = new CachePersistor({
20 | cache,
21 | storage: window.localStorage,
22 | debug: config.debug,
23 | });
24 | persistor.restore();
25 | }
26 |
27 | const authLink = setContext(async (_, { headers }) => {
28 | const token = process.browser
29 | ? window.localStorage.getItem('token')
30 | : undefined;
31 |
32 | // return the headers to the context so httpLink can read them
33 | return {
34 | headers: {
35 | ...headers,
36 | authorization: token || '',
37 | },
38 | };
39 | });
40 |
41 | // Purge persistor when the store was reset.
42 | // persistor.purge(); // clear local storage
43 |
44 | const client = new ApolloClient({
45 | link: authLink.concat(httpLink),
46 | cache,
47 | typeDefs,
48 | resolvers,
49 | });
50 |
51 | export default client;
52 |
--------------------------------------------------------------------------------
/src/utils/config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | debug: process.env.NODE_ENV === 'development',
3 |
4 | siteName: 'GatsbyJs Ecommerce',
5 | author: 'Parminder Klair',
6 | description:
7 | 'A ecommerce system using ReactJs, bundled with awesome GatsbyJs.',
8 | siteUrl: 'http://kickoff-gatsbyjs.netlify.com',
9 | logo: '/images/logo-1024.png',
10 | graphQlUri: 'http://localhost:4000/',
11 | graphQlUriDev: 'http://localhost:4000/',
12 |
13 | homeBannerImage: '/images/home-bg-3.jpg',
14 | type: 'website',
15 | googleAnalytics: '',
16 | backgroundColor: '#e0e0e0',
17 | themeColor: '#c62828',
18 |
19 | currency: '£',
20 | stripePublishableKey:
21 | process.env.NODE_ENV === 'development'
22 | ? 'pk_test_P0DEB2otulfya51U9lIkLXAn'
23 | : 'pk_live_eMN5tHGymDNn3DOZH8MX5ziD',
24 | };
25 |
--------------------------------------------------------------------------------
/src/utils/helpers.js:
--------------------------------------------------------------------------------
1 | import currency from 'currency.js';
2 |
3 | import config from './config';
4 |
5 | export const formatCurrency = value =>
6 | currency(parseFloat(value), {
7 | symbol: `${config.currency} `,
8 | formatWithSymbol: true,
9 | }).format();
10 |
11 | export const log = value => console.log(value); // eslint-disable-line
12 |
--------------------------------------------------------------------------------
/src/utils/localState.js:
--------------------------------------------------------------------------------
1 | import { graphql } from 'gatsby';
2 |
3 | export const typeDefs = graphql`
4 | type CartItem {
5 | id: ID!
6 | sku: String!
7 | title: String!
8 | price: Int!
9 | image: String
10 | quantity: Int!
11 | }
12 |
13 | extend type Query {
14 | isLoggedIn: Boolean!
15 | cartItems: [CartItem]!
16 | }
17 |
18 | # extend type Launch {
19 | # isInCart: Boolean!
20 | # }
21 |
22 | extend type Mutation {
23 | addOrRemoveFromCart(id: ID!): [CartItem]
24 | }
25 | `;
26 |
27 | export const resolvers = {};
28 |
--------------------------------------------------------------------------------
/src/utils/theme.js:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components';
2 | import reset from 'styled-reset-advanced';
3 | import { darken, lighten } from 'polished';
4 |
5 | const mainBrandColor = '#00D1B2';
6 | const lightShades = '#F5F5F5';
7 | const darkAccent = '#3173DC';
8 | const darkShades = '#0A0A0A';
9 |
10 | export const theme = {
11 | // It can be liberally applied to your layout as its main identity.
12 | mainBrandColor,
13 | // Accent colors can be used to bring attention to design elements
14 | // by contrasting with the rest of the palette.
15 | lightAccent: '#FFDC57',
16 | // Use this color as the background for your dark-on-light designs,
17 | // or the text color of an inverted design.
18 | lightShades,
19 | // Another accent color to consider. Not all colors have to be used -
20 | // sometimes a simple color scheme works best.
21 | darkAccent,
22 | // Use as the text color for dark-on-light designs,
23 | // or as the background for inverted designs.
24 | darkShades,
25 | dangerColor: '#f44336',
26 |
27 | primaryColor: mainBrandColor,
28 | borderColor: '#e0e6ef',
29 | backgroundColor: '#FFFFFF',
30 | backgroundInputColor: lightShades,
31 | backgroundInputColorDark: darkShades,
32 | fontSize: 16,
33 | fontSizeSmall: 14,
34 | fontSizeExtraSmall: 12,
35 | fontSizeMedium: 18,
36 | fontSizeLarge: 22,
37 | textColor: darkShades, // '#0A0B11',
38 | textColorInverse: lightShades,
39 | textColorLite: '#8B8989',
40 | menuTintColor: darkAccent,
41 | primaryFontFamily: "'Open Sans', sans-serif",
42 | secondaryFontFamily: "'Open Sans', sans-serif",
43 | boxShadow: 'rgba(0,0,0,0.08) 0px 7px 18px',
44 | };
45 |
46 | const GlobalStyle = createGlobalStyle`
47 | ${reset};
48 |
49 | body {
50 | font-family: ${theme.secondaryFontFamily};
51 | color: ${theme.textColor};
52 | letter-spacing: 0.03rem !important;
53 | font-size: 17px;
54 | }
55 | .title {
56 | font-family: ${theme.primaryFontFamily};
57 | }
58 | .button {
59 | font-family: ${theme.primaryFontFamily};
60 | }
61 | p {
62 | line-height: 1.5rem;
63 | }
64 | p, .title, .box {
65 | color: ${theme.textColor} !important;
66 | }
67 | .subtitle {
68 | color: ${lighten(0.06, theme.textColor)} !important;
69 | }
70 | .button.is-primary {
71 | background-color: ${theme.mainBrandColor};
72 | transition: background-color 0.2s ease;
73 | :hover {
74 | background-color: ${darken(0.06, theme.mainBrandColor)};
75 | }
76 | }
77 | .button.is-secondary {
78 | background-color: ${theme.lightAccent};
79 | transition: background-color 0.2s ease;
80 | color: #ffffff;
81 | :hover {
82 | background-color: ${darken(0.06, theme.lightAccent)};
83 | }
84 | }
85 | .button.is-link {
86 | background-color: ${theme.darkAccent};
87 | transition: background-color 0.2s ease;
88 | :hover {
89 | background-color: ${darken(0.06, theme.darkAccent)};
90 | }
91 | }
92 | .button, .input, .card {
93 | box-shadow: ${theme.boxShadow};
94 | }
95 | .has-text-warning {
96 | color: ${theme.lightAccent} !important;
97 | }
98 | `;
99 |
100 | export default GlobalStyle;
101 |
--------------------------------------------------------------------------------
/src/utils/wrapRootElement.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ApolloProvider } from '@apollo/react-hooks';
3 | import apolloClient from './apolloClient';
4 |
5 | export default ({ element }) => (
6 | {element}
7 | );
8 |
--------------------------------------------------------------------------------
/static/images/contact.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/home-bg-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gatsbyjs-ecommerce/web-v1-old/0f25c24d1dfd7f7181e5da21effc75b619020002/static/images/home-bg-3.jpg
--------------------------------------------------------------------------------
/static/images/logo-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gatsbyjs-ecommerce/web-v1-old/0f25c24d1dfd7f7181e5da21effc75b619020002/static/images/logo-1024.png
--------------------------------------------------------------------------------
/static/images/payment-strip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gatsbyjs-ecommerce/web-v1-old/0f25c24d1dfd7f7181e5da21effc75b619020002/static/images/payment-strip.png
--------------------------------------------------------------------------------
/static/js/scripts.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gatsbyjs-ecommerce/web-v1-old/0f25c24d1dfd7f7181e5da21effc75b619020002/static/js/scripts.js
--------------------------------------------------------------------------------
/static/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------