├── .github └── FUNDING.yml ├── netlify.toml ├── gatsby-browser.js ├── .prettierignore ├── renovate.json ├── content └── products │ ├── images │ ├── hoodie-navy.png │ ├── snapback-hat.png │ ├── full-logo-tee-navy.png │ ├── hoodie-dark-heather.png │ ├── emblem-logo-tee-berry.png │ ├── emblem-logo-tee-navy.png │ ├── full-logo-tee-maroon.png │ ├── full-logo-tee-purple.png │ ├── emblem-logo-tee-charcoal.png │ └── full-logo-tee-charcoal.png │ ├── snapback-hat.yaml │ ├── hoodie.yaml │ ├── emblem-logo-tee.yaml │ └── full-logo-tee.yaml ├── .prettierrc ├── src ├── context │ └── CartContext.js ├── pages │ ├── thankyou.js │ ├── index.js │ └── cart.js ├── components │ ├── ProductGrid.js │ ├── CartProvider.js │ ├── Product.js │ ├── Layout.js │ ├── AddToCart.js │ ├── RemoveFromCart.js │ ├── CartItem.js │ ├── CartSummary.js │ ├── CartItemList.js │ └── UpdateQuantity.js ├── hooks │ ├── useCartId.js │ └── useLocalStorage.js └── templates │ └── ProductPage.js ├── gatsby-config.js ├── gatsby-ssr.js ├── package.json ├── gatsby-node.js ├── .gitignore └── functions └── create-checkout-session.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: notrab 2 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | functions = "functions" -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | export { wrapRootElement } from "./gatsby-ssr" 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | package.json 3 | package-lock.json 4 | public 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /content/products/images/hoodie-navy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartQL/gatsby-cartql-starter/HEAD/content/products/images/hoodie-navy.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": false, 4 | "singleQuote": false, 5 | "tabWidth": 2, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /content/products/images/snapback-hat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartQL/gatsby-cartql-starter/HEAD/content/products/images/snapback-hat.png -------------------------------------------------------------------------------- /content/products/images/full-logo-tee-navy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartQL/gatsby-cartql-starter/HEAD/content/products/images/full-logo-tee-navy.png -------------------------------------------------------------------------------- /content/products/images/hoodie-dark-heather.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartQL/gatsby-cartql-starter/HEAD/content/products/images/hoodie-dark-heather.png -------------------------------------------------------------------------------- /src/context/CartContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react" 2 | 3 | export const CartContext = createContext() 4 | 5 | export default CartContext 6 | -------------------------------------------------------------------------------- /content/products/images/emblem-logo-tee-berry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartQL/gatsby-cartql-starter/HEAD/content/products/images/emblem-logo-tee-berry.png -------------------------------------------------------------------------------- /content/products/images/emblem-logo-tee-navy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartQL/gatsby-cartql-starter/HEAD/content/products/images/emblem-logo-tee-navy.png -------------------------------------------------------------------------------- /content/products/images/full-logo-tee-maroon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartQL/gatsby-cartql-starter/HEAD/content/products/images/full-logo-tee-maroon.png -------------------------------------------------------------------------------- /content/products/images/full-logo-tee-purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartQL/gatsby-cartql-starter/HEAD/content/products/images/full-logo-tee-purple.png -------------------------------------------------------------------------------- /content/products/images/emblem-logo-tee-charcoal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartQL/gatsby-cartql-starter/HEAD/content/products/images/emblem-logo-tee-charcoal.png -------------------------------------------------------------------------------- /content/products/images/full-logo-tee-charcoal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartQL/gatsby-cartql-starter/HEAD/content/products/images/full-logo-tee-charcoal.png -------------------------------------------------------------------------------- /src/pages/thankyou.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | const ThankyouPage = () => { 4 | return ( 5 | 6 |

Thank you

7 | 8 |

Thanks for your order.

9 |
10 | ) 11 | } 12 | 13 | export default ThankyouPage 14 | -------------------------------------------------------------------------------- /src/components/ProductGrid.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import Product from "./Product" 4 | 5 | const ProductGrid = ({ products }) => { 6 | if (!products) return

No products available

7 | 8 | return
{products.map(Product)}
9 | } 10 | 11 | export default ProductGrid 12 | -------------------------------------------------------------------------------- /content/products/snapback-hat.yaml: -------------------------------------------------------------------------------- 1 | name: Snapback 2 | slug: snapback 3 | description: This is a one for the fans! 4 | image: ./images/snapback-hat.png 5 | variants: 6 | - id: snapback 7 | name: One size fits all 8 | description: This is a one for the fans! 9 | image: ./images/snapback-hat.png 10 | price: 2000 11 | -------------------------------------------------------------------------------- /src/hooks/useCartId.js: -------------------------------------------------------------------------------- 1 | import { useContext } from "react" 2 | 3 | import CartContext from "../context/CartContext" 4 | 5 | const useCartId = () => { 6 | const cartId = useContext(CartContext) 7 | 8 | if (!cartId) { 9 | throw new Error("useCartId must be used within a CartProvider") 10 | } 11 | 12 | return cartId 13 | } 14 | 15 | export default useCartId 16 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | 3 | module.exports = { 4 | plugins: [ 5 | `gatsby-plugin-react-helmet`, 6 | `gatsby-transformer-sharp`, 7 | `gatsby-plugin-sharp`, 8 | `gatsby-transformer-yaml`, 9 | { 10 | resolve: `gatsby-source-filesystem`, 11 | options: { 12 | name: `products`, 13 | path: path.resolve(__dirname, "./content/products"), 14 | }, 15 | }, 16 | ], 17 | } 18 | -------------------------------------------------------------------------------- /src/components/CartProvider.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react" 2 | 3 | import useLocalStorage from "../hooks/useLocalStorage" 4 | import CartContext from "../context/CartContext" 5 | 6 | const CartProvider = ({ id, ...props }) => { 7 | const [cartId, saveCartId] = useLocalStorage("cartql-cart-id", id) 8 | 9 | useEffect(() => { 10 | saveCartId(cartId) 11 | }, [cartId, saveCartId]) 12 | 13 | return 14 | } 15 | 16 | export default CartProvider 17 | -------------------------------------------------------------------------------- /src/components/Product.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Link } from "gatsby" 3 | import Img from "gatsby-image" 4 | 5 | const Product = ({ id, slug, image, name }) => ( 6 |
7 | 8 | {image && ( 9 | {name} 15 | )} 16 | 17 |

{name}

18 | 19 |
20 | ) 21 | 22 | export default Product 23 | -------------------------------------------------------------------------------- /src/components/Layout.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Link } from "gatsby" 3 | 4 | import CartSummary from "./CartSummary" 5 | import useCartId from "../hooks/useCartId" 6 | 7 | const Layout = ({ children }) => { 8 | const cartId = useCartId() 9 | 10 | return ( 11 | 12 | 20 | 21 |
{children}
22 |
23 | ) 24 | } 25 | 26 | export default Layout 27 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { graphql } from "gatsby" 3 | 4 | import ProductGrid from "../components/ProductGrid" 5 | 6 | const HomePage = ({ 7 | data: { 8 | products: { nodes: products }, 9 | }, 10 | }) => 11 | 12 | export const pageQuery = graphql` 13 | query allProductsQuery { 14 | products: allProductsYaml { 15 | nodes { 16 | id 17 | name 18 | slug 19 | image { 20 | childImageSharp { 21 | fluid(maxWidth: 560) { 22 | ...GatsbyImageSharpFluid 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | ` 30 | 31 | export default HomePage 32 | -------------------------------------------------------------------------------- /gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { 3 | ApolloProvider, 4 | ApolloClient, 5 | HttpLink, 6 | InMemoryCache, 7 | } from "@apollo/client" 8 | import fetch from "isomorphic-unfetch" 9 | import cuid from "cuid" 10 | 11 | import CartProvider from "./src/components/CartProvider" 12 | import Layout from "./src/components/Layout" 13 | 14 | const client = new ApolloClient({ 15 | cache: new InMemoryCache(), 16 | link: new HttpLink({ 17 | fetch, 18 | uri: process.env.GATSBY_GRAPHQL_ENDPOINT, 19 | }), 20 | }) 21 | 22 | export const wrapRootElement = ({ element }) => ( 23 | 24 | 25 | {element} 26 | 27 | 28 | ) 29 | -------------------------------------------------------------------------------- /src/hooks/useLocalStorage.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | 3 | const useLocalStorage = (key, initialValue) => { 4 | const [storedValue, setStoredValue] = useState(() => { 5 | try { 6 | const item = 7 | typeof window !== "undefined" && window.localStorage.getItem(key) 8 | 9 | return item ? item : initialValue 10 | } catch (error) { 11 | return initialValue 12 | } 13 | }) 14 | 15 | const setValue = value => { 16 | try { 17 | const valueToStore = 18 | value instanceof Function ? value(storedValue) : value 19 | 20 | setStoredValue(valueToStore) 21 | 22 | window.localStorage.setItem(key, valueToStore) 23 | } catch (error) { 24 | console.log(error) 25 | } 26 | } 27 | 28 | return [storedValue, setValue] 29 | } 30 | 31 | export default useLocalStorage 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "@apollo/client": "^3.2.9", 5 | "@stripe/stripe-js": "^1.11.0", 6 | "cuid": "2.1.8", 7 | "gatsby": "2.27.3", 8 | "gatsby-image": "2.6.0", 9 | "gatsby-plugin-manifest": "2.7.0", 10 | "gatsby-plugin-react-helmet": "3.5.0", 11 | "gatsby-plugin-sharp": "2.9.0", 12 | "gatsby-source-filesystem": "2.6.1", 13 | "gatsby-transformer-sharp": "2.7.0", 14 | "gatsby-transformer-yaml": "2.6.0", 15 | "graphql-request": "3.3.0", 16 | "isomorphic-unfetch": "^3.1.0", 17 | "react": "17.0.1", 18 | "react-dom": "17.0.1", 19 | "react-helmet": "6.1.0", 20 | "stripe": "8.126.0" 21 | }, 22 | "license": "MIT", 23 | "scripts": { 24 | "build": "gatsby build", 25 | "develop": "gatsby develop", 26 | "start": "npm run develop", 27 | "serve": "gatsby serve", 28 | "clean": "gatsby clean" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/AddToCart.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { gql, useMutation } from "@apollo/client" 3 | 4 | const ADD_ITEM_MUTATION = gql` 5 | mutation addToCart($input: AddToCartInput!) { 6 | addItem(input: $input) { 7 | id 8 | isEmpty 9 | totalUniqueItems 10 | subTotal { 11 | formatted 12 | } 13 | items { 14 | id 15 | name 16 | description 17 | images 18 | quantity 19 | unitTotal { 20 | formatted 21 | } 22 | lineTotal { 23 | formatted 24 | } 25 | } 26 | } 27 | } 28 | ` 29 | 30 | const AddToCart = (input) => { 31 | const [addItem, { loading }] = useMutation(ADD_ITEM_MUTATION, { 32 | variables: { 33 | input, 34 | }, 35 | }) 36 | 37 | return ( 38 | 41 | ) 42 | } 43 | 44 | export default AddToCart 45 | -------------------------------------------------------------------------------- /src/components/RemoveFromCart.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { gql, useMutation } from "@apollo/client" 3 | 4 | const REMOVE_ITEM_MUTATION = gql` 5 | mutation removeFromCart($input: RemoveCartItemInput!) { 6 | removeItem(input: $input) { 7 | id 8 | isEmpty 9 | totalUniqueItems 10 | subTotal { 11 | formatted 12 | } 13 | items { 14 | id 15 | name 16 | description 17 | images 18 | quantity 19 | unitTotal { 20 | formatted 21 | } 22 | lineTotal { 23 | formatted 24 | } 25 | } 26 | } 27 | } 28 | ` 29 | 30 | const RemoveFromCart = (input) => { 31 | const [removeItem, { loading }] = useMutation(REMOVE_ITEM_MUTATION, { 32 | variables: { 33 | input, 34 | }, 35 | }) 36 | 37 | return ( 38 | 41 | ) 42 | } 43 | 44 | export default RemoveFromCart 45 | -------------------------------------------------------------------------------- /src/components/CartItem.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Img from "gatsby-image" 3 | 4 | import UpdateQuantity from "./UpdateQuantity" 5 | import RemoveFromCart from "./RemoveFromCart" 6 | 7 | const CartItem = ({ 8 | cartId, 9 | id, 10 | name, 11 | description, 12 | images = [], 13 | quantity, 14 | unitTotal, 15 | lineTotal, 16 | }) => { 17 | const [image] = images 18 | 19 | return ( 20 |
21 | {image && ( 22 | 28 | )} 29 | 30 |

{name}

31 |

32 | {description} 33 |

34 |

35 | x{" "} 36 | {unitTotal.formatted}: {lineTotal.formatted} 37 |

38 | 39 | 40 |
41 | ) 42 | } 43 | 44 | export default CartItem 45 | -------------------------------------------------------------------------------- /src/components/CartSummary.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Link } from "gatsby" 3 | import { gql, useQuery } from "@apollo/client" 4 | 5 | const GET_CART_QUERY = gql` 6 | query getCart($id: ID!) { 7 | cart(id: $id) { 8 | id 9 | isEmpty 10 | totalUniqueItems 11 | subTotal { 12 | formatted 13 | } 14 | items { 15 | id 16 | name 17 | description 18 | images 19 | quantity 20 | unitTotal { 21 | formatted 22 | } 23 | lineTotal { 24 | formatted 25 | } 26 | } 27 | } 28 | } 29 | ` 30 | 31 | const CartSummary = ({ cartId: id }) => { 32 | const { loading, error, data } = useQuery(GET_CART_QUERY, { 33 | variables: { 34 | id, 35 | }, 36 | }) 37 | 38 | if (loading) return Loading 39 | if (error) return Umm. Oops. 40 | 41 | return ( 42 | 43 | Cart {data.cart.totalUniqueItems} ({data.cart.subTotal.formatted}) 44 | 45 | ) 46 | } 47 | 48 | export default CartSummary 49 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | 3 | const PRODUCTS_QUERY = ` 4 | query { 5 | allProductsYaml { 6 | nodes { 7 | slug 8 | } 9 | } 10 | } 11 | ` 12 | 13 | exports.createResolvers = ({ createResolvers }) => { 14 | const resolvers = { 15 | ProductsYamlVariants: { 16 | formattedPrice: { 17 | type: `String`, 18 | resolve: ({ price }) => 19 | price 20 | ? new Intl.NumberFormat("en-US", { 21 | style: "currency", 22 | currency: "usd", 23 | }).format(price / 100) 24 | : null, 25 | }, 26 | }, 27 | } 28 | 29 | createResolvers(resolvers) 30 | } 31 | 32 | exports.createPages = async ({ graphql, actions }) => { 33 | const { createPage } = actions 34 | 35 | const { 36 | data: { 37 | allProductsYaml: { nodes: products }, 38 | }, 39 | } = await graphql(PRODUCTS_QUERY) 40 | 41 | products.forEach(({ slug }) => 42 | createPage({ 43 | component: path.resolve("./src/templates/ProductPage.js"), 44 | context: { slug }, 45 | path: `/products/${slug}`, 46 | }) 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/pages/cart.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { loadStripe } from "@stripe/stripe-js" 3 | 4 | import useCartId from "../hooks/useCartId" 5 | import CartItemList from "../components/CartItemList" 6 | 7 | const stripePromise = loadStripe(process.env.GATSBY_STRIPE_PUBLISHABLE_KEY) 8 | 9 | const CartPage = () => { 10 | const cartId = useCartId() 11 | 12 | const goToCheckout = async (e) => { 13 | e.preventDefault() 14 | 15 | const stripe = await stripePromise 16 | 17 | const { id: sessionId } = await fetch( 18 | "/.netlify/functions/create-checkout-session", 19 | { 20 | method: "POST", 21 | headers: { 22 | "Content-Type": "application/json", 23 | }, 24 | body: JSON.stringify({ 25 | cartId, 26 | }), 27 | } 28 | ).then((res) => res.json()) 29 | 30 | await stripe.redirectToCheckout({ 31 | sessionId, 32 | }) 33 | } 34 | 35 | return ( 36 | 37 |

Cart

38 | 39 | 40 | 41 | 44 |
45 | ) 46 | } 47 | 48 | export default CartPage 49 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/components/CartItemList.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { gql, useQuery } from "@apollo/client" 3 | 4 | import useCartId from "../hooks/useCartId" 5 | import CartItem from "./CartItem" 6 | 7 | const GET_CART_QUERY = gql` 8 | query getCart($id: ID!) { 9 | cart(id: $id) { 10 | id 11 | isEmpty 12 | totalUniqueItems 13 | subTotal { 14 | formatted 15 | } 16 | items { 17 | id 18 | name 19 | description 20 | images 21 | quantity 22 | unitTotal { 23 | formatted 24 | } 25 | lineTotal { 26 | formatted 27 | } 28 | } 29 | } 30 | } 31 | ` 32 | 33 | const CartItemList = ({ cartId: id }) => { 34 | const cartId = useCartId() 35 | const { loading, error, data } = useQuery(GET_CART_QUERY, { 36 | variables: { 37 | id, 38 | }, 39 | }) 40 | 41 | if (loading) return Loading cart 42 | if (error) return Umm. Oops. 43 | 44 | if (data.cart.isEmpty) return

Your cart is empty

45 | 46 | return ( 47 |
48 | {data.cart.items.map((item) => ( 49 | 50 | ))} 51 | 52 |
53 | Sub total: {data.cart.subTotal.formatted} 54 |
55 |
56 | ) 57 | } 58 | 59 | export default CartItemList 60 | -------------------------------------------------------------------------------- /src/components/UpdateQuantity.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { gql, useMutation } from "@apollo/client" 3 | 4 | const UPDATE_ITEM_MUTATION = gql` 5 | mutation updateQuantity($input: UpdateCartItemInput!) { 6 | updateItem(input: $input) { 7 | id 8 | isEmpty 9 | totalUniqueItems 10 | subTotal { 11 | formatted 12 | } 13 | items { 14 | id 15 | name 16 | description 17 | images 18 | quantity 19 | unitTotal { 20 | formatted 21 | } 22 | lineTotal { 23 | formatted 24 | } 25 | } 26 | } 27 | } 28 | ` 29 | 30 | const options = new Array(10).fill(0).map((v, k) => k + 1) 31 | 32 | const UpdateQuantity = ({ initialValue, ...props }) => { 33 | const [updateItem, { loading }] = useMutation(UPDATE_ITEM_MUTATION) 34 | 35 | const handleChange = ({ target: { value: quantity } }) => { 36 | updateItem({ 37 | variables: { input: { ...props, quantity: parseInt(quantity) } }, 38 | }) 39 | } 40 | 41 | return ( 42 | 54 | ) 55 | } 56 | 57 | export default UpdateQuantity 58 | -------------------------------------------------------------------------------- /content/products/hoodie.yaml: -------------------------------------------------------------------------------- 1 | name: Hoodie 2 | slug: hoodie 3 | description: Represent CartQL with this emblem logo hoodie! 4 | image: ./images/hoodie-dark-heather.png 5 | variants: 6 | - id: dark-heather-xs 7 | name: Dark Heather / XS 8 | image: ./images/hoodie-dark-heather.png 9 | price: 4200 10 | - id: dark-heather-s 11 | name: Dark Heather / S 12 | image: ./images/hoodie-dark-heather.png 13 | price: 4200 14 | - id: dark-heather-m 15 | name: Dark Heather / M 16 | image: ./images/hoodie-dark-heather.png 17 | price: 4200 18 | - id: dark-heather-l 19 | name: Dark Heather / L 20 | image: ./images/hoodie-dark-heather.png 21 | price: 4200 22 | - id: dark-heather-xl 23 | name: Dark Heather / XL 24 | image: ./images/hoodie-dark-heather.png 25 | price: 4200 26 | - id: dark-heather-xxl 27 | name: Dark Heather / XXL 28 | image: ./images/hoodie-dark-heather.png 29 | price: 4200 30 | - id: navy-xs 31 | name: Navy / XS 32 | image: ./images/hoodie-navy.png 33 | price: 4500 34 | - id: navy-s 35 | name: Navy / S 36 | image: ./images/hoodie-navy.png 37 | price: 4500 38 | - id: navy-m 39 | name: Navy / M 40 | image: ./images/hoodie-navy.png 41 | price: 4500 42 | - id: navy-l 43 | name: Navy / L 44 | image: ./images/hoodie-navy.png 45 | price: 4500 46 | - id: navy-xl 47 | name: Navy / XL 48 | image: ./images/hoodie-navy.png 49 | price: 4500 50 | - id: navy-xxl 51 | name: Navy / XXL 52 | image: ./images/hoodie-navy.png 53 | price: 4500 54 | -------------------------------------------------------------------------------- /functions/create-checkout-session.js: -------------------------------------------------------------------------------- 1 | const Stripe = require("stripe") 2 | const { request, gql } = require("graphql-request") 3 | 4 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY) 5 | 6 | const query = gql` 7 | query getCart($cartId: ID!) { 8 | cart(id: $cartId) { 9 | id 10 | isEmpty 11 | items { 12 | id 13 | name 14 | description 15 | unitTotal { 16 | amount 17 | currency { 18 | code 19 | } 20 | } 21 | quantity 22 | } 23 | } 24 | } 25 | ` 26 | 27 | exports.handler = async function (event) { 28 | const { cartId } = JSON.parse(event.body) 29 | 30 | const { 31 | cart: { isEmpty, items }, 32 | } = await request(process.env.GATSBY_GRAPHQL_ENDPOINT, query, { 33 | cartId, 34 | }) 35 | 36 | if (isEmpty) { 37 | return { 38 | statusCode: 400, 39 | body: JSON.stringify({ message: "The cart is empty." }), 40 | } 41 | } 42 | 43 | try { 44 | const session = await stripe.checkout.sessions.create({ 45 | mode: "payment", 46 | payment_method_types: ["card"], 47 | success_url: `${process.env.URL}/thankyou`, 48 | cancel_url: `${process.env.URL}/cart`, 49 | line_items: items.map( 50 | ({ 51 | name, 52 | description, 53 | unitTotal: { 54 | amount: unit_amount, 55 | currency: { code: currency }, 56 | }, 57 | quantity, 58 | }) => ({ 59 | ...(description && { description }), 60 | price_data: { 61 | currency, 62 | unit_amount, 63 | product_data: { 64 | name, 65 | ...(description && { description }), 66 | }, 67 | }, 68 | quantity, 69 | }) 70 | ), 71 | }) 72 | 73 | return { 74 | statusCode: 201, 75 | body: JSON.stringify(session), 76 | } 77 | } catch ({ message }) { 78 | return { 79 | statusCode: 401, 80 | body: JSON.stringify({ message }), 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /content/products/emblem-logo-tee.yaml: -------------------------------------------------------------------------------- 1 | name: Emblem Logo Tee 2 | slug: emblem-logo-tee 3 | description: Represent CartQL with this emblem only logo tee! 4 | image: ./images/emblem-logo-tee-charcoal.png 5 | variants: 6 | - id: charcoal-xs 7 | name: Charcoal / XS 8 | image: ./images/emblem-logo-tee-charcoal.png 9 | price: 4500 10 | - id: charcoal-s 11 | name: Charcoal / S 12 | image: ./images/emblem-logo-tee-charcoal.png 13 | price: 4500 14 | - id: charcoal-m 15 | name: Charcoal / M 16 | image: ./images/emblem-logo-tee-charcoal.png 17 | price: 4500 18 | - id: charcoal-l 19 | name: Charcoal / L 20 | image: ./images/emblem-logo-tee-charcoal.png 21 | price: 4500 22 | - id: charcoal-xl 23 | name: Charcoal / XL 24 | image: ./images/emblem-logo-tee-charcoal.png 25 | price: 4500 26 | - id: charcoal-xxl 27 | name: Charcoal / XXL 28 | image: ./images/emblem-logo-tee-charcoal.png 29 | price: 4500 30 | - id: navy-xs 31 | name: Navy / XS 32 | image: ./images/emblem-logo-tee-navy.png 33 | price: 4500 34 | - id: navy-s 35 | name: Navy / S 36 | image: ./images/emblem-logo-tee-navy.png 37 | price: 4500 38 | - id: navy-m 39 | name: Navy / M 40 | image: ./images/emblem-logo-tee-navy.png 41 | price: 4500 42 | - id: navy-l 43 | name: Navy / L 44 | image: ./images/emblem-logo-tee-navy.png 45 | price: 4500 46 | - id: navy-xl 47 | name: Navy / XL 48 | image: ./images/emblem-logo-tee-navy.png 49 | price: 4500 50 | - id: navy-xxl 51 | name: Navy / XXL 52 | image: ./images/emblem-logo-tee-navy.png 53 | price: 4500 54 | - id: berry-xs 55 | name: Berry / XS 56 | image: ./images/emblem-logo-tee-berry.png 57 | price: 4500 58 | - id: berry-s 59 | name: Berry / S 60 | image: ./images/emblem-logo-tee-berry.png 61 | price: 4500 62 | - id: berry-m 63 | name: Berry / M 64 | image: ./images/emblem-logo-tee-berry.png 65 | price: 4500 66 | - id: berry-l 67 | name: Berry / L 68 | image: ./images/emblem-logo-tee-berry.png 69 | price: 4500 70 | - id: berry-xl 71 | name: Berry / XL 72 | image: ./images/emblem-logo-tee-berry.png 73 | price: 4500 74 | - id: berry-xxl 75 | name: Berry / XXL 76 | image: ./images/emblem-logo-tee-berry.png 77 | price: 4500 78 | -------------------------------------------------------------------------------- /src/templates/ProductPage.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | import { graphql } from "gatsby" 3 | import Img from "gatsby-image" 4 | 5 | import useCartId from "../hooks/useCartId" 6 | import AddToCart from "../components/AddToCart" 7 | 8 | const ProductPage = ({ data: { product } }) => { 9 | const cartId = useCartId() 10 | const { name, description, variants } = product 11 | const [firstVariant] = variants 12 | const [activeVariantId, setActiveVariantId] = useState(firstVariant.id) 13 | const hasVariants = variants.length > 1 14 | const activeVariant = variants.find((v) => v.id === activeVariantId) 15 | 16 | return ( 17 | 18 |

{name}

19 | 20 |

{description}

21 | 22 |

{activeVariant.formattedPrice}

23 | 24 | {activeVariant.image && ( 25 | {activeVariant.name} 31 | )} 32 | 33 | {hasVariants && ( 34 | 35 |

Style

36 | 37 | 47 |
48 | )} 49 | 50 | 58 |
59 | ) 60 | } 61 | 62 | export const pageQuery = graphql` 63 | query ProductPageQuery($slug: String!) { 64 | product: productsYaml(slug: { eq: $slug }) { 65 | id 66 | name 67 | slug 68 | description 69 | variants { 70 | id 71 | name 72 | description 73 | price 74 | formattedPrice 75 | image { 76 | childImageSharp { 77 | fluid(maxWidth: 560) { 78 | ...GatsbyImageSharpFluid 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | ` 86 | 87 | export default ProductPage 88 | -------------------------------------------------------------------------------- /content/products/full-logo-tee.yaml: -------------------------------------------------------------------------------- 1 | name: Full Logo Tee 2 | slug: full-logo-tee 3 | description: Represent CartQL with this full logo tee! 4 | image: ./images/full-logo-tee-purple.png 5 | variants: 6 | - id: purple-xs 7 | name: Purple / XS 8 | image: ./images/full-logo-tee-purple.png 9 | price: 4200 10 | - id: purple-s 11 | name: Purple / S 12 | image: ./images/full-logo-tee-purple.png 13 | price: 4200 14 | - id: purple-m 15 | name: Purple / M 16 | image: ./images/full-logo-tee-purple.png 17 | price: 4200 18 | - id: purple-l 19 | name: Purple / L 20 | image: ./images/full-logo-tee-purple.png 21 | price: 4200 22 | - id: purple-xl 23 | name: Purple / XL 24 | image: ./images/full-logo-tee-purple.png 25 | price: 4200 26 | - id: purple-xxl 27 | name: Purple / XXL 28 | image: ./images/full-logo-tee-purple.png 29 | price: 4200 30 | - id: maroon-xs 31 | name: Maroon / XS 32 | image: ./images/full-logo-tee-maroon.png 33 | price: 4500 34 | - id: maroon-s 35 | name: Maroon / S 36 | image: ./images/full-logo-tee-maroon.png 37 | price: 4500 38 | - id: maroon-m 39 | name: Maroon / M 40 | image: ./images/full-logo-tee-maroon.png 41 | price: 4500 42 | - id: maroon-l 43 | name: Maroon / L 44 | image: ./images/full-logo-tee-maroon.png 45 | price: 4500 46 | - id: maroon-xl 47 | name: Maroon / XL 48 | image: ./images/full-logo-tee-maroon.png 49 | price: 4500 50 | - id: maroon-xxl 51 | name: Maroon / XXL 52 | image: ./images/full-logo-tee-maroon.png 53 | price: 4500 54 | - id: charcoal-xs 55 | name: Charcoal / XS 56 | image: ./images/full-logo-tee-charcoal.png 57 | price: 4500 58 | - id: charcoal-s 59 | name: Charcoal / S 60 | image: ./images/full-logo-tee-charcoal.png 61 | price: 4500 62 | - id: charcoal-m 63 | name: Charcoal / M 64 | image: ./images/full-logo-tee-charcoal.png 65 | price: 4500 66 | - id: charcoal-l 67 | name: Charcoal / L 68 | image: ./images/full-logo-tee-charcoal.png 69 | price: 4500 70 | - id: charcoal-xl 71 | name: Charcoal / XL 72 | image: ./images/full-logo-tee-charcoal.png 73 | price: 4500 74 | - id: charcoal-xxl 75 | name: Charcoal / XXL 76 | image: ./images/full-logo-tee-charcoal.png 77 | price: 4500 78 | - id: navy-xs 79 | name: Navy / XS 80 | image: ./images/full-logo-tee-navy.png 81 | price: 4500 82 | - id: navy-s 83 | name: Navy / S 84 | image: ./images/full-logo-tee-navy.png 85 | price: 4500 86 | - id: navy-m 87 | name: Navy / M 88 | image: ./images/full-logo-tee-navy.png 89 | price: 4500 90 | - id: navy-l 91 | name: Navy / L 92 | image: ./images/full-logo-tee-navy.png 93 | price: 4500 94 | - id: navy-xl 95 | name: Navy / XL 96 | image: ./images/full-logo-tee-navy.png 97 | price: 4500 98 | - id: navy-xxl 99 | name: Navy / XXL 100 | image: ./images/full-logo-tee-navy.png 101 | price: 4500 102 | --------------------------------------------------------------------------------