├── .gitignore ├── src ├── site │ ├── images │ │ └── testimonial-bg.jpeg │ ├── index.njk │ ├── product-pages.njk │ ├── meats.njk │ ├── boards.njk │ ├── cheeses.njk │ ├── _includes │ │ ├── product_summary.njk │ │ ├── product_detail.njk │ │ └── layouts │ │ │ └── base.njk │ └── _data │ │ └── products.js ├── scss │ ├── _mixins.scss │ ├── _variables.scss │ ├── main.blue.scss │ └── main.scss └── js │ └── shopping-ui.js ├── netlify.toml ├── package.json ├── netlify └── functions │ ├── utils │ ├── postToShopify.js │ ├── addItemToCart.js │ ├── removeItemFromCart.js │ └── createCartWithItem.js │ ├── remove-from-cart.js │ ├── add-to-cart.js │ ├── get-cart.js │ └── cart-view.js ├── .eleventy.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .env 4 | .netlify 5 | -------------------------------------------------------------------------------- /src/site/images/testimonial-bg.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philhawksworth/shopify-11ty/HEAD/src/site/images/testimonial-bg.jpeg -------------------------------------------------------------------------------- /src/site/index.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Home 3 | layout: layouts/base.njk 4 | --- 5 | 6 | 11 | -------------------------------------------------------------------------------- /src/site/product-pages.njk: -------------------------------------------------------------------------------- 1 | --- 2 | pagination: 3 | data: products 4 | alias: item 5 | size: 1 6 | permalink: "/product/{{ item.node.handle | slug }}/index.html" 7 | layout: layouts/base.njk 8 | --- 9 | {% include "product_detail.njk" %} 10 | -------------------------------------------------------------------------------- /src/site/meats.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Meats 3 | layout: layouts/base.njk 4 | --- 5 | 6 | 13 | -------------------------------------------------------------------------------- /src/site/boards.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Boards 3 | layout: layouts/base.njk 4 | --- 5 | 6 | 13 | -------------------------------------------------------------------------------- /src/site/cheeses.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Cheeses 3 | layout: layouts/base.njk 4 | --- 5 | 6 | 13 | -------------------------------------------------------------------------------- /src/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin bp($point) { 2 | 3 | @if $point == narrow { 4 | @media #{$bp-narrow} { @content; } 5 | } 6 | @else if $point == mid { 7 | @media #{$bp-mid} { @content; } 8 | } 9 | @else if $point == wide { 10 | @media #{$bp-wide} { @content; } 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | // Breakpoints 2 | $bp-narrow: "(min-width: 500px)"; 3 | $bp-mid: "(min-width: 800px)"; 4 | $bp-wide: "(min-width: 1266px)"; 5 | 6 | // Colours 7 | 8 | $primary-text-color: #222; 9 | $secondary-text-color: #555555; 10 | $primary-page-color: #e3e6e6; 11 | $accent-color: #d7176a; 12 | $accent-color-alternate: #00d9ff; -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "dist" 3 | command = "npm run build" 4 | 5 | [dev] 6 | publish = "./dist" 7 | command = "npm run serve" 8 | 9 | [[redirects]] 10 | from = "/api/*" 11 | to = "/.netlify/functions/:splat" 12 | status = 200 13 | 14 | [[redirects]] 15 | from = "/cart" 16 | to = "/.netlify/functions/cart-view" 17 | status = 200 18 | 19 | 20 | [context.production] 21 | environment = { SHOPIFY_API_ENDPOINT = "https://netlify-developer-starter.myshopify.com/api/unstable/graphql.json", SHOPIFY_STOREFRONT_API_TOKEN = "e9c4dbd5f540d4cefcb2518f7648caf9" } 22 | 23 | -------------------------------------------------------------------------------- /src/site/_includes/product_summary.njk: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | {{ item.node.images.edges[0].node.alt }} 5 | 6 |
7 |
8 |

{{ item.node.title }}

9 |

{{ item.node.description | truncate(60) }}

10 |
11 | 12 | 13 | 14 |
15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shopify-storefront-reference-with-eleventy", 3 | "version": "0.1.0", 4 | "description": "A reference site for exploring the Shopify storefront API with Eleventy ", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/philhawksworth/shopify-11ty" 8 | }, 9 | "scripts": { 10 | "build": "eleventy", 11 | "serve": "eleventy --serve", 12 | "start": "npm run dev", 13 | "dev": "ntl dev" 14 | }, 15 | "keywords": [ 16 | "eleventy", 17 | "11ty", 18 | "ssg", 19 | "serverless", 20 | "netlify" 21 | ], 22 | "author": "Phil Hawksworth ", 23 | "license": "ISC", 24 | "dependencies": { 25 | "@11ty/eleventy": "^0.12.1", 26 | "dotenv": "^10.0.0", 27 | "node-fetch": "^2.6.1", 28 | "sass": "^1.29.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /netlify/functions/utils/postToShopify.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | 4 | require('dotenv').config(); 5 | exports.postToShopify = async ({ query, variables }) => { 6 | try { 7 | const result = await fetch(process.env.SHOPIFY_API_ENDPOINT, { 8 | method: 'POST', 9 | headers: { 10 | 'Content-Type': 'application/json', 11 | 'X-Shopify-Storefront-Access-Token': 12 | process.env.SHOPIFY_STOREFRONT_API_TOKEN, 13 | }, 14 | body: JSON.stringify({ query, variables }), 15 | }).then((res) => res.json()) 16 | 17 | if (result.errors) { 18 | console.log({ errors: result.errors }) 19 | } else if (!result || !result.data) { 20 | console.log({ result }) 21 | return 'No results found.' 22 | } 23 | 24 | return result.data 25 | } catch (error) { 26 | console.log(error) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.eleventy.js: -------------------------------------------------------------------------------- 1 | const sass = require("sass"); 2 | const fs = require("fs-extra"); 3 | 4 | 5 | module.exports = (eleventyConfig) => { 6 | 7 | // pass files directly through to the output 8 | eleventyConfig.addPassthroughCopy({ 9 | "src/js": "js", 10 | "src/site/images" : "images" 11 | }); 12 | 13 | 14 | // watch the scss source files in case of need to regenerate 15 | eleventyConfig.addWatchTarget("src/scss/"); 16 | 17 | // Compile Sass before a build 18 | eleventyConfig.on("beforeBuild", () => { 19 | let result = sass.renderSync({ 20 | file: "src/scss/main.scss", 21 | sourceMap: false, 22 | outputStyle: "compressed", 23 | }); 24 | fs.ensureDirSync('dist/css/'); 25 | fs.writeFile("dist/css/main.css", result.css, (err) => { 26 | if (err) throw err; 27 | console.log("CSS generated"); 28 | }); 29 | }); 30 | 31 | // where do things live? 32 | return { 33 | dir: { 34 | input: "src/site", 35 | output: "dist" 36 | } 37 | }; 38 | 39 | }; 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shopify Storefront reference with 11ty 2 | 3 | A reference site for exploring the Shopify storefront API with Eleventy 4 | 5 | Running at https://shopify-11ty.netlify.app/ 6 | Blogged at https://www.netlify.com/blog/ 7 | 8 | 9 | 10 | ``` 11 | # install the dependencies 12 | npm i 13 | 14 | # Run the local dev server during development 15 | netlify dev 16 | 17 | # Build the site 18 | npm run build 19 | ``` 20 | 21 | ## Clone and deploy 22 | 23 | Make a copy of this site and deploy it for free to Netlify by clicking this button: 24 | 25 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/philhawksworth/shopify-11ty) 26 | 27 | 28 | ## Environment variables 29 | 30 | This example site is preconfigured with the following environment variable (via the netlify.toml file) since these variables are safe to share. To customise this example to point at your own store in Shopify, you'll need to update these to your own values. 31 | 32 | ```conf 33 | SHOPIFY_API_ENDPOINT = "{YOUR SHOPIFY STORE URL}/api/unstable/graphql.json" 34 | SHOPIFY_STOREFRONT_API_TOKEN = "{YOUR SHOPIFY STOREFRONT ACCESS TOKEN}" 35 | ``` 36 | 37 | -------------------------------------------------------------------------------- /netlify/functions/remove-from-cart.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Remove Item From Cart API Endpoint 3 | * 4 | * * Purpose: Remove a single item from the cart 5 | * @param {string} cartId 6 | * @param {string} lineId - Not the item or variant id 7 | * 8 | * Example: 9 | * ``` 10 | * fetch('/.netlify/functions/remove-from-cart, { 11 | * method: 'POST', 12 | * body: JSON.stringify({ 13 | * cartId: 'S9Qcm9kdWN0VmFyaWFudC8zOTc0NDEyMDEyNzY5NA', 14 | * lineId: 'RIJC3mn0c862e2fc3314ba5971bf22d73d7accb' 15 | * }) 16 | * }) 17 | * ``` 18 | */ 19 | 20 | const { removeItemFromCart } = require('./utils/removeItemFromCart') 21 | const querystring = require("querystring"); 22 | 23 | exports.handler = async (event) => { 24 | 25 | if (event.httpMethod !== "POST") { 26 | return { statusCode: 405, body: "Method Not Allowed" }; 27 | } 28 | const { cartId, lineId } = querystring.parse(event.body) 29 | try { 30 | console.log('--------------------------------') 31 | console.log('Removing item from cart...') 32 | console.log('--------------------------------') 33 | const shopifyResponse = await removeItemFromCart({ 34 | cartId, 35 | lineId, 36 | }) 37 | 38 | return { 39 | statusCode: 302, 40 | headers: { 41 | Location: `/cart/?cartId=${cartId}`, 42 | }, 43 | } 44 | 45 | } catch (error) { 46 | console.log(error) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/site/_data/products.js: -------------------------------------------------------------------------------- 1 | const { postToShopify } = require('../../../netlify/functions/utils/postToShopify'); 2 | 3 | module.exports = async () => { 4 | 5 | const response = await postToShopify({ 6 | query: `{ 7 | products(sortKey: TITLE, first: 100) { 8 | edges { 9 | node { 10 | id 11 | handle 12 | description 13 | title 14 | productType 15 | totalInventory 16 | variants(first: 5) { 17 | edges { 18 | node { 19 | id 20 | title 21 | quantityAvailable 22 | priceV2 { 23 | amount 24 | currencyCode 25 | } 26 | } 27 | } 28 | } 29 | priceRange { 30 | maxVariantPrice { 31 | amount 32 | currencyCode 33 | } 34 | minVariantPrice { 35 | amount 36 | currencyCode 37 | } 38 | } 39 | images(first: 1) { 40 | edges { 41 | node { 42 | src 43 | altText 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | }`, 51 | variables: null 52 | }); 53 | 54 | return response.products.edges; 55 | 56 | }; -------------------------------------------------------------------------------- /src/site/_includes/product_detail.njk: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
7 |
8 |

{{ item.node.title }}

9 |

{{ item.node.description }}

10 |
11 | 12 |
13 | {% if (item.node.variants.edges.length > 1) %} 14 | {% for variant in item.node.variants.edges %} 15 |
16 | 23 |
24 | {% endfor %} 25 | {% else %} 26 | {{ item.node.variants.edges[0].node.priceV2.currencyCode }} {{ item.node.variants.edges[0].node.priceV2.amount }} 27 | 28 | {% endif %} 29 | 30 |
31 | 32 | 33 |
34 | 35 |
36 |
37 |
38 |
39 | 40 | -------------------------------------------------------------------------------- /netlify/functions/utils/addItemToCart.js: -------------------------------------------------------------------------------- 1 | const { postToShopify } = require('./postToShopify') 2 | 3 | exports.addItemToCart = async ({ cartId, itemId, quantity }) => { 4 | try { 5 | const shopifyResponse = postToShopify({ 6 | query: ` 7 | mutation addItemToCart($cartId: ID!, $lines: [CartLineInput!]!) { 8 | cartLinesAdd(cartId: $cartId, lines: $lines) { 9 | cart { 10 | id 11 | lines(first: 10) { 12 | edges { 13 | node { 14 | id 15 | quantity 16 | merchandise { 17 | ... on ProductVariant { 18 | id 19 | title 20 | priceV2 { 21 | amount 22 | currencyCode 23 | } 24 | product { 25 | title 26 | handle 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | estimatedCost { 34 | totalAmount { 35 | amount 36 | currencyCode 37 | } 38 | subtotalAmount { 39 | amount 40 | currencyCode 41 | } 42 | totalTaxAmount { 43 | amount 44 | currencyCode 45 | } 46 | totalDutyAmount { 47 | amount 48 | currencyCode 49 | } 50 | } 51 | } 52 | } 53 | } 54 | `, 55 | variables: { 56 | cartId, 57 | lines: [ 58 | { 59 | merchandiseId: itemId, 60 | quantity, 61 | }, 62 | ], 63 | }, 64 | }) 65 | 66 | return shopifyResponse 67 | } catch (error) { 68 | console.log(error) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /netlify/functions/utils/removeItemFromCart.js: -------------------------------------------------------------------------------- 1 | const { postToShopify } = require('./postToShopify') 2 | 3 | /** 4 | * @param {string} cartId - Target cart to update 5 | * @param lineId - Line id that the item belongs to 6 | */ 7 | exports.removeItemFromCart = async ({ cartId, lineId }) => { 8 | try { 9 | const shopifyResponse = await postToShopify({ 10 | query: ` 11 | mutation removeItemFromCart($cartId: ID!, $lineIds: [ID!]!) { 12 | cartLinesRemove(cartId: $cartId, lineIds: $lineIds) { 13 | cart { 14 | id 15 | lines(first: 10) { 16 | edges { 17 | node { 18 | id 19 | quantity 20 | merchandise { 21 | ... on ProductVariant { 22 | id 23 | title 24 | priceV2 { 25 | amount 26 | currencyCode 27 | } 28 | product { 29 | title 30 | handle 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | estimatedCost { 38 | totalAmount { 39 | amount 40 | currencyCode 41 | } 42 | subtotalAmount { 43 | amount 44 | currencyCode 45 | } 46 | totalTaxAmount { 47 | amount 48 | currencyCode 49 | } 50 | totalDutyAmount { 51 | amount 52 | currencyCode 53 | } 54 | } 55 | } 56 | } 57 | } 58 | `, 59 | variables: { 60 | cartId, 61 | lineIds: [lineId], 62 | }, 63 | }) 64 | 65 | return shopifyResponse 66 | } catch (error) { 67 | console.log(error) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /netlify/functions/utils/createCartWithItem.js: -------------------------------------------------------------------------------- 1 | const { postToShopify } = require('./postToShopify') 2 | 3 | // Creates a cart with a single item 4 | exports.createCartWithItem = async ({ itemId, quantity }) => { 5 | try { 6 | const response = await postToShopify({ 7 | query: ` 8 | mutation createCart($cartInput: CartInput) { 9 | cartCreate(input: $cartInput) { 10 | cart { 11 | id 12 | createdAt 13 | updatedAt 14 | lines(first:10) { 15 | edges { 16 | node { 17 | id 18 | quantity 19 | merchandise { 20 | ... on ProductVariant { 21 | id 22 | title 23 | priceV2 { 24 | amount 25 | currencyCode 26 | } 27 | product { 28 | id 29 | title 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | estimatedCost { 37 | totalAmount { 38 | amount 39 | currencyCode 40 | } 41 | subtotalAmount { 42 | amount 43 | currencyCode 44 | } 45 | totalTaxAmount { 46 | amount 47 | currencyCode 48 | } 49 | totalDutyAmount { 50 | amount 51 | currencyCode 52 | } 53 | } 54 | } 55 | } 56 | } 57 | `, 58 | variables: { 59 | cartInput: { 60 | lines: [ 61 | { 62 | quantity, 63 | merchandiseId: itemId, 64 | }, 65 | ], 66 | }, 67 | }, 68 | }) 69 | 70 | return response 71 | } catch (error) { 72 | console.log(error) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /netlify/functions/add-to-cart.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Add to Cart API Endpoint 3 | * 4 | * * Purpose: Add a single item to the cart 5 | * @param {string} cartId (Optional) 6 | * @param {string} itemId - Usually it's the product variant id 7 | * @param {number} quantity - Minimum 1 8 | * 9 | * @returns {object} cart that contains lines of items inside 10 | * See './utils/createCartWithItem' for the data structure 11 | * 12 | * Examples: 13 | * 14 | * If a cart does not exist yet, 15 | * ``` 16 | * fetch('/.netlify/functions/add-to-cart', { 17 | * method: 'POST', 18 | * body: JSON.stringify({ 19 | * cardId: '', // cardId can also be omitted if desired 20 | * itemId: 'Z2lkOi8vc2hvcGlmFyaWFudC8zOTc0NDEyMDEyNzY5NA==', 21 | * quantity: 4 22 | * }) 23 | * }) 24 | * ``` 25 | * 26 | * Add item to an existing cart 27 | * ``` 28 | * fetch('/.netlify/functions/add-to-cart', { 29 | * method: 'POST', 30 | * body: JSON.stringify({ 31 | * cartId: 'S9Qcm9kdWN0VmFyaWFudC8zOTc0NDEyMDEyNzY5NA', 32 | * itemId: 'Z2lkOi8vc2hvcGlmFyaWFudC8zOTc0NDEyMDEyNzY5NA==', 33 | * quantity: 4 34 | * }) 35 | * }) 36 | * ``` 37 | */ 38 | 39 | const { createCartWithItem } = require('./utils/createCartWithItem') 40 | const { addItemToCart } = require('./utils/addItemToCart') 41 | 42 | exports.handler = async (event) => { 43 | let { cartId, itemId, quantity } = JSON.parse(event.body) 44 | quantity = parseInt(quantity); 45 | 46 | 47 | if (cartId) { 48 | console.log('--------------------------------') 49 | console.log('Adding item to existing cart...') 50 | console.log('--------------------------------') 51 | 52 | const shopifyResponse = await addItemToCart({ 53 | cartId, 54 | itemId, 55 | quantity, 56 | }) 57 | 58 | return { 59 | statusCode: 200, 60 | body: JSON.stringify(shopifyResponse.cartLinesAdd.cart), 61 | } 62 | } else { 63 | console.log('--------------------------------') 64 | console.log('Creating new cart with item...') 65 | console.log('--------------------------------') 66 | 67 | console.log(itemId, quantity); 68 | 69 | const createCartResponse = await createCartWithItem({ 70 | itemId, 71 | quantity, 72 | }) 73 | 74 | return { 75 | statusCode: 200, 76 | body: JSON.stringify(createCartResponse.cartCreate.cart), 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/site/_includes/layouts/base.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | 6 | 7 | 8 |
9 |

Shoperoni

10 | 30 |
31 |
32 | {{ content | safe }} 33 |
34 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/js/shopping-ui.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | getCartSummaryDetails(); 4 | 5 | const forms = document.getElementsByClassName('addToCart'); 6 | for (var f = 0; f < forms.length; f++) { 7 | // add form event listeners 8 | forms[f].addEventListener('submit', postToCart); 9 | 10 | // if we have a cartId stashed, we should add it to any cart form on this page 11 | forms[f].elements['cartId'].value = localStorage.getItem('shopifyCartId') || ""; 12 | 13 | } 14 | })(); 15 | 16 | 17 | // fetch cart data from the API 18 | function getCartSummaryDetails() { 19 | if (localStorage.getItem('shopifyCartId')){ 20 | postData('/api/get-cart', { 21 | 'cartId': localStorage.getItem('shopifyCartId') 22 | }) 23 | .then(data => { 24 | if(data.cart) { 25 | displayCartSummaryDetails(data.cart.lines.edges.length, data.cart.id); 26 | } 27 | else { 28 | //clear a local cart if it has expired with Shopify 29 | localStorage.removeItem('shopifyCartId'); 30 | } 31 | }); 32 | } else { 33 | console.log(`No shopping cart yet`); 34 | } 35 | } 36 | 37 | 38 | // Update the UI with latest cart info 39 | function displayCartSummaryDetails(count, id) { 40 | const cartLink = document.getElementsByClassName('cartLink')[0]; 41 | const cartSize = document.getElementsByClassName('cart-size')[0]; 42 | if (cartLink) { 43 | cartLink.href = `/cart/?cartId=${id}`; 44 | } 45 | if (cartSize) { 46 | cartSize.innerHTML = `${count}`; 47 | } 48 | } 49 | 50 | 51 | // dispatch post requests 52 | async function postData(url = '', data = {}) { 53 | const response = await fetch(url, { 54 | method: 'POST', 55 | headers: { 56 | 'Content-Type': 'application/json' 57 | }, 58 | body: JSON.stringify(data) 59 | }); 60 | return response.json(); 61 | } 62 | 63 | 64 | // Send an item to the cart API 65 | function postToCart(event) { 66 | event.preventDefault(); 67 | const inputs = event.target.elements; 68 | const data = { 69 | cartId: inputs['cartId'].value == "undefined" ? null : inputs['cartId'].value, 70 | itemId: inputs['merchandiseId'].value, 71 | quantity: inputs['quantity'].value, 72 | }; 73 | 74 | postData('/api/add-to-cart', data) 75 | .then(data => { 76 | // persist that cartId for subsequent actions 77 | localStorage.setItem('shopifyCartId', data.id); 78 | // update the cart ;abel in the navigation 79 | displayCartSummaryDetails(data.lines.edges.length, data.id); 80 | }); 81 | }; 82 | -------------------------------------------------------------------------------- /netlify/functions/get-cart.js: -------------------------------------------------------------------------------- 1 | /** 2 | * API Endpoint 3 | * 4 | * * Purpose: Get items from an existing cart 5 | * @param {string} cartId 6 | * 7 | * Example: 8 | *``` 9 | * fetch('/.netlify/functions/get-cart', { 10 | * method: 'POST', 11 | * body: JSON.stringify({ cartId: '12345' }) 12 | * }) 13 | * ``` 14 | * 15 | * ! POST method is intentional for future enhancement 16 | * 17 | * TODO: Add enhancement for pagination 18 | */ 19 | 20 | const { postToShopify } = require('./utils/postToShopify') 21 | 22 | exports.handler = async (event) => { 23 | const { cartId } = JSON.parse(event.body); 24 | try { 25 | console.log('--------------------------------') 26 | console.log('Retrieving existing cart...') 27 | console.log('--------------------------------') 28 | const shopifyResponse = await postToShopify({ 29 | query: ` 30 | query getCart($cartId: ID!) { 31 | cart(id: $cartId) { 32 | id 33 | lines(first: 10) { 34 | edges { 35 | node { 36 | id 37 | quantity 38 | merchandise { 39 | ... on ProductVariant { 40 | id 41 | title 42 | priceV2 { 43 | amount 44 | currencyCode 45 | } 46 | product { 47 | title 48 | handle 49 | images(first: 1) { 50 | edges { 51 | node { 52 | src 53 | altText 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | estimatedCost { 64 | totalAmount { 65 | amount 66 | currencyCode 67 | } 68 | subtotalAmount { 69 | amount 70 | currencyCode 71 | } 72 | totalTaxAmount { 73 | amount 74 | currencyCode 75 | } 76 | totalDutyAmount { 77 | amount 78 | currencyCode 79 | } 80 | } 81 | } 82 | } 83 | `, 84 | variables: { 85 | cartId, 86 | }, 87 | }) 88 | return { 89 | statusCode: 200, 90 | body: JSON.stringify(shopifyResponse), 91 | } 92 | } catch (error) { 93 | console.log(error) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/scss/main.blue.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | } 5 | 6 | :root { 7 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, 8 | sans-serif, Apple Color Emoji, Segoe UI Emoji; 9 | font-size: 1rem; 10 | --user-font-scale: calc(1rem - 16px); 11 | font-size: clamp( 12 | 0.875rem, 13 | calc(0.4626rem + 1.0309vw + var(--user-font-scale)), 14 | 1.125rem 15 | ); 16 | } 17 | 18 | body { 19 | margin: 0; 20 | width: 100%; 21 | min-height: 100vh; 22 | display: grid; 23 | justify-content: center; 24 | background: #f9fafb; 25 | color: #111827; 26 | } 27 | 28 | header { 29 | margin: 50px 0; 30 | padding-bottom: 50px; 31 | border-bottom: 2px solid #e5eaef; 32 | text-align: center; 33 | } 34 | 35 | header h2 { 36 | font-weight: 500; 37 | margin-bottom: 30px; 38 | } 39 | 40 | main { 41 | max-width: 1200px; 42 | padding: 20px; 43 | } 44 | 45 | a { 46 | color: #e5eaef; 47 | } 48 | 49 | .cart { 50 | position: absolute; 51 | top: 10px; 52 | right: 10px; 53 | padding: 10px; 54 | border-radius: 10px; 55 | text-decoration: none; 56 | 57 | border: 2px solid #5890f3; 58 | color: #5890f3; 59 | } 60 | 61 | .home { 62 | position: absolute; 63 | top: 10px; 64 | left: 10px; 65 | padding: 10px; 66 | border-radius: 10px; 67 | text-decoration: none; 68 | 69 | border: 2px solid #5890f3; 70 | color: #5890f3; 71 | } 72 | 73 | .cart:hover, 74 | .home:hover { 75 | background: #5890f3; 76 | color: #e5eaef; 77 | } 78 | 79 | .products { 80 | display: flex; 81 | flex-wrap: wrap; 82 | justify-content: center; 83 | padding: 0; 84 | } 85 | 86 | .product { 87 | display: flex; 88 | flex-direction: column; 89 | justify-content: space-between; 90 | align-items: center; 91 | padding: 10px; 92 | margin: 10px; 93 | width: 200px; 94 | height: 250px; 95 | list-style-type: none; 96 | border: 2px solid #e5eaef; 97 | border-radius: 10px; 98 | box-shadow: 0 10px 15px -10px rgba(0, 0, 0, 0.1); 99 | transition: box-shadow 0.5s; 100 | } 101 | .product:hover { 102 | box-shadow: 0 20px 15px -10px rgba(0, 0, 0, 0.1); 103 | cursor: pointer; 104 | } 105 | 106 | .product a { 107 | text-decoration: none; 108 | } 109 | 110 | .product h2 { 111 | padding: 5px; 112 | color: #435a70; 113 | font-size: 1.2em; 114 | text-align: center; 115 | } 116 | 117 | .product p { 118 | padding: 5px; 119 | font-size: 0.8em; 120 | opacity: 0.6; 121 | text-overflow: ellipsis; 122 | } 123 | 124 | .product .frame { 125 | width: 180px; 126 | height: 100px; 127 | border-radius: 10px; 128 | overflow: hidden; 129 | } 130 | 131 | .product img { 132 | width: 100%; 133 | border-radius: 10px; 134 | object-fit: cover; 135 | height: 100%; 136 | } 137 | 138 | .product-page { 139 | display: flex; 140 | flex-wrap: wrap; 141 | justify-content: center; 142 | } 143 | 144 | .product-img { 145 | margin-bottom: 20px; 146 | padding: 0 10px; 147 | } 148 | 149 | .product-img img { 150 | max-width: min(100%, 500px); 151 | border-radius: 10px; 152 | } 153 | 154 | .product-copy { 155 | padding: 0 10px; 156 | max-width: 500px; 157 | } 158 | 159 | .product-copy p { 160 | margin: 20px 0; 161 | } 162 | 163 | .product-copy button { 164 | padding: 10px; 165 | background: transparent; 166 | border-radius: 10px; 167 | border: 2px solid #5890f3; 168 | color: #5890f3; 169 | font-size: 1em; 170 | } 171 | 172 | .product-copy button:hover, 173 | .product-copy button:focus { 174 | background: #5890f3; 175 | color: #e5eaef; 176 | cursor: pointer; 177 | } 178 | 179 | @media (prefers-color-scheme: dark) { 180 | body { 181 | background: #376fd6; 182 | color: #fff; 183 | } 184 | .product { 185 | background: #5890f3; 186 | border: 2px solid rgba(255, 255, 255, 0.3); 187 | } 188 | .product h2 { 189 | color: #fff; 190 | } 191 | .home, 192 | .cart { 193 | border: 2px solid #e5eaef; 194 | color: #e5eaef; 195 | } 196 | .product-copy button { 197 | border: 2px solid #e5eaef; 198 | color: #e5eaef; 199 | } 200 | .home:hover, 201 | .cart:hover, 202 | .product-copy button:hover, 203 | .product-copy button:focus { 204 | background: #e5eaef; 205 | color: #5890f3; 206 | } 207 | } 208 | 209 | 210 | th,td { 211 | text-align: left; 212 | padding: 0.2rem 1.6rem 0 0 213 | } 214 | 215 | div.variant { 216 | display: block; 217 | label { 218 | margin-left: 0.5rem; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /netlify/functions/cart-view.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | 4 | exports.handler = async (event) => { 5 | 6 | const rootURL = process.env.URL || "https://localhost:8888"; 7 | 8 | const cartId = event.queryStringParameters.cartId; 9 | const result = await fetch(`${rootURL}/api/get-cart`, { 10 | method: 'POST', 11 | headers: { 12 | 'Content-Type': 'application/json', 13 | }, 14 | body: JSON.stringify({ 15 | cartId: cartId 16 | }), 17 | }) 18 | .then((res) =>{ 19 | return res.json() 20 | }); 21 | 22 | 23 | const itemTotal = function(price, quantity) { 24 | const totalPrice = Number(price) * Number(quantity) 25 | return totalPrice.toFixed(2) 26 | } 27 | 28 | 29 | const cartItem = (cartId, item) => { 30 | const displayTitleModifier = item.merchandise.title == "Default Title" ? "" : item.merchandise.title; 31 | return ` 32 | 33 | 34 | ${ item.merchandise.product.title } (${ item.merchandise.title }) 35 | 36 | 37 | 38 | ${item.merchandise.priceV2.amount} 39 | 40 | ${ item.quantity } 41 | 42 | ${ itemTotal(item.merchandise.priceV2.amount, item.quantity) } 43 | 44 | 45 |
46 | 47 | 48 | 49 |
50 | 51 | 52 | `}; 53 | 54 | const cartTotals = (cart) => { 55 | 56 | if (!cart.lines.edges.length) { 57 | console.log(`No basket`); 58 | return `
59 | 62 |
`; 63 | } 64 | 65 | return ` 66 |
67 |
68 |

69 | Subtotal: 70 |

71 |

Shipping:

72 |

Tax:

73 |

Total:

74 |
75 |
76 |

77 | ${cart.estimatedCost.subtotalAmount.amount} ${cart.estimatedCost.totalAmount.currencyCode} 78 |

79 |

Free Shipping

80 |

${cart.estimatedCost.totalTaxAmount.amount} ${cart.estimatedCost.totalAmount.currencyCode}

81 |

${cart.estimatedCost.totalAmount.amount} ${cart.estimatedCost.totalAmount.currencyCode}

82 |
83 |
`; 84 | } 85 | 86 | 87 | let items = ""; 88 | result.cart.lines.edges.forEach(item => { 89 | items += cartItem(result.cart.id, item.node) 90 | }); 91 | 92 | 93 | 94 | 95 | const pageTemplate = (items, totals) => {return ` 96 | 97 | 98 | 99 | Your Cart 100 | 101 | 102 | 103 |
104 |

Shoperoni

105 | 125 |
126 |
127 |
128 |
129 |

Your Cart

130 |
131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | ${items} 141 | 142 |
ItemPriceQuantityTotalActions
143 |
144 | ${cartTotals(result.cart)} 145 |
146 |
147 |
148 |
149 |
150 | 192 | 193 | 194 | 195 | `}; 196 | 197 | return { 198 | statusCode: 200, 199 | body: pageTemplate(items, result.cart.estimatedCost) 200 | }; 201 | 202 | } -------------------------------------------------------------------------------- /src/scss/main.scss: -------------------------------------------------------------------------------- 1 | *, 2 | :after, 3 | :before { 4 | box-sizing: border-box; 5 | margin: 0; 6 | } 7 | body, 8 | html { 9 | font-family: Nanum Gothic, Montserrat, -apple-system, BlinkMacSystemFont, 10 | Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif; 11 | font-size: 16px; 12 | word-spacing: 1px; 13 | -ms-text-size-adjust: 100%; 14 | -webkit-text-size-adjust: 100%; 15 | -moz-osx-font-smoothing: grayscale; 16 | -webkit-font-smoothing: antialiased; 17 | box-sizing: border-box; 18 | } 19 | body { 20 | border: 10px solid #ccc; 21 | min-height: 100vh; 22 | line-height: 1.4; 23 | } 24 | h1, 25 | h2, 26 | h3 { 27 | font-family: Domine, 'PT Serif', -apple-system, BlinkMacSystemFont, Segoe UI, 28 | Roboto, Helvetica Neue, Arial, sans-serif; 29 | font-weight: 400; 30 | } 31 | h1 { 32 | font-size: 2.5rem; 33 | } 34 | p { 35 | margin: 20px 0; 36 | } 37 | a, 38 | a:active, 39 | a:visited { 40 | color: #d96528; 41 | text-decoration: none; 42 | transition: all 0.3s ease; 43 | } 44 | button { 45 | border: 1px solid #ccc; 46 | background: #fff; 47 | padding: 10px 14px; 48 | cursor: pointer; 49 | color: #000; 50 | font-weight: 700; 51 | font-family: Nanum Gothic, Montserrat, -apple-system, BlinkMacSystemFont, 52 | Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif; 53 | transition: all 0.3s ease; 54 | } 55 | button:hover { 56 | background: #000; 57 | border: 1px solid #000; 58 | color: #fff; 59 | } 60 | hr { 61 | border-top: 1px solid #eee; 62 | margin: 30px 0; 63 | } 64 | input { 65 | font-family: Nanum Gothic, Montserrat, -apple-system, BlinkMacSystemFont, 66 | Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif; 67 | font-size: 16px; 68 | padding: 5px 10px; 69 | } 70 | .app-header { 71 | flex-direction: column; 72 | padding: 40px 40px 0; 73 | } 74 | .app-header, 75 | .main-nav { 76 | display: flex; 77 | justify-content: center; 78 | align-items: center; 79 | } 80 | .main-nav { 81 | width: 80vw; 82 | margin-top: 30px; 83 | border-top: 1px solid #ccc; 84 | border-bottom: 1px solid #ccc; 85 | padding: 8px 0; 86 | } 87 | .main-nav ul { 88 | padding-left: 0; 89 | } 90 | .main-nav-item { 91 | position: relative; 92 | display: inline; 93 | padding: 0 3px; 94 | font-size: 0.6rem; 95 | letter-spacing: 0.1em; 96 | text-transform: uppercase; 97 | } 98 | @media screen and (min-width: 414px) { 99 | .main-nav-item { 100 | padding: 0 8px; 101 | border-left: 1px solid #ddd; 102 | border-right: 1px solid #ddd; 103 | font-size: 0.7rem; 104 | } 105 | } 106 | @media screen and (min-width: 640px) { 107 | .main-nav-item { 108 | padding: 0 10px; 109 | font-size: 0.8rem; 110 | } 111 | } 112 | .main-nav-item a { 113 | color: #000; 114 | } 115 | .main-nav-item a:hover { 116 | color: #d96528; 117 | } 118 | .cart-size { 119 | position: absolute; 120 | top: -18px; 121 | right: -20px; 122 | width: 25px; 123 | height: 25px; 124 | padding: 6px 10px; 125 | border-radius: 1000px; 126 | background: #000; 127 | text-align: center; 128 | color: #fff; 129 | font-size: 10px; 130 | font-weight: 700; 131 | } 132 | @media screen and (min-width: 768px) { 133 | .cart-size { 134 | right: -18px; 135 | } 136 | } 137 | .testimonial { 138 | width: 100%; 139 | height: 280px; 140 | background: url(/images/testimonial-bg.jpeg) 50% no-repeat; 141 | background-size: cover; 142 | display: flex; 143 | justify-content: center; 144 | align-items: center; 145 | flex-direction: column; 146 | color: #fff; 147 | } 148 | .testimonial h2 { 149 | padding: 0 30px; 150 | text-align: center; 151 | } 152 | .project-credit { 153 | width: 100%; 154 | padding: 10px 30px; 155 | background: #000; 156 | color: #fff; 157 | text-align: center; 158 | } 159 | .project-credit a, 160 | .project-credit a:active, 161 | .project-credit a:visited { 162 | color: #2af; 163 | font-weight: 700; 164 | } 165 | .app-footer-links { 166 | width: 80%; 167 | padding: 40px 0; 168 | margin-left: 10%; 169 | display: grid; 170 | grid-template-columns: 1fr 1fr; 171 | grid-template-rows: 1fr 1fr; 172 | grid-row-gap: 30px; 173 | } 174 | @media screen and (min-width: 1024px) { 175 | .app-footer-links { 176 | grid-template-columns: 1fr 1fr 2fr; 177 | grid-template-rows: 1fr; 178 | grid-row-gap: 0; 179 | } 180 | } 181 | .app-footer-links ul { 182 | list-style: none; 183 | padding-left: 0; 184 | } 185 | .newsletter { 186 | width: 100%; 187 | grid-column: 1 / span 2; 188 | } 189 | @media screen and (min-width: 1024px) { 190 | .newsletter { 191 | grid-column: 3; 192 | } 193 | } 194 | .newsletter-title { 195 | margin-bottom: 1rem; 196 | } 197 | .newsletter-input { 198 | width: 100%; 199 | padding: 10px; 200 | } 201 | .cart-page { 202 | width: 80vw; 203 | margin: 0 auto; 204 | } 205 | .cart-page-button.is-dark { 206 | background: #222; 207 | color: #f8f8f8; 208 | padding: 10px 14px; 209 | display: inline-block; 210 | } 211 | .cart-page-content { 212 | margin: 2rem 0 3rem; 213 | text-align: center; 214 | } 215 | .cart-page-message { 216 | margin-bottom: 1.5rem; 217 | } 218 | .cart-table { 219 | width: 100%; 220 | margin-top: 20px; 221 | margin-bottom: 30px; 222 | } 223 | .cart-table-cell { 224 | padding: 8px 0; 225 | border-bottom: 1px solid #ccc; 226 | } 227 | .cart-table-heading { 228 | padding: 10px 0; 229 | border-bottom: 1px solid #ccc; 230 | } 231 | .cart-table-row { 232 | text-align: center; 233 | } 234 | .cart-total { 235 | display: grid; 236 | grid-template-columns: repeat(5, 1fr); 237 | } 238 | .cart-total-content { 239 | grid-column: 1 / span 5; 240 | display: grid; 241 | grid-template-columns: repeat(2, 1fr); 242 | } 243 | @media screen and (min-width: 1024px) { 244 | .cart-total-content { 245 | grid-column: 4 / span 2; 246 | } 247 | } 248 | .cart-total-column p { 249 | padding: 10px; 250 | margin: 0; 251 | text-align: right; 252 | } 253 | .cart-total-column p:last-child { 254 | font-weight: 700; 255 | background: #f2eee2; 256 | } 257 | .product-page { 258 | margin: 60px 0; 259 | } 260 | .product-page-content { 261 | width: 80%; 262 | margin: 30px auto 0; 263 | } 264 | @media screen and (min-width: 1024px) { 265 | .product-page-content { 266 | display: grid; 267 | justify-content: space-between; 268 | justify-items: center; 269 | align-items: center; 270 | grid-template-columns: 1fr 1fr; 271 | grid-column-gap: 30px; 272 | } 273 | } 274 | .product-page-image { 275 | width: 100%; 276 | margin-bottom: 30px; 277 | } 278 | @media screen and (min-width: 1024px) { 279 | .product-page-image { 280 | width: 100%; 281 | margin-bottom: 0; 282 | } 283 | } 284 | .product-page-price { 285 | color: #d96528; 286 | font-size: 1.2rem; 287 | margin: 5px 0; 288 | font-weight: 400; 289 | font-family: Domine, 'PT Serif', -apple-system, BlinkMacSystemFont, Segoe UI, 290 | Roboto, Helvetica Neue, Arial, sans-serif; 291 | } 292 | .product-page-price-list, 293 | .product-page-price.is-solo { 294 | margin-bottom: 30px; 295 | } 296 | .product-page-quantity-input { 297 | width: 70px; 298 | } 299 | .product-page-quantity-row { 300 | display: flex; 301 | } 302 | main { 303 | margin: 30px 0 45px; 304 | } 305 | .product-grid { 306 | max-width: 60vw; 307 | margin: 0 auto; 308 | display: grid; 309 | grid-template-columns: 1fr; 310 | grid-template-rows: 1fr; 311 | grid-column-gap: 40px; 312 | grid-row-gap: 0; 313 | } 314 | @media screen and (min-width: 640px) { 315 | .product-grid { 316 | grid-template-columns: repeat(2, 1fr); 317 | } 318 | } 319 | @media screen and (min-width: 1024px) { 320 | .product-grid { 321 | grid-template-columns: repeat(3, 1fr); 322 | } 323 | } 324 | @media screen and (min-width: 1280px) { 325 | .product-grid { 326 | grid-template-columns: repeat(4, 1fr); 327 | } 328 | } 329 | .product-card { 330 | max-height: 500px; 331 | display: flex; 332 | justify-content: space-between; 333 | align-items: center; 334 | flex-direction: column; 335 | margin: 20px 0; 336 | } 337 | .product-card-description { 338 | margin-top: 0; 339 | margin-bottom: 1rem; 340 | overflow: hidden; 341 | width: 100%; 342 | display: -webkit-box; 343 | -webkit-box-orient: vertical; 344 | -webkit-line-clamp: 2; 345 | } 346 | .product-card-frame { 347 | height: 120px; 348 | margin-bottom: 0.5rem; 349 | display: flex; 350 | align-content: center; 351 | align-items: center; 352 | border-radius: 10px; 353 | overflow: hidden; 354 | } 355 | .product-card-frame img { 356 | width: 100%; 357 | border-radius: 10px; 358 | -o-object-fit: cover; 359 | object-fit: cover; 360 | height: 100%; 361 | } 362 | .product-card-text { 363 | margin: 0.5rem 0; 364 | } 365 | .product-card-title { 366 | margin: 0.5rem 0; 367 | font-weight: 700; 368 | } --------------------------------------------------------------------------------