├── .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 |
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 |
13 | -
14 | Home
15 |
16 | -
17 |
18 |
19 |
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 |
![{name}]()
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 |
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 |
--------------------------------------------------------------------------------