├── .prettierignore
├── design.jpg
├── postcss.config.js
├── src
├── styles
│ └── site.css
├── images
│ ├── icon.png
│ └── logo.png
├── theme.js
├── components
│ ├── heroComponents
│ │ ├── Spacer.js
│ │ ├── Showcase.js
│ │ ├── Tag.js
│ │ ├── Footer.js
│ │ ├── Center.js
│ │ ├── DisplayMedium.js
│ │ └── DisplaySmall.js
│ ├── Image.js
│ ├── Button.js
│ ├── index.js
│ ├── ListItem.js
│ ├── header.js
│ ├── QuantityPicker.js
│ ├── CartLink.js
│ ├── Nav.js
│ ├── seo.js
│ └── formComponents
│ │ ├── ConfirmSignUp.js
│ │ ├── SignIn.js
│ │ ├── SignUp.js
│ │ └── AddInventory.js
├── pages
│ ├── 404.js
│ ├── admin.js
│ ├── index.js
│ ├── cart.js
│ └── checkout.js
├── templates
│ ├── Inventory.js
│ ├── CategoryView.js
│ ├── ItemView.js
│ └── ViewInventory.js
├── context
│ └── mainContext.js
└── layouts
│ ├── baseLayout.js
│ └── layout.css
├── static
├── fonts
│ ├── Eina.otf
│ ├── Eina.ttf
│ ├── Eina-Light.otf
│ ├── Eina-SemiBold.otf
│ ├── Eina-SemiBold.ttf
│ └── fonts.css
└── images
│ └── products
│ ├── chair1.png
│ ├── chair2.png
│ ├── chair3.png
│ ├── chair4.png
│ ├── chair5.png
│ ├── chair6.png
│ ├── chair7.png
│ ├── chair8.png
│ ├── chair9.png
│ ├── couch1.png
│ ├── couch2.png
│ ├── couch3.png
│ ├── couch4.png
│ ├── couch5.png
│ ├── couch6.png
│ ├── couch7.png
│ ├── couch8.png
│ ├── couch9.png
│ ├── chair10.png
│ ├── couch10.png
│ ├── couch11.png
│ ├── couch12.png
│ ├── couch13.png
│ ├── couch14.png
│ └── couch15.png
├── .prettierrc
├── gatsby-ssr.js
├── gatsby-browser.js
├── gatsby-node.js
├── providers
├── inventoryProvider.js
└── inventory.js
├── LICENSE
├── utils
└── helpers.js
├── .gitignore
├── gatsby-config.js
├── package.json
├── api
└── index.js
├── tailwind.config.js
├── gatsby-node.esm.js
└── README.md
/.prettierignore:
--------------------------------------------------------------------------------
1 | .cache
2 | package.json
3 | package-lock.json
4 | public
5 |
--------------------------------------------------------------------------------
/design.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/design.jpg
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = () => ({
2 | plugins: [require("tailwindcss")],
3 | })
--------------------------------------------------------------------------------
/src/styles/site.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 |
3 | @tailwind components;
4 |
5 | @tailwind utilities;
--------------------------------------------------------------------------------
/src/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/src/images/icon.png
--------------------------------------------------------------------------------
/src/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/src/images/logo.png
--------------------------------------------------------------------------------
/static/fonts/Eina.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/fonts/Eina.otf
--------------------------------------------------------------------------------
/static/fonts/Eina.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/fonts/Eina.ttf
--------------------------------------------------------------------------------
/static/fonts/Eina-Light.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/fonts/Eina-Light.otf
--------------------------------------------------------------------------------
/static/fonts/Eina-SemiBold.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/fonts/Eina-SemiBold.otf
--------------------------------------------------------------------------------
/static/fonts/Eina-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/fonts/Eina-SemiBold.ttf
--------------------------------------------------------------------------------
/static/images/products/chair1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/chair1.png
--------------------------------------------------------------------------------
/static/images/products/chair2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/chair2.png
--------------------------------------------------------------------------------
/static/images/products/chair3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/chair3.png
--------------------------------------------------------------------------------
/static/images/products/chair4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/chair4.png
--------------------------------------------------------------------------------
/static/images/products/chair5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/chair5.png
--------------------------------------------------------------------------------
/static/images/products/chair6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/chair6.png
--------------------------------------------------------------------------------
/static/images/products/chair7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/chair7.png
--------------------------------------------------------------------------------
/static/images/products/chair8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/chair8.png
--------------------------------------------------------------------------------
/static/images/products/chair9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/chair9.png
--------------------------------------------------------------------------------
/static/images/products/couch1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/couch1.png
--------------------------------------------------------------------------------
/static/images/products/couch2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/couch2.png
--------------------------------------------------------------------------------
/static/images/products/couch3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/couch3.png
--------------------------------------------------------------------------------
/static/images/products/couch4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/couch4.png
--------------------------------------------------------------------------------
/static/images/products/couch5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/couch5.png
--------------------------------------------------------------------------------
/static/images/products/couch6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/couch6.png
--------------------------------------------------------------------------------
/static/images/products/couch7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/couch7.png
--------------------------------------------------------------------------------
/static/images/products/couch8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/couch8.png
--------------------------------------------------------------------------------
/static/images/products/couch9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/couch9.png
--------------------------------------------------------------------------------
/src/theme.js:
--------------------------------------------------------------------------------
1 | const colors = {
2 | primary: '#000000',
3 | secondary: '#00baa6'
4 | }
5 |
6 | export {
7 | colors
8 | }
9 |
--------------------------------------------------------------------------------
/static/images/products/chair10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/chair10.png
--------------------------------------------------------------------------------
/static/images/products/couch10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/couch10.png
--------------------------------------------------------------------------------
/static/images/products/couch11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/couch11.png
--------------------------------------------------------------------------------
/static/images/products/couch12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/couch12.png
--------------------------------------------------------------------------------
/static/images/products/couch13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/couch13.png
--------------------------------------------------------------------------------
/static/images/products/couch14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/couch14.png
--------------------------------------------------------------------------------
/static/images/products/couch15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauchg/jamstack-ecommerce/HEAD/static/images/products/couch15.png
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "endOfLine": "lf",
3 | "semi": false,
4 | "singleQuote": false,
5 | "tabWidth": 2,
6 | "trailingComma": "es5"
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/heroComponents/Spacer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Spacer = ({ width }) => (
4 |
5 | )
6 |
7 | export default Spacer
--------------------------------------------------------------------------------
/gatsby-ssr.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Implement Gatsby's SSR (Server Side Rendering) APIs in this file.
3 | *
4 | * See: https://www.gatsbyjs.org/docs/ssr-apis/
5 | */
6 |
7 | // You can delete this file if you're not using it
8 |
9 |
--------------------------------------------------------------------------------
/gatsby-browser.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Implement Gatsby's Browser APIs in this file.
3 | *
4 | * See: https://www.gatsbyjs.org/docs/browser-apis/
5 | */
6 |
7 | // You can delete this file if you're not using it
8 | import "./src/styles/site.css"
9 | import "./src/layouts/layout.css"
10 |
--------------------------------------------------------------------------------
/static/fonts/fonts.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "Eina";
3 | src: url("Eina.otf");
4 | }
5 |
6 | @font-face {
7 | font-family: "Eina Bold";
8 | src: url("Eina-SemiBold.otf");
9 | }
10 |
11 | @font-face {
12 | font-family: "Eina Light";
13 | src: url("Eina-Light.otf");
14 | }
--------------------------------------------------------------------------------
/gatsby-node.js:
--------------------------------------------------------------------------------
1 | require = require('esm')(module)
2 | module.exports = require('./gatsby-node.esm.js')
3 |
4 | /**
5 | * Implement Gatsby's Node APIs in this file.
6 | *
7 | * See: https://www.gatsbyjs.org/docs/node-apis/
8 | */
9 |
10 | // You can delete this file if you're not using it
11 |
--------------------------------------------------------------------------------
/src/components/heroComponents/Showcase.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Image from '../Image'
3 |
4 | const Showcase = ({ imageSrc }) => {
5 | return (
6 |
7 |
8 |
9 | )
10 | }
11 |
12 | export default Showcase
--------------------------------------------------------------------------------
/src/pages/404.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | import SEO from "../components/seo"
4 |
5 | const NotFoundPage = () => (
6 | <>
7 |
8 | NOT FOUND
9 | You just hit a route that doesn't exist... the sadness.
10 | >
11 | )
12 |
13 | export default NotFoundPage
14 |
--------------------------------------------------------------------------------
/src/components/heroComponents/Tag.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Tag = ({ category, year }) => {
4 | return (
5 |
6 |
{category}
7 | { year &&
{year}
}
8 |
9 | )
10 | }
11 |
12 | export default Tag
--------------------------------------------------------------------------------
/src/components/Image.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react"
2 |
3 | async function fetchImage(src, updateSrc) {
4 | // const image = await S3.getimage(src)
5 | updateSrc(src)
6 | }
7 |
8 | const Image = ({ src, ...props}) => {
9 | const [imageSrc, updateSrc] = useState(null)
10 | useEffect(() => {
11 | fetchImage(src, updateSrc)
12 | }, [])
13 |
14 | return imageSrc ? : null
15 | }
16 |
17 | export default Image
18 |
--------------------------------------------------------------------------------
/src/components/heroComponents/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Footer = ({ designer }) => {
4 | return (
5 |
6 |
Design by
7 |
{designer}
8 |
9 | )
10 | }
11 |
12 | export default Footer
--------------------------------------------------------------------------------
/src/components/Button.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function Button({ title, onClick, full = false }) {
4 | let classNames = "text-sm font-bold tracking-wider bg-transparent hover:bg-black text-black font-semibold hover:text-white py-4 px-12 border-2 border-black hover:border-transparent"
5 |
6 | if (full) {
7 | classNames = `${classNames} w-full`
8 | }
9 | return (
10 |
11 |
12 | {title}
13 |
14 |
15 | )
16 | }
--------------------------------------------------------------------------------
/src/components/heroComponents/Center.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button } from '../';
3 | import { navigate } from "gatsby"
4 |
5 | const Center = ({ price, title, link }) => {
6 | function navigateTo() {
7 | navigate(link)
8 | }
9 |
10 | return (
11 |
12 |
{title}
13 |
FROM ${price}
14 |
18 |
19 | )
20 | }
21 |
22 | export default Center
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | import Tag from './heroComponents/Tag'
2 | import Center from './heroComponents/Center'
3 | import Footer from './heroComponents/Footer'
4 | import Showcase from './heroComponents/Showcase'
5 | import DisplaySmall from './heroComponents/DisplaySmall'
6 | import DisplayMedium from './heroComponents/DisplayMedium'
7 | import Spacer from './heroComponents/Spacer'
8 | import Button from './Button'
9 | import Image from './Image'
10 |
11 | export {
12 | Tag,
13 | Center,
14 | Footer,
15 | Button,
16 | Image,
17 | Showcase,
18 | DisplaySmall,
19 | DisplayMedium,
20 | Spacer
21 | }
--------------------------------------------------------------------------------
/providers/inventoryProvider.js:
--------------------------------------------------------------------------------
1 | import inventory from './inventory';
2 |
3 | /*
4 | Inventory items must adhere to the following schema:
5 |
6 | type Product {
7 | id: ID!
8 | categories: [String]!
9 | price: Float!
10 | name: String!
11 | image: String!
12 | description: String!
13 | currentInventory: Int!
14 | brand: String
15 | }
16 | */
17 |
18 | async function getInventory() {
19 | return new Promise((resolve, reject) => {
20 | // const inventory = API.get(apiUrl)
21 | resolve(inventory)
22 | })
23 | }
24 |
25 | const DENOMINATION = '$'
26 |
27 | export { DENOMINATION, getInventory as default }
28 |
--------------------------------------------------------------------------------
/src/components/heroComponents/DisplayMedium.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'gatsby'
3 | import Image from '../Image'
4 |
5 | const DisplayMedium = ({ imageSrc, title, subtitle, link }) => {
6 | return (
7 |
10 |
11 |
12 |
13 |
14 |
15 |
{title}
16 |
{subtitle}
17 |
18 |
19 |
20 | )
21 | }
22 |
23 | export default DisplayMedium;
--------------------------------------------------------------------------------
/src/components/heroComponents/DisplaySmall.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'gatsby'
3 | import { getTrimmedString } from '../../../utils/helpers'
4 | import Image from '../Image'
5 |
6 | const DisplaySmall = ({ link, title, subtitle, imageSrc }) => (
7 |
10 |
11 |
12 |
13 |
14 |
15 |
{title}
16 |
{getTrimmedString(subtitle, 150)}
17 |
18 |
19 |
20 | )
21 |
22 | export default DisplaySmall
--------------------------------------------------------------------------------
/src/components/ListItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'gatsby'
3 | import { DENOMINATION } from '../../providers/inventoryProvider'
4 | import Image from './Image'
5 |
6 | const ListItem = ({ link, title, imageSrc, price }) => (
7 |
13 |
14 |
19 |
20 |
21 |
{title}
22 |
{`${DENOMINATION}${price}`}
23 |
24 |
25 | )
26 |
27 | export default ListItem
--------------------------------------------------------------------------------
/src/components/header.js:
--------------------------------------------------------------------------------
1 | import { Link } from "gatsby"
2 | import PropTypes from "prop-types"
3 | import React from "react"
4 |
5 | const Header = ({ siteTitle }) => (
6 |
32 | )
33 |
34 | Header.propTypes = {
35 | siteTitle: PropTypes.string,
36 | }
37 |
38 | Header.defaultProps = {
39 | siteTitle: ``,
40 | }
41 |
42 | export default Header
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 gatsbyjs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, 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,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/src/components/QuantityPicker.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function QuantityPicker({
4 | increment, decrement, numberOfitems, hideQuantityLabel
5 | }) {
6 | return (
7 |
8 | {
9 | !hideQuantityLabel && (
10 |
QUANTITY
11 | )
12 | }
13 |
+
23 |
{numberOfitems}
27 |
-
36 |
37 | )
38 | }
--------------------------------------------------------------------------------
/src/templates/Inventory.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import AddInventory from '../components/formComponents/AddInventory'
3 | import ViewInventory from './ViewInventory'
4 |
5 | class Inventory extends React.Component {
6 | state = {
7 | viewState: 'view'
8 | }
9 | toggleViewState(viewState) {
10 | this.setState(() => ({ viewState }))
11 | }
12 | render() {
13 | return (
14 |
15 |
Inventory
16 |
17 |
this.toggleViewState('view')}>View
18 |
this.toggleViewState('add')}>Add
19 |
20 | {
21 | this.state.viewState === 'view' ? (
22 |
23 | ) : (
)
24 | }
25 |
26 | Sign Out
27 |
28 |
29 | )
30 | }
31 | }
32 |
33 | export default Inventory
--------------------------------------------------------------------------------
/utils/helpers.js:
--------------------------------------------------------------------------------
1 | function slugify(string) {
2 | const a = 'àáäâãåăæąçćčđďèéěėëêęğǵḧìíïîįłḿǹńňñòóöôœøṕŕřßşśšșťțùúüûǘůűūųẃẍÿýźžż·/_,:;'
3 | const b = 'aaaaaaaaacccddeeeeeeegghiiiiilmnnnnooooooprrsssssttuuuuuuuuuwxyyzzz------'
4 | const p = new RegExp(a.split('').join('|'), 'g')
5 |
6 | return string.toString().toLowerCase()
7 | .replace(/\s+/g, '-') // Replace spaces with -
8 | .replace(p, c => b.charAt(a.indexOf(c))) // Replace special characters
9 | .replace(/&/g, '-and-') // Replace & with 'and'
10 | .replace(/[^\w-]+/g, '') // Remove all non-word characters
11 | .replace(/--+/g, '-') // Replace multiple - with single -
12 | .replace(/^-+/, '') // Trim - from start of text
13 | .replace(/-+$/, '') // Trim - from end of text
14 | }
15 |
16 | function titleIfy(slug) {
17 | var words = slug.split('-');
18 | for (var i = 0; i < words.length; i++) {
19 | var word = words[i];
20 | words[i] = word.charAt(0).toUpperCase() + word.slice(1);
21 | }
22 | return words.join(' ');
23 | }
24 |
25 | function getTrimmedString(string, length = 8) {
26 | if (string.length <= length) {
27 | return string
28 | } else {
29 | return string.substring(0, length) + '...'
30 | }
31 | }
32 |
33 | export {
34 | slugify, titleIfy, getTrimmedString
35 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # dotenv environment variable files
55 | .env*
56 |
57 | # gatsby files
58 | .cache/
59 | public
60 |
61 | # Mac files
62 | .DS_Store
63 |
64 | # Yarn
65 | yarn-error.log
66 | .pnp/
67 | .pnp.js
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | .vercel
--------------------------------------------------------------------------------
/src/components/CartLink.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { SiteContext } from '../context/mainContext'
3 | import { FaShoppingCart, FaCircle } from 'react-icons/fa';
4 | import { Link } from "gatsby"
5 | import { colors } from '../theme'
6 | const { secondary } = colors
7 |
8 | class CartLink extends React.Component {
9 | render() {
10 | let { context: { numberOfItemsInCart } = { numberOfItemsInCart: 0 } } = this.props
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 | {
19 | numberOfItemsInCart > Number(0) && (
20 |
21 |
22 |
23 | )
24 | }
25 |
26 |
27 |
28 | )
29 | }
30 | }
31 |
32 |
33 | function CartLinkWithContext(props) {
34 | return (
35 |
36 | {
37 | context =>
38 | }
39 |
40 | )
41 | }
42 |
43 |
44 | export default CartLinkWithContext
--------------------------------------------------------------------------------
/src/templates/CategoryView.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ListItem from '../components/ListItem'
3 | import { titleIfy, slugify } from '../../utils/helpers'
4 | import CartLink from '../components/CartLink'
5 |
6 | const CategoryView = (props) => {
7 | const { pageContext: { title, content: { items = [] }}} = props
8 | return (
9 | <>
10 |
11 |
12 |
13 |
14 |
{titleIfy(title)}
15 |
16 |
17 |
18 |
19 | {
20 | items.map((item, index) => {
21 | return (
22 |
29 | )
30 | })
31 | }
32 |
33 |
34 |
35 |
36 | >
37 | )
38 | }
39 |
40 | export default CategoryView
--------------------------------------------------------------------------------
/gatsby-config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | siteMetadata: {
3 | title: `Gatsby JAMstack ECommerce Professional`,
4 | description: `Get up and running with your next E Commerce Website.`,
5 | author: `@dabit3`,
6 | },
7 | plugins: [
8 | {
9 | resolve: `gatsby-plugin-layout`,
10 | options: {
11 | component: require.resolve(`./src/layouts/baseLayout.js`),
12 | },
13 | },
14 | `gatsby-plugin-stripe`,
15 | `gatsby-plugin-postcss`,
16 | `gatsby-plugin-react-helmet`,
17 | {
18 | resolve: "gatsby-plugin-web-font-loader",
19 | options: {
20 | custom: {
21 | families: ["Eina, Eina-SemiBold"],
22 | urls: ["/fonts/fonts.css"],
23 | },
24 | },
25 | },
26 | {
27 | resolve: `gatsby-source-filesystem`,
28 | options: {
29 | name: `images`,
30 | path: `${__dirname}/src/images`,
31 | },
32 | },
33 | `gatsby-plugin-offline`,
34 | `gatsby-transformer-sharp`,
35 | `gatsby-plugin-sharp`,
36 | {
37 | resolve: `gatsby-plugin-manifest`,
38 | options: {
39 | name: `gatsby-starter-default`,
40 | short_name: `starter`,
41 | start_url: `/`,
42 | background_color: `#663399`,
43 | theme_color: `#663399`,
44 | display: `minimal-ui`,
45 | icon: `src/images/icon.png`, // This path is relative to the root of the site.
46 | },
47 | },
48 | // this (optional) plugin enables Progressive Web App + Offline functionality
49 | // To learn more, visit: https://gatsby.dev/offline
50 | // `gatsby-plugin-offline`,
51 | ],
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/Nav.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { titleIfy, slugify } from '../../utils/helpers'
4 | import { FaShoppingCart, FaCircle } from 'react-icons/fa';
5 | import { Link } from "gatsby"
6 |
7 | import { SiteContext, ContextProviderComponent } from '../context/mainContext'
8 |
9 | class Nav extends React.Component {
10 | render() {
11 | let { numberOfItemsInCart, navItems: { navInfo: { data: links }}} = this.props.context
12 |
13 | links = links.map(link => {
14 | const newLink = {}
15 | newLink.link = slugify(link)
16 | newLink.name = titleIfy(link)
17 | return newLink
18 | })
19 | links.unshift({ name: 'Home', link: '/'})
20 | return (
21 | <>
22 |
23 | {
24 | links.map((l, i) => (
25 |
26 |
{l.name}
27 |
28 | ))
29 | }
30 |
31 |
32 |
33 |
34 |
35 | {
36 | numberOfItemsInCart > Number(0) && (
37 |
38 |
39 |
40 | )
41 | }
42 |
43 | >
44 | )
45 | }
46 | }
47 |
48 | function NavWithContext(props) {
49 | return (
50 |
51 |
52 | {
53 | context =>
54 | }
55 |
56 |
57 | )
58 | }
59 |
60 | export default NavWithContext
61 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gatsby-ecommerce-professional",
3 | "private": true,
4 | "description": "An E Commerce starter for Gatsby",
5 | "version": "0.1.0",
6 | "author": "Nader Dabit ",
7 | "dependencies": {
8 | "@stripe/react-stripe-js": "^1.0.0-beta.6",
9 | "@stripe/stripe-js": "^1.0.0-beta.8",
10 | "axios": "^0.19.2",
11 | "esm": "^3.2.25",
12 | "fs": "^0.0.1-security",
13 | "gatsby": "^2.19.7",
14 | "gatsby-image": "^2.2.39",
15 | "gatsby-plugin-layout": "^1.1.22",
16 | "gatsby-plugin-manifest": "^2.2.39",
17 | "gatsby-plugin-offline": "^2.2.10",
18 | "gatsby-plugin-postcss": "^2.1.19",
19 | "gatsby-plugin-react-helmet": "^3.1.21",
20 | "gatsby-plugin-sharp": "^2.4.3",
21 | "gatsby-plugin-stripe": "^1.2.3",
22 | "gatsby-plugin-web-font-loader": "^1.0.4",
23 | "gatsby-source-filesystem": "^2.1.46",
24 | "gatsby-transformer-sharp": "^2.3.13",
25 | "graphql": "^14.6.0",
26 | "graphql-tag": "^2.10.3",
27 | "path": "^0.12.7",
28 | "prop-types": "^15.7.2",
29 | "react": "^16.12.0",
30 | "react-country-region-selector": "^1.4.7",
31 | "react-dom": "^16.12.0",
32 | "react-helmet": "^5.2.1",
33 | "react-icons": "^3.9.0",
34 | "react-toastify": "^5.5.0",
35 | "reactjs-popup": "^1.5.0",
36 | "stripe": "^8.67.0",
37 | "tailwindcss": "^1.1.4",
38 | "uuid": "^3.4.0"
39 | },
40 | "devDependencies": {
41 | "prettier": "^1.19.1"
42 | },
43 | "keywords": [
44 | "gatsby"
45 | ],
46 | "license": "MIT",
47 | "scripts": {
48 | "build": "gatsby build",
49 | "develop": "gatsby develop",
50 | "format": "prettier --write \"**/*.{js,jsx,json,md}\"",
51 | "start": "npm run develop",
52 | "serve": "gatsby serve",
53 | "clean": "gatsby clean",
54 | "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1"
55 | },
56 | "repository": {
57 | "type": "git",
58 | "url": "https://github.com/dabit3/gatsby-ecommerce-professional"
59 | },
60 | "bugs": {
61 | "url": "https://github.com/dabit3/gatsby-ecommerce-professional/issues"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/components/seo.js:
--------------------------------------------------------------------------------
1 | /**
2 | * SEO component that queries for data with
3 | * Gatsby's useStaticQuery React hook
4 | *
5 | * See: https://www.gatsbyjs.org/docs/use-static-query/
6 | */
7 |
8 | import React from "react"
9 | import PropTypes from "prop-types"
10 | import Helmet from "react-helmet"
11 | import { useStaticQuery, graphql } from "gatsby"
12 |
13 | function SEO({ description, lang, meta, title }) {
14 | const { site } = useStaticQuery(
15 | graphql`
16 | query {
17 | site {
18 | siteMetadata {
19 | title
20 | description
21 | author
22 | }
23 | }
24 | }
25 | `
26 | )
27 |
28 | const metaDescription = description || site.siteMetadata.description
29 |
30 | return (
31 |
72 | )
73 | }
74 |
75 | SEO.defaultProps = {
76 | lang: `en`,
77 | meta: [],
78 | description: ``,
79 | }
80 |
81 | SEO.propTypes = {
82 | description: PropTypes.string,
83 | lang: PropTypes.string,
84 | meta: PropTypes.arrayOf(PropTypes.object),
85 | title: PropTypes.string.isRequired,
86 | }
87 |
88 | export default SEO
89 |
--------------------------------------------------------------------------------
/src/pages/admin.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import SignUp from '../components/formComponents/SignUp'
3 | import ConfirmSignUp from '../components/formComponents/ConfirmSignUp'
4 | import SignIn from '../components/formComponents/SignIn'
5 | import Inventory from '../templates/Inventory'
6 |
7 | class Admin extends React.Component {
8 | state = { formState: 'signUp', isAdmin: false }
9 | toggleFormState = (formState) => {
10 | this.setState(() => ({ formState }))
11 | }
12 | async componentDidMount() {
13 | // check and update signed in state
14 | }
15 | signUp = async (form) => {
16 | const { username, email, password } = form
17 | // sign up
18 | this.setState({ formState: 'confirmSignUp' })
19 | }
20 | confirmSignUp = async (form) => {
21 | const { username, authcode } = form
22 | // confirm sign up
23 | this.setState({ formState: 'signIn' })
24 | }
25 | signIn = async (form) => {
26 | const { username, password } = form
27 | // signIn
28 | this.setState({ formState: 'signedIn', isAdmin: true })
29 | }
30 | signOut = async() => {
31 | // sign out
32 | this.setState({ formState: 'signUp' })
33 | }
34 |
35 | render() {
36 | const { formState, isAdmin } = this.state
37 | const renderForm = (formState, state) => {
38 | switch(formState) {
39 | case 'signUp':
40 | return
41 | case 'confirmSignUp':
42 | return
43 | case 'signIn':
44 | return
45 | case 'signedIn':
46 | return isAdmin ? : Not an admin
47 | default:
48 | return null
49 | }
50 | }
51 |
52 | return (
53 |
54 |
55 |
56 |
Admin Panel
57 |
58 | {
59 | renderForm(formState)
60 | }
61 |
62 |
63 | )
64 | }
65 | }
66 |
67 | export default Admin
--------------------------------------------------------------------------------
/src/components/formComponents/ConfirmSignUp.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | class ConfirmSignUp extends React.Component {
4 | state = {
5 | username: '', authcode: ''
6 | }
7 | onChange = (e) => {
8 | this.setState({ [e.target.name]: e.target.value})
9 | }
10 | render() {
11 | return (
12 |
13 |
Sign Up
14 |
15 |
16 |
42 |
43 | ©2020 JAMstack ECommerce. All rights reserved.
44 |
45 |
46 |
47 |
48 | )
49 | }
50 | }
51 |
52 | export default ConfirmSignUp
--------------------------------------------------------------------------------
/src/components/formComponents/SignIn.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { colors } from '../../theme'
3 |
4 | class SignIn extends React.Component {
5 | state = {
6 | username: '', password: ''
7 | }
8 | onChange = (e) => {
9 | this.setState({ [e.target.name]: e.target.value})
10 | }
11 | render() {
12 | return (
13 |
14 |
Sign Up
15 |
16 |
17 |
43 |
44 | ©2020 JAMstack ECommerce. All rights reserved.
45 |
46 |
47 |
48 |
49 | )
50 | }
51 | }
52 |
53 | export default SignIn
--------------------------------------------------------------------------------
/src/templates/ItemView.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import Popup from 'reactjs-popup'
3 |
4 | import { SiteContext, ContextProviderComponent } from '../context/mainContext'
5 | import CartLink from '../components/CartLink'
6 | import Button from '../components/Button'
7 | import Image from '../components/Image'
8 | import QuantityPicker from '../components/QuantityPicker'
9 |
10 | const ItemView = (props) => {
11 | const [numberOfitems, updateNumberOfItems] = useState(1)
12 | const item = props.pageContext.content
13 | const { price, image, name, description } = item
14 | const { context: { addToCart }} = props
15 |
16 | function addItemToCart (item) {
17 | item["quantity"] = numberOfitems
18 | addToCart(item)
19 | }
20 |
21 | function increment() {
22 | updateNumberOfItems(numberOfitems + 1)
23 | }
24 |
25 | function decrement() {
26 | if (numberOfitems === 1) return
27 | updateNumberOfItems(numberOfitems - 1)
28 | }
29 |
30 | return (
31 | <>
32 |
33 |
37 |
44 |
45 |
{name}
46 |
${price}
47 |
{description}
48 |
49 |
54 |
55 |
addItemToCart(item)}
59 | />
60 |
61 |
62 | >
63 | )
64 | }
65 |
66 |
67 | function ItemViewWithContext(props) {
68 | return (
69 |
70 |
71 | {
72 | context =>
73 | }
74 |
75 |
76 | )
77 | }
78 |
79 |
80 | export default ItemViewWithContext
81 |
--------------------------------------------------------------------------------
/api/index.js:
--------------------------------------------------------------------------------
1 | // https://stripe.com/docs/payments/without-card-authentication
2 | const stripe = require("stripe")(process.env.STRIPE_KEY)
3 |
4 | module.exports = async (req, res) => {
5 | if (req.method !== "POST") {
6 | res.status(400);
7 | res.json({
8 | error: { code: 'INVALID_METHOD', message: "Only POST method is allowed" }
9 | });
10 | return;
11 | }
12 |
13 | const order = req.body;
14 | console.log(order);
15 | // FIXME: the payload should be validated against a JSON schema
16 | // Otherwise, if clients send the wrong types or miss fields,
17 | // they will have a hard time understanding why this function
18 | // doesn't work correctly
19 |
20 | const calculateOrderAmount = items => {
21 | // Replace this constant with a calculation of the order's amount
22 | // You should always calculate the order total on the server to prevent
23 | // people from directly manipulating the amount on the client
24 | return 1400
25 | }
26 |
27 | try {
28 | const intent = await stripe.paymentIntents.create({
29 | amount: calculateOrderAmount(order.items),
30 | currency: "usd",
31 | payment_method: order.payment_method_id,
32 |
33 | // A PaymentIntent can be confirmed some time after creation,
34 | // but here we want to confirm (collect payment) immediately.
35 | confirm: true,
36 |
37 | // If the payment requires any follow-up actions from the
38 | // customer, like two-factor authentication, Stripe will error
39 | // and you will need to prompt them for a new payment method.
40 | error_on_requires_action: true,
41 | })
42 |
43 | if (intent.status === "succeeded") {
44 | // This creates a new Customer and attaches the PaymentMethod in one API call.
45 | const customer = await stripe.customers.create({
46 | payment_method: intent.payment_method,
47 | email: order.email,
48 | address: order.address,
49 | })
50 | // Handle post-payment fulfillment
51 | console.log(`Created Payment: ${intent.id} for Customer: ${customer.id}`)
52 |
53 | // Now ship those goodies
54 | // await inventoryAPI.ship(order)
55 |
56 | res.status(200);
57 | res.json({ ok: true });
58 | } else {
59 | const message = "Unexpected status " + intent.status;
60 |
61 | // Any other status would be unexpected, so error
62 | console.log({ error: message })
63 |
64 | res.status(500);
65 | res.json({ error: { code: 'PAYMENT_ERROR', message } });
66 | }
67 | } catch (e) {
68 | if (e.type === "StripeCardError") {
69 | // Display error to customer
70 | console.log({ error: e.message })
71 | } else {
72 | // Something else happened
73 | console.log({ error: e.type })
74 | }
75 |
76 | res.status(500);
77 | res.json({ error: { code: e.type, message: e.message } });
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/components/formComponents/SignUp.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | class SignUp extends React.Component {
4 | state = {
5 | username: '', email: '', password: ''
6 | }
7 | onChange = (e) => {
8 | this.setState({ [e.target.name]: e.target.value})
9 | }
10 | render() {
11 | return (
12 |
13 |
Sign Up
14 |
15 |
16 |
50 |
51 | ©2020 JAMstack ECommerce. All rights reserved.
52 |
53 |
54 |
55 |
56 | )
57 | }
58 | }
59 |
60 | export default SignUp
--------------------------------------------------------------------------------
/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | import SEO from "../components/seo"
4 | import { Center, Footer, Tag, Showcase, DisplaySmall, DisplayMedium } from '../components'
5 | import CartLink from '../components/CartLink'
6 | import { titleIfy, slugify } from '../../utils/helpers'
7 |
8 | import { graphql } from 'gatsby'
9 |
10 | const Home = ({ data: gqlData }) => {
11 | const { inventoryInfo, categoryInfo: { data }} = gqlData
12 | const categories = data.slice(0, 2)
13 | const inventory = inventoryInfo.data.slice(0, 4)
14 |
15 | return (
16 | <>
17 |
18 |
19 |
20 |
24 |
25 |
29 |
34 |
37 |
38 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
Trending Now
54 |
Find the perfect piece or accessory to finish off your favorite room in the house.
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | >
66 | )
67 | }
68 |
69 | export const pageQuery = graphql`
70 | query {
71 | navInfo {
72 | data
73 | }
74 | categoryInfo {
75 | data {
76 | name
77 | image
78 | itemCount
79 | }
80 | }
81 | inventoryInfo {
82 | data {
83 | image
84 | name
85 | categories
86 | description
87 | id
88 | }
89 | }
90 | }
91 | `
92 |
93 | export default Home
94 |
--------------------------------------------------------------------------------
/src/context/mainContext.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { StaticQuery, graphql } from 'gatsby'
3 | import { toast } from 'react-toastify';
4 |
5 | const mainQuery = graphql`
6 | query {
7 | navInfo {
8 | data
9 | }
10 | }
11 | `
12 |
13 | const STORAGE_KEY = 'GATSBY_ECOMMERCE_STARTER_'
14 |
15 | const initialState = {
16 | cart: [],
17 | numberOfItemsInCart: 0,
18 | total: 0
19 | }
20 |
21 | const SiteContext = React.createContext()
22 |
23 | function calculateTotal(cart) {
24 | const total = cart.reduce((acc, next) => {
25 | const quantity = next.quantity
26 | acc = acc + JSON.parse(next.price) * quantity
27 | return acc
28 | }, 0)
29 | return total
30 | }
31 |
32 | class ContextProviderComponent extends React.Component {
33 | componentDidMount() {
34 | if (typeof window !== 'undefined') {
35 | const storageState = window.localStorage.getItem(STORAGE_KEY)
36 | if (!storageState) {
37 | window.localStorage.setItem(STORAGE_KEY, JSON.stringify(initialState))
38 | }
39 | }
40 | }
41 |
42 | setItemQuantity = (item) => {
43 | const storageState = JSON.parse(window.localStorage.getItem(STORAGE_KEY))
44 | const { cart } = storageState
45 | const index = cart.findIndex(cartItem => cartItem.id === item.id)
46 | cart[index].quantity = item.quantity
47 | window.localStorage.setItem(STORAGE_KEY, JSON.stringify({
48 | cart, numberOfItemsInCart: cart.length, total: calculateTotal(cart)
49 | }))
50 | this.forceUpdate()
51 | }
52 |
53 | addToCart = item => {
54 | const storageState = JSON.parse(window.localStorage.getItem(STORAGE_KEY))
55 | const { cart } = storageState
56 | if (cart.length) {
57 | const index = cart.findIndex(cartItem => cartItem.id === item.id)
58 | if (index >= Number(0)) {
59 | cart[index].quantity = cart[index].quantity + item.quantity
60 | } else {
61 | cart.push(item)
62 | }
63 | } else {
64 | cart.push(item)
65 | }
66 |
67 | window.localStorage.setItem(STORAGE_KEY, JSON.stringify({
68 | cart, numberOfItemsInCart: cart.length, total: calculateTotal(cart)
69 | }))
70 | toast("Successfully added item to cart!", {
71 | position: toast.POSITION.TOP_LEFT
72 | })
73 | this.forceUpdate()
74 | }
75 |
76 | removeFromCart = (item) => {
77 | const storageState = JSON.parse(window.localStorage.getItem(STORAGE_KEY))
78 | let { cart } = storageState
79 | cart = cart.filter(c => c.id !== item.id)
80 |
81 | window.localStorage.setItem(STORAGE_KEY, JSON.stringify({
82 | cart, numberOfItemsInCart: cart.length, total: calculateTotal(cart)
83 | }))
84 | this.forceUpdate()
85 | }
86 |
87 | clearCart = () => {
88 | window.localStorage.setItem(STORAGE_KEY, JSON.stringify(initialState))
89 | this.forceUpdate()
90 | }
91 |
92 | render() {
93 | let state = initialState
94 | if (typeof window !== 'undefined') {
95 | const storageState = window.localStorage.getItem(STORAGE_KEY)
96 | if (storageState) {
97 | state = JSON.parse(storageState)
98 | }
99 | }
100 |
101 | return (
102 |
103 | { queryData => {
104 | return (
105 |
113 | {this.props.children}
114 |
115 | )
116 | }}
117 |
118 | )
119 | }
120 | }
121 |
122 | export {
123 | SiteContext,
124 | ContextProviderComponent
125 | }
--------------------------------------------------------------------------------
/src/layouts/baseLayout.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Layout component that queries for data
3 | * with Gatsby's useStaticQuery component
4 | *
5 | * See: https://www.gatsbyjs.org/docs/use-static-query/
6 | */
7 |
8 | import React from "react"
9 | import { Link } from "gatsby"
10 | import { SiteContext, ContextProviderComponent } from '../context/mainContext'
11 | import { titleIfy, slugify } from '../../utils/helpers'
12 | import 'react-toastify/dist/ReactToastify.css'
13 | import { toast } from 'react-toastify';
14 | import { colors } from '../theme'
15 |
16 | toast.configure( {
17 | progressStyle: {
18 | background: colors.primary,
19 | }
20 | })
21 |
22 | const logo = require('../images/logo.png');
23 |
24 | class Layout extends React.Component {
25 | render() {
26 | const { children } = this.props
27 |
28 | return (
29 |
30 |
31 | {
32 | context => {
33 | console.log('baselayout rerendering...')
34 | let { navItems: { navInfo: { data: links }}} = context
35 |
36 | links = links.map(link => ({
37 | name: titleIfy(link),
38 | link: slugify(link)
39 | }));
40 | links.unshift({
41 | name: 'Home',
42 | link: '/'
43 | })
44 |
45 | return (
46 |
47 |
48 |
49 |
54 |
55 |
56 |
57 |
58 | {
59 | links.map((l, i) => (
60 |
61 |
{l.name}
62 |
63 | ))
64 | }
65 |
66 | {/*
67 |
68 |
69 |
70 | {
71 | numberOfItemsInCart > Number(0) && (
72 |
73 |
74 |
75 | )
76 | }
77 |
*/}
78 |
79 |
80 |
81 |
82 | {children}
83 |
84 |
85 |
86 |
Copyright © 2020 JAMstack Ecommerce. All rights reserved.
87 |
88 |
89 |
Admins
90 |
91 |
92 |
93 |
94 |
95 | )
96 | }
97 | }
98 |
99 |
100 | )
101 | }
102 | }
103 |
104 | export default Layout
105 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 |
2 | // import theme from './src/theme'
3 |
4 |
5 |
6 | // const { colors } = theme
7 | // console.log('colors: ', colors)
8 |
9 | // const { primary, secondary } = colors
10 |
11 | module.exports = {
12 | theme: {
13 | extend: {
14 | screens: {
15 | 'mobile': '600px',
16 | 'c_large': '1200px',
17 | 'desktop': '1440px'
18 | },
19 | width: {
20 | '28': '7rem',
21 | 'c_large': '1200px',
22 | "38":"10rem",
23 | "48":"12rem",
24 | "52":"13rem",
25 | "56":"14rem",
26 | "60":"15rem",
27 | "64": "16rem",
28 | "68": "17rem",
29 | "72": "18rem",
30 | "80": "20rem",
31 | "88": "22rem",
32 | "96": "24rem",
33 | "104": "26rem",
34 | "112": "28rem",
35 | "120": "30rem",
36 | "124": "31rem",
37 | "128": "32rem",
38 | "132": "33rem",
39 | "136": "34rem",
40 | "140": "35rem",
41 | "144": "36rem",
42 | 'flex-half': "calc((100%/2) - 15px)",
43 | 'flex-fourth': "calc((100% / 4) - 20px)"
44 | },
45 | inset: {
46 | 'flexiblemargin': "calc((100vw - 1420px) / 2)",
47 | '100': '100px',
48 | '200': '200px',
49 | '250': '250px',
50 | '300': '300px',
51 | '400': '400px',
52 | '20': '20px',
53 | '30': '30px',
54 | '35': '35px',
55 | '40': '40px',
56 | '45': '45px',
57 | '45': '45px',
58 | '46': '46px',
59 | '47': '47px',
60 | '48': '48px',
61 | '49': '49px',
62 | '50': '50px',
63 | '55': '55px',
64 | '60': '60px'
65 | },
66 | height: {
67 | 'hero': '600px',
68 | "48":"12rem",
69 | "52":"13rem",
70 | "56":"14rem",
71 | "60":"15rem",
72 | "64": "16rem",
73 | "68":"17rem",
74 | "72": "18rem",
75 | "80": "20rem",
76 | "88": "22rem",
77 | "96": "24rem",
78 | "104": "26rem",
79 | "112": "28rem",
80 | "120": "30rem",
81 | "124": "31rem",
82 | "128": "32rem",
83 | "132": "33rem",
84 | "136": "34rem",
85 | "140": "35rem",
86 | "144": "36rem",
87 | "fw": "1440px"
88 | },
89 | spacing: {
90 | "72": "18rem",
91 | "80": "20rem",
92 | "88": "22rem",
93 | "96": "24rem",
94 | "104": "26rem",
95 | "112": "28rem",
96 | "120": "30rem",
97 | "124": "31rem",
98 | "128": "32rem",
99 | "132": "33rem",
100 | "136": "34rem",
101 | "140": "35rem",
102 | "144": "36rem",
103 | "fw": "1440px"
104 | },
105 | fontSize: {
106 | 'xxs': '.6rem',
107 | 'smaller': '.8rem'
108 | },
109 | padding: {
110 | ".5": ".125rem"
111 | },
112 | maxWidth: {
113 | "48":"12rem",
114 | "52":"13rem",
115 | "56":"14rem",
116 | "60":"15rem",
117 | "64": "16rem",
118 | "68":"17rem",
119 | "72": "18rem",
120 | "80": "20rem",
121 | "88": "22rem",
122 | "96": "24rem",
123 | "104": "26rem",
124 | "112": "28rem",
125 | "120": "30rem",
126 | "124": "31rem",
127 | "128": "32rem",
128 | "132": "33rem",
129 | "136": "34rem",
130 | "140": "35rem",
131 | "144": "36rem",
132 | "fw": "1440px",
133 | 'c_large': '1200px'
134 | },
135 | maxHeight: {
136 | "36":"9rem",
137 | "40":"10rem",
138 | "44":"11rem",
139 | "48":"12rem",
140 | "52":"13rem",
141 | "56":"14rem",
142 | "60":"15rem",
143 | "64": "16rem",
144 | "68":"17rem",
145 | "72": "18rem",
146 | "80": "20rem",
147 | "88": "22rem",
148 | "96": "24rem",
149 | "104": "26rem",
150 | "112": "28rem",
151 | "120": "30rem",
152 | "124": "31rem",
153 | "128": "32rem",
154 | "132": "33rem",
155 | "136": "34rem",
156 | "140": "35rem",
157 | "144": "36rem",
158 | "fw": "1440px"
159 | },
160 | fontFamily: {
161 | 'light': ['Eina Light']
162 | },
163 | zIndex: {
164 | '-2': '-2',
165 | '-4': '-4',
166 | '-6': '-6',
167 | '-12': '-12',
168 | },
169 | textColor: {
170 | 'primary': '#000000',
171 | 'secondary': '#00baa6',
172 | }
173 | },
174 | backgroundColor: theme => ({
175 | ...theme('colors'),
176 | 'primary': '#000000',
177 | 'secondary': '#00baa6',
178 | 'light': '#f5f5f5',
179 | 'light-200': '#f0f0f0',
180 | 'light-300': '#e8e8e8'
181 | })
182 | },
183 | variants: {},
184 | plugins: [],
185 | }
--------------------------------------------------------------------------------
/gatsby-node.esm.js:
--------------------------------------------------------------------------------
1 | import getInventory from './providers/inventoryProvider.js'
2 | import { slugify } from './utils/helpers'
3 |
4 | const ItemView = require.resolve('./src/templates/ItemView')
5 | const CategoryView = require.resolve('./src/templates/CategoryView')
6 |
7 | exports.createPages = async ({ graphql, actions }) => {
8 | const { createPage } = actions
9 | const inventory = await getInventory()
10 |
11 | createPage({
12 | path: 'all',
13 | component: CategoryView,
14 | context: {
15 | content: inventory,
16 | title: 'all',
17 | type: "categoryPage"
18 | },
19 | })
20 |
21 |
22 | const inventoryByCategory = inventory.reduce((acc, next) => {
23 | const categories = next.categories
24 | categories.forEach(c => {
25 | if (acc[c]) {
26 | acc[c].items.push(next)
27 | } else {
28 | acc[c] = {}
29 | acc[c].items = []
30 | acc[c].items.push(next)
31 | }
32 | })
33 | return acc
34 | }, {})
35 |
36 | const categories = Object.keys(inventoryByCategory)
37 |
38 | categories.map(async(category, index) => {
39 | const previous = index === categories.length - 1 ? null : categories[index + 1].node
40 | const next = index === 0 ? null : categories[index - 1]
41 | createPage({
42 | path: slugify(category),
43 | component: CategoryView,
44 | context: {
45 | content: inventoryByCategory[category],
46 | title: category,
47 | type: "categoryPage",
48 | previous,
49 | next,
50 | },
51 | })
52 | })
53 |
54 | inventory.map(async(item, index) => {
55 | const previous = index === inventory.length - 1 ? null : inventory[index + 1].node
56 | const next = index === 0 ? null : inventory[index - 1]
57 | createPage({
58 | path: slugify(item.name),
59 | component: ItemView,
60 | context: {
61 | content: item,
62 | title: item.name,
63 | type: "itemPage",
64 | previous,
65 | next,
66 | },
67 | })
68 | })
69 | }
70 |
71 | exports.sourceNodes = async ({ actions, createNodeId, createContentDigest }) => {
72 | const { createNode } = actions
73 | const inventory = await getInventory()
74 |
75 | /* create nav info for categories */
76 | const categoryNames = inventory.reduce((acc, next) => {
77 | next.categories.forEach(c => {
78 | if (!acc.includes(c)) acc.push(c)
79 | })
80 | return acc
81 | }, [])
82 |
83 | const navData = {
84 | key: 'nav-info',
85 | data: categoryNames
86 | }
87 |
88 | const navNodeContent = JSON.stringify(navData)
89 | const navNodeMeta = {
90 | id: createNodeId(`my-data-${navData.key}`),
91 | parent: null,
92 | children: [],
93 | internal: {
94 | type: `NavInfo`,
95 | mediaType: `json`,
96 | content: navNodeContent,
97 | contentDigest: createContentDigest(navData)
98 | }
99 | }
100 |
101 | const navNode = Object.assign({}, navData, navNodeMeta)
102 | createNode(navNode)
103 |
104 | /* create category info for home page */
105 | const inventoryByCategory = inventory.reduce((acc, next) => {
106 | const categories = next.categories
107 |
108 | categories.forEach(c => {
109 | const index = acc.findIndex(item => item.name === c)
110 | if (index !== -1) {
111 | const item = acc[index]
112 | item.itemCount = item.itemCount + 1
113 | acc[index] = item
114 | } else {
115 | const item = {
116 | name: c,
117 | image: next.image,
118 | itemCount: 1
119 | }
120 | acc.push(item)
121 | }
122 | })
123 | return acc
124 | }, [])
125 |
126 | const catData = {
127 | key: 'category-info',
128 | data: inventoryByCategory
129 | }
130 |
131 | const catNodeContent = JSON.stringify(catData)
132 | const catNodeMeta = {
133 | id: createNodeId(`my-data-${catData.key}`),
134 | parent: null,
135 | children: [],
136 | internal: {
137 | type: `CategoryInfo`,
138 | mediaType: `json`,
139 | content: catNodeContent,
140 | contentDigest: createContentDigest(catData)
141 | }
142 | }
143 |
144 | const catNode = Object.assign({}, catData, catNodeMeta)
145 | createNode(catNode)
146 |
147 | /* all inventory */
148 | const inventoryData = {
149 | key: 'all-inventory',
150 | data: inventory
151 | }
152 |
153 | const inventoryNodeContent = JSON.stringify(inventoryData)
154 | const inventoryNodeMeta = {
155 | id: createNodeId(`my-data-${inventoryData.key}`),
156 | parent: null,
157 | children: [],
158 | internal: {
159 | type: `InventoryInfo`,
160 | mediaType: `json`,
161 | content: inventoryNodeContent,
162 | contentDigest: createContentDigest(inventoryData)
163 | }
164 | }
165 |
166 | const inventoryNode = Object.assign({}, inventoryData, inventoryNodeMeta)
167 | createNode(inventoryNode)
168 | }
--------------------------------------------------------------------------------
/src/templates/ViewInventory.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import getInventory, { DENOMINATION } from '../../providers/inventoryProvider'
3 | import Image from '../components/Image'
4 | import { Link } from 'gatsby'
5 | import { slugify } from '../../utils/helpers'
6 | import { FaTimes } from 'react-icons/fa'
7 |
8 | class ViewInventory extends React.Component {
9 | state = {
10 | inventory: [],
11 | currentItem: {},
12 | editingIndex: []
13 | }
14 | componentDidMount() {
15 | this.fetchInventory()
16 | }
17 | fetchInventory = async() => {
18 | const inventory = await getInventory()
19 | this.setState({ inventory })
20 | }
21 | editItem = (item, index) => {
22 | const editingIndex = index
23 | this.setState({ editingIndex, currentItem: item })
24 | }
25 | saveItem = async index => {
26 | const inventory = [...this.state.inventory]
27 | inventory[index] = this.state.currentItem
28 | // update item in database
29 | this.setState({ editingIndex: null, inventory })
30 | }
31 | deleteItem = async index => {
32 | const inventory = [...this.state.inventory.slice(0, index), ...this.state.inventory.slice(index + 1)]
33 | this.setState({ inventory })
34 | }
35 | onChange = event => {
36 | const currentItem = {
37 | ...this.state.currentItem,
38 | [event.target.name]: event.target.value
39 | }
40 |
41 | this.setState({ currentItem })
42 | }
43 | render() {
44 | const { inventory, currentItem, editingIndex } = this.state
45 | console.log('currentItem: ', currentItem)
46 | return (
47 |
48 |
Inventory
49 | {
50 | inventory.map((item, index) => {
51 | const isEditing = editingIndex === index
52 | if (isEditing) {
53 | return (
54 |
55 |
56 |
57 |
58 |
59 |
this.onChange(e, index)}
61 | className="ml-8 shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
62 | value={currentItem.name}
63 | placeholder="Item name"
64 | name="name"
65 | />
66 |
83 |
this.saveItem(index)} className="m-0 ml-10 text-gray-900 text-s cursor-pointer">
84 |
Save
85 |
86 |
87 |
88 | )
89 | }
90 | return (
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | {item.name}
99 |
100 |
101 |
102 |
In stock: {item.currentInventory}
103 |
104 | {DENOMINATION + item.price}
105 |
106 |
107 |
108 |
this.deleteItem(index)} />
109 | this.editItem(item, index)} className="text-sm ml-10 m-0">Edit
110 |
111 |
112 |
113 | )
114 | })
115 | }
116 |
117 | )
118 | }
119 | }
120 |
121 | export default ViewInventory
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## JAMstack ECommerce Professional (Beta)
2 |
3 | JAMstack ECommerce Professional provides a way to quickly get up and running with a fully configurable JAMstack E Commerce site.
4 |
5 | Out of the box, the site uses completely static data coming from a provider at `providers/inventoryProvider.js`. You can update this provider to fetch data from any real API by changing the call in the `getInventory` function.
6 |
7 | 
8 |
9 | > This project is still in Beta.
10 |
11 | ### Getting started
12 |
13 | 1. Clone the project
14 |
15 | ```sh
16 | $ git clone https://github.com/jamstack-cms/jamstack-ecommerce.git
17 | ```
18 |
19 | 2. Install the dependencies:
20 |
21 | ```sh
22 | $ yarn
23 |
24 | # or
25 |
26 | $ npm install
27 | ```
28 |
29 | 3. Run the project
30 |
31 | ```sh
32 | $ gatsby develop
33 |
34 | # or to build
35 |
36 | $ gatsby build
37 | ```
38 |
39 | ## About the project
40 |
41 | ### Tailwind
42 |
43 | This project is styled using Tailwind. To learn more how this works, check out the Tailwind documentation [here](https://tailwindcss.com/docs).
44 |
45 | ### Components
46 |
47 | The main files, components, and images you may want to change / modify are:
48 |
49 | __Logo__ - src/images/logo.png
50 | __Buttons, Nav, Header__ - src/components
51 | __Form components__ - src/components/formComponents
52 | __Context (state)__ - src/context/mainContext.js
53 | __Pages (admin, cart, checkout, index)__ - src/pages
54 | __Templates (category view, single item view, inventory views)__ - src/templates
55 |
56 | ### How it works
57 |
58 | As it is set up, inventory is fetched from a local hard coded array of inventory items. This can easily be configured to instead be fetched from a remote source like Shopify or another CMS or data source by changing the inventory provider.
59 |
60 | #### Configuring inventory provider
61 |
62 | Update __providers/inventoryProvider.js__ with your own inventory provider.
63 |
64 | #### Download images at build time
65 |
66 | If you change the provider to fetch images from a remote source, you may choose to also download the images locally at build time to improve performance. Here is an example of some code that should work for this use case:
67 |
68 | ```javascript
69 | import fs from 'fs'
70 | import axios from 'axios'
71 | import path from 'path'
72 |
73 | function getImageKey(url) {
74 | const split = url.split('/')
75 | const key = split[split.length - 1]
76 | const keyItems = key.split('?')
77 | const imageKey = keyItems[0]
78 | return imageKey
79 | }
80 |
81 | function getPathName(url, pathName = 'downloads') {
82 | let reqPath = path.join(__dirname, '..')
83 | let key = getImageKey(url)
84 | key = key.replace(/%/g, "")
85 | const rawPath = `${reqPath}/public/${pathName}/${key}`
86 | return rawPath
87 | }
88 |
89 | async function downloadImage (url) {
90 | return new Promise(async (resolve, reject) => {
91 | const path = getPathName(url)
92 | const writer = fs.createWriteStream(path)
93 | const response = await axios({
94 | url,
95 | method: 'GET',
96 | responseType: 'stream'
97 | })
98 | response.data.pipe(writer)
99 | writer.on('finish', resolve)
100 | writer.on('error', reject)
101 | })
102 | }
103 |
104 | export default downloadImage
105 | ```
106 |
107 | You can use this function in __gatsby-node.esm.js__, map over the inventory data after fetching and replace the image paths with a reference to the location of the downloaded images, probably would look something like this:
108 |
109 | ```javascript
110 | await Promise.all(
111 | inventory.map(async (item, index) => {
112 | try {
113 | const relativeUrl = `../downloads/${item.image}`
114 | if (!fs.existsSync(`${__dirname}/public/downloads/${item.image}`)) {
115 | await downloadImage(image)
116 | }
117 | inventory[index].image = relativeUrl
118 | } catch (err) {
119 | console.log('error downloading image: ', err)
120 | }
121 | })
122 | )
123 | ```
124 |
125 | ### Updating with Auth / Admin panel
126 |
127 | 1. Update __src/pages/admin.js__ with sign up, sign, in, sign out, and confirm sign in methods.
128 |
129 | 2. Update __src/templates/ViewInventory.js__ with methods to interact with the actual inventory API.
130 |
131 | 3. Update __src/components/formComponents/AddInventory.js__ with methods to add item to actual inventory API.
132 |
133 | ### Roadmap for V1
134 |
135 | - Add ability to specify quantities in cart
136 | - Auto dropdown navigation for large number of categories
137 | - Ability to add more / more configurable metadata to item details
138 | - Themeing + dark mode
139 | - Better image support out of the box
140 | - Optional user account / profiles out of the box
141 | - Have an idea or a request? Submit [an issue](https://github.com/jamstack-cms/jamstack-ecommerce/issues) or [a pull request](https://github.com/jamstack-cms/jamstack-ecommerce/pulls)!
142 |
143 | ### Other considerations
144 |
145 | #### Server-side processing of payments
146 |
147 | Alongside Gatsby, we deploy a function called `/api`, which can be
148 | used to submit a payment.
149 |
150 | The original codebase doesn't [yet make the request](https://github.com/jamstack-cms/jamstack-ecommerce/blob/88a2d489af4204d11d8612b4fb3c24a14321c030/src/pages/checkout.js#L114) to process the payment,
151 | but it should be easy to add with `fetch`.
152 |
153 | Also, consider verifying totals by passing in an array of IDs into the function, calculating the total on the server, then comparing the totals to check and make sure they match.
154 |
--------------------------------------------------------------------------------
/src/components/formComponents/AddInventory.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const initialState = {
4 | name: '', brand: '', price: '', categories: [], image: '', description: '', currentInventory: ''
5 | }
6 |
7 | class AddInventory extends React.Component {
8 | state = initialState
9 | clearForm = () => {
10 | this.setState(() => (initialState))
11 | }
12 | onChange = (e) => {
13 | this.setState({ [e.target.name]: e.target.value})
14 | }
15 | onImageChange = async (e) => {
16 | const file = e.target.files[0];
17 | this.setState({ image: file })
18 | // const storageUrl = await Storage.put('example.png', file, {
19 | // contentType: 'image/png'
20 | // })
21 | // this.setState({ image: storageUrl })
22 | }
23 | addItem = async () => {
24 | const { name, brand, price, categories, image, description, currentInventory } = this.state
25 | if (!name || !brand || !price || !categories.length || !description || !currentInventory || !image) return
26 | // add to database
27 | this.clearForm()
28 | }
29 | render() {
30 | const {
31 | name, brand, price, categories, image, description, currentInventory
32 | } = this.state
33 | return (
34 |
35 |
Add Item
36 |
37 |
38 |
105 |
106 | ©2020 JAMstack ECommerce. All rights reserved.
107 |
108 |
109 |
110 |
111 | )
112 | }
113 | }
114 |
115 | export default AddInventory
--------------------------------------------------------------------------------
/src/pages/cart.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { SiteContext, ContextProviderComponent } from '../context/mainContext'
4 | import { DENOMINATION } from '../../providers/inventoryProvider'
5 | import { FaTimes, FaLongArrowAltRight } from 'react-icons/fa'
6 | import { Link } from 'gatsby'
7 | import CartLink from '../components/CartLink'
8 | import QuantityPicker from '../components/QuantityPicker'
9 | import { slugify } from '../../utils/helpers'
10 | import Image from '../components/Image'
11 |
12 | const Cart = ({ context }) => {
13 | const {
14 | numberOfItemsInCart, cart, removeFromCart, total, setItemQuantity
15 | } = context
16 | const cartEmpty = numberOfItemsInCart === Number(0)
17 |
18 | function increment(item) {
19 | item.quantity = item.quantity + 1
20 | setItemQuantity(item)
21 | }
22 |
23 | function decrement(item) {
24 | if (item.quantity === 1) return
25 | item.quantity = item.quantity - 1
26 | setItemQuantity(item)
27 | }
28 |
29 | return (
30 | <>
31 |
32 |
33 |
37 |
38 |
Your Cart
39 |
40 |
41 | {
42 | cartEmpty ? (
43 |
No items in cart.
44 | ) : (
45 |
46 |
47 | {
48 | cart.map((item) => {
49 | return (
50 |
51 |
52 | { /* Responsive - Desktop */}
53 |
54 |
55 |
56 |
57 |
58 |
61 | {item.name}
62 |
63 |
64 |
65 | increment(item)}
68 | decrement={() => decrement(item)}
69 | />
70 |
71 |
72 |
73 | {DENOMINATION + item.price}
74 |
75 |
76 |
removeFromCart(item)} className="
77 | m-0 ml-10 text-gray-900 text-s cursor-pointer
78 | ">
79 |
80 |
81 |
82 |
83 | { /* Responsive - Mobile */}
84 |
85 |
86 |
87 |
88 |
89 |
90 |
93 | {item.name}
94 |
95 |
96 |
97 | increment(item)}
101 | decrement={() => decrement(item)}
102 | />
103 |
104 |
105 |
106 | {DENOMINATION + item.price}
107 |
108 |
109 |
110 |
removeFromCart(item)} className="
111 | m-0 ml-10 text-gray-900 text-s cursor-pointer mr-2
112 | ">
113 |
114 |
115 |
116 |
117 | )
118 | })
119 | }
120 |
121 |
122 | )
123 | }
124 |
125 |
Total
126 |
{DENOMINATION + total}
127 |
128 | {!cartEmpty && (
129 |
130 |
131 |
Proceed to check out
132 |
133 |
134 |
135 | )}
136 |
137 |
138 | >
139 | )
140 | }
141 |
142 | function CartWithContext(props) {
143 | return (
144 |
145 |
146 | {
147 | context =>
148 | }
149 |
150 |
151 | )
152 | }
153 |
154 |
155 | export default CartWithContext
--------------------------------------------------------------------------------
/src/pages/checkout.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react"
2 |
3 | import { SiteContext, ContextProviderComponent } from "../context/mainContext"
4 | import { DENOMINATION } from "../../providers/inventoryProvider"
5 | import { FaLongArrowAltLeft } from "react-icons/fa"
6 | import { Link } from "gatsby"
7 | import Image from "../components/Image"
8 | import uuid from "uuid/v4"
9 |
10 | import {
11 | CardElement,
12 | Elements,
13 | useStripe,
14 | useElements,
15 | } from "@stripe/react-stripe-js"
16 | import { loadStripe } from "@stripe/stripe-js"
17 |
18 | // Make sure to call `loadStripe` outside of a component’s render to avoid
19 | // recreating the `Stripe` object on every render.
20 | const stripePromise = loadStripe("pk_test_DvXwcKnVaaZUpWJIbh9cjgZr00IjIAjZAA")
21 |
22 | function CheckoutWithContext(props) {
23 | return (
24 |
25 |
26 | {context => (
27 |
28 |
29 |
30 | )}
31 |
32 |
33 | )
34 | }
35 |
36 | const calculateShipping = () => {
37 | return 0
38 | }
39 |
40 | const Input = ({ onChange, value, name, placeholder }) => (
41 |
49 | )
50 |
51 | const Checkout = ({ context }) => {
52 | const [errorMessage, setErrorMessage] = useState(null)
53 | const [orderCompleted, setOrderCompleted] = useState(false)
54 | const [input, setInput] = useState({
55 | name: "",
56 | email: "",
57 | street: "",
58 | city: "",
59 | postal_code: "",
60 | state: "",
61 | })
62 |
63 | const stripe = useStripe()
64 | const elements = useElements()
65 |
66 | const onChange = e => {
67 | setErrorMessage(null)
68 | setInput({ ...input, [e.target.name]: e.target.value })
69 | }
70 |
71 | const handleSubmit = async event => {
72 | event.preventDefault()
73 | const { name, email, street, city, postal_code, state } = input
74 | const { total, clearCart } = context
75 |
76 | if (!stripe || !elements) {
77 | // Stripe.js has not loaded yet. Make sure to disable
78 | // form submission until Stripe.js has loaded.
79 | return
80 | }
81 |
82 | // Validate input
83 | if (!street || !city || !postal_code || !state) {
84 | setErrorMessage("Please fill in the form!")
85 | return
86 | }
87 |
88 | // Get a reference to a mounted CardElement. Elements knows how
89 | // to find your CardElement because there can only ever be one of
90 | // each type of element.
91 | const cardElement = elements.getElement(CardElement)
92 |
93 | // Use your card Element with other Stripe.js APIs
94 | const { error, paymentMethod } = await stripe.createPaymentMethod({
95 | type: "card",
96 | card: cardElement,
97 | billing_details: { name: name },
98 | })
99 |
100 | if (error) {
101 | setErrorMessage(error.message)
102 | return
103 | }
104 |
105 | const order = {
106 | email,
107 | amount: total,
108 | address: state, // should this be {street, city, postal_code, state} ?
109 | payment_method_id: paymentMethod.id,
110 | receipt_email: "customer@example.com",
111 | id: uuid(),
112 | }
113 | console.log("order: ", order)
114 | // TODO call API
115 | setOrderCompleted(true)
116 | clearCart()
117 | }
118 |
119 | const { numberOfItemsInCart, cart, total } = context
120 | const cartEmpty = numberOfItemsInCart === Number(0)
121 |
122 | if (orderCompleted) {
123 | return (
124 |
125 |
Thanks! Your order has been successfully processed.
126 |
127 | )
128 | }
129 |
130 | return (
131 |
132 |
138 |
139 |
Checkout
140 |
141 |
142 |
143 |
Edit Cart
144 |
145 |
146 |
147 |
148 | {cartEmpty ? (
149 |
No items in cart.
150 | ) : (
151 |
152 |
153 | {cart.map((item, index) => {
154 | return (
155 |
156 |
157 |
162 |
163 | {item.name}
164 |
165 |
166 |
167 | {DENOMINATION + item.price}
168 |
169 |
170 |
171 |
172 | )
173 | })}
174 |
175 |
176 |
229 |
230 |
231 |
Subtotal
232 |
233 | {DENOMINATION + total}
234 |
235 |
236 |
237 |
Shipping
238 |
239 | FREE SHIPPING
240 |
241 |
242 |
243 |
Total
244 |
245 | {DENOMINATION + (total + calculateShipping())}
246 |
247 |
248 |
255 | Confirm order
256 |
257 |
258 |
259 |
260 | )}
261 |
262 |
263 | )
264 | }
265 |
266 | export default CheckoutWithContext
267 |
--------------------------------------------------------------------------------
/providers/inventory.js:
--------------------------------------------------------------------------------
1 | import uuid from 'uuid/v4'
2 |
3 | let inventory = [
4 | { categories: ['new arrivals'], name: 'Timber Gray Sofa', price: '1000', image: '../images/products/couch1.png', description: 'Stay a while. The Timber charme chocolat sofa is set atop an oak trim and flaunts fluffy leather back and seat cushions. Over time, this brown leather sofa’s full-aniline upholstery will develop a worn-in vintage look. Snuggle up with your cutie (animal or human) and dive into a bowl of popcorn. This sofa is really hard to leave. Natural color variations, wrinkles and creases are part of the unique characteristics of this leather. It will develop a relaxed vintage look with regular use.', brand: 'Jason Bourne', currentInventory: 4 },
5 | { categories: ['sofas', 'living room'], name: 'Carmel Brown Sofa', price: '1000', image: '../images/products/couch5.png', description: 'Stay a while. The Timber charme chocolat sofa is set atop an oak trim and flaunts fluffy leather back and seat cushions. Over time, this brown leather sofa’s full-aniline upholstery will develop a worn-in vintage look. Snuggle up with your cutie (animal or human) and dive into a bowl of popcorn. This sofa is really hard to leave. Natural color variations, wrinkles and creases are part of the unique characteristics of this leather. It will develop a relaxed vintage look with regular use.' , brand: 'Jason Bourne' , currentInventory: 2 },
6 | { categories: ['new arrivals', 'sofas'], name: 'Mod Leather Sofa', price: '800', image: '../images/products/couch6.png', description: 'Easy to love. The Sven in birch ivory looks cozy and refined, like a sweater that a fancy lady wears on a coastal vacation. This ivory loveseat has a tufted bench seat, loose back pillows and bolsters, solid walnut legs, and is ready to make your apartment the adult oasis you dream of. Nestle it with plants, an ottoman, an accent chair, or 8 dogs. Your call.', brand: 'Jason Bourne', currentInventory: 8 },
7 | { categories: ['new arrivals', 'sofas'], name: 'Thetis Gray Love Seat', price: '900', image: '../images/products/couch7.png', description: 'You know your dad’s incredible vintage bomber jacket? The Nirvana dakota tan leather sofa is that jacket, but in couch form. With super-plush down-filled cushions, a corner-blocked wooden frame, and a leather patina that only gets better with age, the Nirvana will have you looking cool and feeling peaceful every time you take a seat. Looks pretty great with a sheepskin throw, if we may say so. With use, this leather will become softer and more wrinkled and the cushions will take on a lived-in look, like your favorite leather jacket.' , brand: 'Jason Bourne', currentInventory: 10},
8 | { categories: ['on sale', 'sofas'], name: 'Sven Tan Matte', price: '1200', image: '../images/products/couch8.png', description: 'You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.', brand: 'Jason Bourne' , currentInventory: 7 },
9 | { categories: ['on sale', 'sofas'], name: 'Otis Malt Sofa', price: '500', image: '../images/products/couch9.png', description: 'You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.' , brand: 'Jason Bourne', currentInventory: 13},
10 | { categories: ['on sale', 'sofas'], name: 'Ceni Brown 3 Seater', price: '650', image: '../images/products/couch10.png', description: 'You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.' , brand: 'Jason Bourne', currentInventory: 9},
11 | { categories: ['sofas', 'living room'], name: 'Jameson Jack Lounger', price: '1230', image: '../images/products/couch11.png', description: 'You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.', brand: 'Jason Bourne', currentInventory: 24 },
12 |
13 | { categories: ['sofas'], name: 'Galaxy Blue Sofa', price: '800', image: '../images/products/couch2.png', description: 'Easy to love. The Sven in birch ivory looks cozy and refined, like a sweater that a fancy lady wears on a coastal vacation. This ivory loveseat has a tufted bench seat, loose back pillows and bolsters, solid walnut legs, and is ready to make your apartment the adult oasis you dream of. Nestle it with plants, an ottoman, an accent chair, or 8 dogs. Your call.', brand: 'Jason Bourne', currentInventory: 43 },
14 | { categories: ['new arrivals', 'sofas'], name: 'Markus Green Love Seat', price: '900', image: '../images/products/couch3.png', description: 'You know your dad’s incredible vintage bomber jacket? The Nirvana dakota tan leather sofa is that jacket, but in couch form. With super-plush down-filled cushions, a corner-blocked wooden frame, and a leather patina that only gets better with age, the Nirvana will have you looking cool and feeling peaceful every time you take a seat. Looks pretty great with a sheepskin throw, if we may say so. With use, this leather will become softer and more wrinkled and the cushions will take on a lived-in look, like your favorite leather jacket.', brand: 'Jason Bourne' , currentInventory: 2},
15 | { categories: ['on sale', 'sofas'], name: 'Dabit Matte Black', price: '1200', image: '../images/products/couch4.png', description: 'You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.', currentInventory: 14 },
16 |
17 | { categories: ['on sale', 'chairs'], name: 'Embrace Blue', price: '300', image: '../images/products/chair1.png', description: 'You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.' , brand: 'Jason Bourne', currentInventory: 12 },
18 | { categories: ['on sale', 'chairs'], name: 'Nord Lounger', price: '825', image: '../images/products/chair2.png', description: 'You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.' , brand: 'Jason Bourne', currentInventory: 13},
19 | { categories: ['on sale', 'chairs'], name: 'Ceni Matte Oranve', price: '720', image: '../images/products/chair3.png', description: 'You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.' , brand: 'Jason Bourne', currentInventory: 33},
20 | { categories: ['on sale', 'chairs'], name: 'Abisko Green Recliner', price: '2000', image: '../images/products/chair4.png', description: 'You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.', brand: 'Jason Bourne', currentInventory: 23 },
21 | { categories: ['on sale', 'chairs'], name: 'Denim on Denim Single', price: '1100', image: '../images/products/chair5.png', description: 'You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.' , brand: 'Jason Bourne', currentInventory: 13},
22 | { categories: ['on sale', 'chairs'], name: 'Levo Tan Lounge Chair', price: '600', image: '../images/products/chair6.png', description: 'You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.', brand: 'Jason Bourne', currentInventory: 15 },
23 |
24 | { categories: ['on sale', 'chairs'], name: 'Anime Tint Recliner', price: '775', image: '../images/products/chair7.png', description: 'You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.', brand: 'Jason Bourne', currentInventory: 44 },
25 | { categories: ['on sale', 'chairs'], name: 'Josh Jones Red Chair', price: '1200', image: '../images/products/chair8.png', description: 'You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.', brand: 'Jason Bourne', currentInventory: 17 },
26 | { categories: ['on sale', 'chairs'], name: 'Black Sand Lounge', price: '1600', image: '../images/products/chair9.png', description: 'You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.', brand: 'Jason Bourne', currentInventory: 28 },
27 | { categories: ['on sale', 'chairs'], name: 'Mint Beige Workchair', price: '550', image: '../images/products/chair10.png', description: 'You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.', brand: 'Jason Bourne', currentInventory: 31 }, // {
28 | ]
29 |
30 | inventory.map(i => {
31 | i.id = uuid()
32 | return i
33 | })
34 |
35 | export default inventory
--------------------------------------------------------------------------------
/src/layouts/layout.css:
--------------------------------------------------------------------------------
1 | html {
2 | font-family: sans-serif;
3 | -ms-text-size-adjust: 100%;
4 | -webkit-text-size-adjust: 100%;
5 | }
6 | body {
7 | margin: 0;
8 | -webkit-font-smoothing: antialiased;
9 | -moz-osx-font-smoothing: grayscale;
10 | }
11 | article,
12 | aside,
13 | details,
14 | figcaption,
15 | figure,
16 | footer,
17 | header,
18 | main,
19 | menu,
20 | nav,
21 | section,
22 | summary {
23 | display: block;
24 | }
25 | audio,
26 | canvas,
27 | progress,
28 | video {
29 | display: inline-block;
30 | }
31 | audio:not([controls]) {
32 | display: none;
33 | height: 0;
34 | }
35 | progress {
36 | vertical-align: baseline;
37 | }
38 | [hidden],
39 | template {
40 | display: none;
41 | }
42 | a {
43 | background-color: transparent;
44 | -webkit-text-decoration-skip: objects;
45 | }
46 | a:active,
47 | a:hover {
48 | outline-width: 0;
49 | }
50 | abbr[title] {
51 | border-bottom: none;
52 | text-decoration: underline;
53 | text-decoration: underline dotted;
54 | }
55 | b,
56 | strong {
57 | font-weight: inherit;
58 | font-weight: bolder;
59 | }
60 | dfn {
61 | font-style: italic;
62 | }
63 | h1 {
64 | font-size: 2em;
65 | margin: 0.67em 0;
66 | }
67 | mark {
68 | background-color: #ff0;
69 | color: #000;
70 | }
71 | small {
72 | font-size: 80%;
73 | }
74 | sub,
75 | sup {
76 | font-size: 75%;
77 | line-height: 0;
78 | position: relative;
79 | vertical-align: baseline;
80 | }
81 | sub {
82 | bottom: -0.25em;
83 | }
84 | sup {
85 | top: -0.5em;
86 | }
87 | img {
88 | border-style: none;
89 | }
90 | svg:not(:root) {
91 | overflow: hidden;
92 | }
93 | code,
94 | kbd,
95 | pre,
96 | samp {
97 | font-family: monospace, monospace;
98 | font-size: 1em;
99 | }
100 | figure {
101 | margin: 1em 40px;
102 | }
103 | hr {
104 | box-sizing: content-box;
105 | height: 0;
106 | overflow: visible;
107 | }
108 | button,
109 | input,
110 | optgroup,
111 | select,
112 | textarea {
113 | font: inherit;
114 | margin: 0;
115 | }
116 | optgroup {
117 | font-weight: 700;
118 | }
119 | button,
120 | input {
121 | overflow: visible;
122 | }
123 | button,
124 | select {
125 | text-transform: none;
126 | }
127 | [type="reset"],
128 | [type="submit"],
129 | button,
130 | html [type="button"] {
131 | -webkit-appearance: button;
132 | }
133 | [type="button"]::-moz-focus-inner,
134 | [type="reset"]::-moz-focus-inner,
135 | [type="submit"]::-moz-focus-inner,
136 | button::-moz-focus-inner {
137 | border-style: none;
138 | padding: 0;
139 | }
140 | [type="button"]:-moz-focusring,
141 | [type="reset"]:-moz-focusring,
142 | [type="submit"]:-moz-focusring,
143 | button:-moz-focusring {
144 | outline: 1px dotted ButtonText;
145 | }
146 | fieldset {
147 | border: 1px solid silver;
148 | margin: 0 2px;
149 | padding: 0.35em 0.625em 0.75em;
150 | }
151 | legend {
152 | box-sizing: border-box;
153 | color: inherit;
154 | display: table;
155 | max-width: 100%;
156 | padding: 0;
157 | white-space: normal;
158 | }
159 | textarea {
160 | overflow: auto;
161 | }
162 | [type="checkbox"],
163 | [type="radio"] {
164 | box-sizing: border-box;
165 | padding: 0;
166 | }
167 | [type="number"]::-webkit-inner-spin-button,
168 | [type="number"]::-webkit-outer-spin-button {
169 | height: auto;
170 | }
171 | [type="search"] {
172 | -webkit-appearance: textfield;
173 | outline-offset: -2px;
174 | }
175 | [type="search"]::-webkit-search-cancel-button,
176 | [type="search"]::-webkit-search-decoration {
177 | -webkit-appearance: none;
178 | }
179 | ::-webkit-input-placeholder {
180 | color: inherit;
181 | opacity: 0.54;
182 | }
183 | ::-webkit-file-upload-button {
184 | -webkit-appearance: button;
185 | font: inherit;
186 | }
187 | html {
188 | font: 112.5%/1.45em georgia, serif;
189 | box-sizing: border-box;
190 | overflow-y: scroll;
191 | }
192 | * {
193 | box-sizing: inherit;
194 | font-family: Eina -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";;
195 | }
196 | *:before {
197 | box-sizing: inherit;
198 | }
199 | *:after {
200 | box-sizing: inherit;
201 | }
202 | body {
203 | color: hsla(0, 0%, 0%, 0.8);
204 | font-family: georgia, serif;
205 | font-weight: normal;
206 | word-wrap: break-word;
207 | font-kerning: normal;
208 | -moz-font-feature-settings: "kern", "liga", "clig", "calt";
209 | -ms-font-feature-settings: "kern", "liga", "clig", "calt";
210 | -webkit-font-feature-settings: "kern", "liga", "clig", "calt";
211 | font-feature-settings: "kern", "liga", "clig", "calt";
212 | }
213 | img {
214 | max-width: 100%;
215 | margin-left: 0;
216 | margin-right: 0;
217 | margin-top: 0;
218 | padding-bottom: 0;
219 | padding-left: 0;
220 | padding-right: 0;
221 | padding-top: 0;
222 | margin-bottom: 1.45rem;
223 | }
224 | h1 {
225 | margin-left: 0;
226 | margin-right: 0;
227 | margin-top: 0;
228 | padding-bottom: 0;
229 | padding-left: 0;
230 | padding-right: 0;
231 | padding-top: 0;
232 | margin-bottom: 1.45rem;
233 | color: inherit;
234 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
235 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
236 | font-weight: bold;
237 | text-rendering: optimizeLegibility;
238 | font-size: 2.25rem;
239 | line-height: 1.1;
240 | }
241 | h2 {
242 | margin-left: 0;
243 | margin-right: 0;
244 | margin-top: 0;
245 | padding-bottom: 0;
246 | padding-left: 0;
247 | padding-right: 0;
248 | padding-top: 0;
249 | margin-bottom: 1.45rem;
250 | color: inherit;
251 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
252 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
253 | font-weight: bold;
254 | text-rendering: optimizeLegibility;
255 | font-size: 1.62671rem;
256 | line-height: 1.1;
257 | }
258 | h3 {
259 | margin-left: 0;
260 | margin-right: 0;
261 | margin-top: 0;
262 | padding-bottom: 0;
263 | padding-left: 0;
264 | padding-right: 0;
265 | padding-top: 0;
266 | margin-bottom: 1.45rem;
267 | color: inherit;
268 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
269 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
270 | font-weight: bold;
271 | text-rendering: optimizeLegibility;
272 | font-size: 1.38316rem;
273 | line-height: 1.1;
274 | }
275 | h4 {
276 | margin-left: 0;
277 | margin-right: 0;
278 | margin-top: 0;
279 | padding-bottom: 0;
280 | padding-left: 0;
281 | padding-right: 0;
282 | padding-top: 0;
283 | margin-bottom: 1.45rem;
284 | color: inherit;
285 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
286 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
287 | font-weight: bold;
288 | text-rendering: optimizeLegibility;
289 | font-size: 1rem;
290 | line-height: 1.1;
291 | }
292 | h5 {
293 | margin-left: 0;
294 | margin-right: 0;
295 | margin-top: 0;
296 | padding-bottom: 0;
297 | padding-left: 0;
298 | padding-right: 0;
299 | padding-top: 0;
300 | margin-bottom: 1.45rem;
301 | color: inherit;
302 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
303 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
304 | font-weight: bold;
305 | text-rendering: optimizeLegibility;
306 | font-size: 0.85028rem;
307 | line-height: 1.1;
308 | }
309 | h6 {
310 | margin-left: 0;
311 | margin-right: 0;
312 | margin-top: 0;
313 | padding-bottom: 0;
314 | padding-left: 0;
315 | padding-right: 0;
316 | padding-top: 0;
317 | margin-bottom: 1.45rem;
318 | color: inherit;
319 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
320 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
321 | font-weight: bold;
322 | text-rendering: optimizeLegibility;
323 | font-size: 0.78405rem;
324 | line-height: 1.1;
325 | }
326 | hgroup {
327 | margin-left: 0;
328 | margin-right: 0;
329 | margin-top: 0;
330 | padding-bottom: 0;
331 | padding-left: 0;
332 | padding-right: 0;
333 | padding-top: 0;
334 | margin-bottom: 1.45rem;
335 | }
336 | ul {
337 | margin-left: 1.45rem;
338 | margin-right: 0;
339 | margin-top: 0;
340 | padding-bottom: 0;
341 | padding-left: 0;
342 | padding-right: 0;
343 | padding-top: 0;
344 | margin-bottom: 1.45rem;
345 | list-style-position: outside;
346 | list-style-image: none;
347 | }
348 | ol {
349 | margin-left: 1.45rem;
350 | margin-right: 0;
351 | margin-top: 0;
352 | padding-bottom: 0;
353 | padding-left: 0;
354 | padding-right: 0;
355 | padding-top: 0;
356 | margin-bottom: 1.45rem;
357 | list-style-position: outside;
358 | list-style-image: none;
359 | }
360 | dl {
361 | margin-left: 0;
362 | margin-right: 0;
363 | margin-top: 0;
364 | padding-bottom: 0;
365 | padding-left: 0;
366 | padding-right: 0;
367 | padding-top: 0;
368 | margin-bottom: 1.45rem;
369 | }
370 | dd {
371 | margin-left: 0;
372 | margin-right: 0;
373 | margin-top: 0;
374 | padding-bottom: 0;
375 | padding-left: 0;
376 | padding-right: 0;
377 | padding-top: 0;
378 | margin-bottom: 1.45rem;
379 | }
380 | p {
381 | margin-left: 0;
382 | margin-right: 0;
383 | margin-top: 0;
384 | padding-bottom: 0;
385 | padding-left: 0;
386 | padding-right: 0;
387 | padding-top: 0;
388 | margin-bottom: 1.45rem;
389 | }
390 | figure {
391 | margin-left: 0;
392 | margin-right: 0;
393 | margin-top: 0;
394 | padding-bottom: 0;
395 | padding-left: 0;
396 | padding-right: 0;
397 | padding-top: 0;
398 | margin-bottom: 1.45rem;
399 | }
400 | pre {
401 | margin-left: 0;
402 | margin-right: 0;
403 | margin-top: 0;
404 | margin-bottom: 1.45rem;
405 | font-size: 0.85rem;
406 | line-height: 1.42;
407 | background: hsla(0, 0%, 0%, 0.04);
408 | border-radius: 3px;
409 | overflow: auto;
410 | word-wrap: normal;
411 | padding: 1.45rem;
412 | }
413 | table {
414 | margin-left: 0;
415 | margin-right: 0;
416 | margin-top: 0;
417 | padding-bottom: 0;
418 | padding-left: 0;
419 | padding-right: 0;
420 | padding-top: 0;
421 | margin-bottom: 1.45rem;
422 | font-size: 1rem;
423 | line-height: 1.45rem;
424 | border-collapse: collapse;
425 | width: 100%;
426 | }
427 | fieldset {
428 | margin-left: 0;
429 | margin-right: 0;
430 | margin-top: 0;
431 | padding-bottom: 0;
432 | padding-left: 0;
433 | padding-right: 0;
434 | padding-top: 0;
435 | margin-bottom: 1.45rem;
436 | }
437 | blockquote {
438 | margin-left: 1.45rem;
439 | margin-right: 1.45rem;
440 | margin-top: 0;
441 | padding-bottom: 0;
442 | padding-left: 0;
443 | padding-right: 0;
444 | padding-top: 0;
445 | margin-bottom: 1.45rem;
446 | }
447 | form {
448 | margin-left: 0;
449 | margin-right: 0;
450 | margin-top: 0;
451 | padding-bottom: 0;
452 | padding-left: 0;
453 | padding-right: 0;
454 | padding-top: 0;
455 | margin-bottom: 1.45rem;
456 | }
457 | noscript {
458 | margin-left: 0;
459 | margin-right: 0;
460 | margin-top: 0;
461 | padding-bottom: 0;
462 | padding-left: 0;
463 | padding-right: 0;
464 | padding-top: 0;
465 | margin-bottom: 1.45rem;
466 | }
467 | iframe {
468 | margin-left: 0;
469 | margin-right: 0;
470 | margin-top: 0;
471 | padding-bottom: 0;
472 | padding-left: 0;
473 | padding-right: 0;
474 | padding-top: 0;
475 | margin-bottom: 1.45rem;
476 | }
477 | hr {
478 | margin-left: 0;
479 | margin-right: 0;
480 | margin-top: 0;
481 | padding-bottom: 0;
482 | padding-left: 0;
483 | padding-right: 0;
484 | padding-top: 0;
485 | margin-bottom: calc(1.45rem - 1px);
486 | background: hsla(0, 0%, 0%, 0.2);
487 | border: none;
488 | height: 1px;
489 | }
490 | address {
491 | margin-left: 0;
492 | margin-right: 0;
493 | margin-top: 0;
494 | padding-bottom: 0;
495 | padding-left: 0;
496 | padding-right: 0;
497 | padding-top: 0;
498 | margin-bottom: 1.45rem;
499 | }
500 | b {
501 | font-weight: bold;
502 | }
503 | strong {
504 | font-weight: bold;
505 | }
506 | dt {
507 | font-weight: bold;
508 | }
509 | th {
510 | font-weight: bold;
511 | }
512 | li {
513 | margin-bottom: calc(1.45rem / 2);
514 | }
515 | ol li {
516 | padding-left: 0;
517 | }
518 | ul li {
519 | padding-left: 0;
520 | }
521 | li > ol {
522 | margin-left: 1.45rem;
523 | margin-bottom: calc(1.45rem / 2);
524 | margin-top: calc(1.45rem / 2);
525 | }
526 | li > ul {
527 | margin-left: 1.45rem;
528 | margin-bottom: calc(1.45rem / 2);
529 | margin-top: calc(1.45rem / 2);
530 | }
531 | blockquote *:last-child {
532 | margin-bottom: 0;
533 | }
534 | li *:last-child {
535 | margin-bottom: 0;
536 | }
537 | p *:last-child {
538 | margin-bottom: 0;
539 | }
540 | li > p {
541 | margin-bottom: calc(1.45rem / 2);
542 | }
543 | code {
544 | font-size: 0.85rem;
545 | line-height: 1.45rem;
546 | }
547 | kbd {
548 | font-size: 0.85rem;
549 | line-height: 1.45rem;
550 | }
551 | samp {
552 | font-size: 0.85rem;
553 | line-height: 1.45rem;
554 | }
555 | abbr {
556 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5);
557 | cursor: help;
558 | }
559 | acronym {
560 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5);
561 | cursor: help;
562 | }
563 | abbr[title] {
564 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5);
565 | cursor: help;
566 | text-decoration: none;
567 | }
568 | thead {
569 | text-align: left;
570 | }
571 | td,
572 | th {
573 | text-align: left;
574 | border-bottom: 1px solid hsla(0, 0%, 0%, 0.12);
575 | font-feature-settings: "tnum";
576 | -moz-font-feature-settings: "tnum";
577 | -ms-font-feature-settings: "tnum";
578 | -webkit-font-feature-settings: "tnum";
579 | padding-left: 0.96667rem;
580 | padding-right: 0.96667rem;
581 | padding-top: 0.725rem;
582 | padding-bottom: calc(0.725rem - 1px);
583 | }
584 | th:first-child,
585 | td:first-child {
586 | padding-left: 0;
587 | }
588 | th:last-child,
589 | td:last-child {
590 | padding-right: 0;
591 | }
592 | tt,
593 | code {
594 | background-color: hsla(0, 0%, 0%, 0.04);
595 | border-radius: 3px;
596 | font-family: "SFMono-Regular", Consolas, "Roboto Mono", "Droid Sans Mono",
597 | "Liberation Mono", Menlo, Courier, monospace;
598 | padding: 0;
599 | padding-top: 0.2em;
600 | padding-bottom: 0.2em;
601 | }
602 | pre code {
603 | background: none;
604 | line-height: 1.42;
605 | }
606 | code:before,
607 | code:after,
608 | tt:before,
609 | tt:after {
610 | letter-spacing: -0.2em;
611 | content: " ";
612 | }
613 | pre code:before,
614 | pre code:after,
615 | pre tt:before,
616 | pre tt:after {
617 | content: "";
618 | }
619 | @media only screen and (max-width: 480px) {
620 | html {
621 | font-size: 100%;
622 | }
623 | }
624 |
--------------------------------------------------------------------------------