├── pages ├── shop │ ├── _collection │ │ ├── index.vue │ │ └── products │ │ │ └── _product.vue │ └── index.vue ├── index.vue ├── shopify-admin.vue └── cart.vue ├── .eslintrc.js ├── app.html ├── plugins ├── global-component-loader.js └── google-gtag.client.js ├── layouts └── default.vue ├── gql ├── mutations │ └── Shopify.gql ├── fragments │ └── Shopify.gql └── queries │ └── Shopify.gql ├── components ├── shopify │ ├── SelectQuantity.vue │ ├── MiniCart.vue │ ├── BlockProduct.vue │ ├── SelectVariant.vue │ ├── Price.vue │ └── AddToCart.vue ├── LoadingIcon.vue └── ResponsiveImage.vue ├── utils └── shopify.js ├── package.json ├── .gitignore ├── README.md ├── nuxt.config.js └── store └── shopify.js /pages/shop/_collection/index.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["eslint:recommended", "plugin:vue/recommended"], 3 | rules: { 4 | // override/add rules settings here, such as: 5 | indent: ["error", 4, { SwitchCase: 1 }], 6 | "vue/html-indent": ["error", 4], 7 | semi: [2, "never"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /pages/shopify-admin.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /app.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | {{ HEAD }} 14 | 15 | 16 | {{ APP }} 17 | 18 | 19 | -------------------------------------------------------------------------------- /pages/cart.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 26 | -------------------------------------------------------------------------------- /plugins/global-component-loader.js: -------------------------------------------------------------------------------- 1 | // Register all files inside /components globally 2 | import Vue from "vue" 3 | import _kebabCase from "lodash/kebabCase" 4 | 5 | const components = require.context("~/components", false, /[A-Z]\w+\.(vue)$/) 6 | components.keys().map(fileName => { 7 | // Get component config 8 | const componentConfig = components(fileName) 9 | 10 | // Turn './ComponentName.vue' into 'component-name' 11 | const componentName = _kebabCase( 12 | fileName.replace(/^\.\//, "").replace(/\.vue$/, "") 13 | ) 14 | 15 | // Register new component globally 16 | Vue.component(componentName, componentConfig.default || componentConfig) 17 | }) 18 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 24 | 25 | 34 | -------------------------------------------------------------------------------- /gql/mutations/Shopify.gql: -------------------------------------------------------------------------------- 1 | #import "~/gql/fragments/Shopify.gql" 2 | 3 | # SEE https://help.shopify.com/en/api/storefront-api/reference/mutation/checkoutcreate 4 | mutation CheckoutCreate($variantId: ID!, $quantity: Int!) { 5 | checkoutCreate( 6 | input: { lineItems: [{ variantId: $variantId, quantity: $quantity }] } 7 | ) { 8 | checkoutUserErrors { 9 | code 10 | message 11 | field 12 | } 13 | checkout { 14 | ...checkout 15 | } 16 | } 17 | } 18 | 19 | # SEE https://help.shopify.com/en/api/storefront-api/reference/mutation/checkoutlineitemsreplace 20 | mutation ReplaceCheckout( 21 | $checkoutId: ID! 22 | $lineItems: [CheckoutLineItemInput!]! 23 | ) { 24 | checkoutLineItemsReplace(checkoutId: $checkoutId, lineItems: $lineItems) { 25 | userErrors { 26 | code 27 | message 28 | field 29 | } 30 | checkout { 31 | ...checkout 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /components/shopify/SelectQuantity.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 39 | 40 | 46 | -------------------------------------------------------------------------------- /utils/shopify.js: -------------------------------------------------------------------------------- 1 | import _get from "lodash/get" 2 | 3 | /* 4 | * Takes a Shopiy `checkoutlineitemconnection` object and strips out the edges and nodes 5 | * SEE https://help.shopify.com/en/api/storefront-api/reference/object/checkoutlineitemconnection 6 | */ 7 | export const stripLineItems = items => { 8 | return _get(items, "edges", []).map(item => item.node) 9 | } 10 | 11 | /* 12 | * Takes an array of objects and returns an array of {variantId, quantity} objects 13 | * Useful for replacing a checkout 14 | * SEE https://help.shopify.com/en/api/storefront-api/reference/mutation/checkoutlineitemsreplace 15 | */ 16 | export const simplifyLineItems = items => { 17 | return items.map(obj => { 18 | return { 19 | variantId: obj.variantId || obj.variant.id, 20 | quantity: obj.quantity 21 | } 22 | }) 23 | } 24 | 25 | /* 26 | * Takes a Shopify value, and then appends the currency to it. 27 | */ 28 | export const formatMoney = (value, currency) => { 29 | return `$${parseFloat(value).toFixed(2)} ${currency}` 30 | } 31 | -------------------------------------------------------------------------------- /components/shopify/MiniCart.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 39 | 40 | 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stackhaus", 3 | "version": "1.0.0", 4 | "engines": { 5 | "node": "10.13.0" 6 | }, 7 | "description": "A Vue/Nuxt frontend boiler plate optimzed to work with the Stackhaus-Backend WordPress theme.", 8 | "author": "Funkhaus", 9 | "private": true, 10 | "scripts": { 11 | "dev": "nuxt", 12 | "build": "nuxt build", 13 | "start": "nuxt start", 14 | "generate": "nuxt generate", 15 | "heroku-postbuild": "npm run build" 16 | }, 17 | "dependencies": { 18 | "@nuxtjs/apollo": "^4.0.0-rc9", 19 | "core-js": "^2.6.9", 20 | "graphql": "^14.4.2", 21 | "lodash": "^4.17.15", 22 | "node-fetch": "^2.4.1", 23 | "nuxt": "^2.9.1", 24 | "nuxt-vuex-localstorage": "^1.2.6", 25 | "vue-apollo": "^3.0.0-rc.2" 26 | }, 27 | "devDependencies": { 28 | "eslint": "^5.16.0", 29 | "eslint-plugin-vue": "^5.2.3", 30 | "node-sass": "^4.12.0", 31 | "nodemon": "^1.19.1", 32 | "prettier": "^1.18.2", 33 | "prettier-eslint": "^9.0.0", 34 | "sass-loader": "^7.3.1", 35 | "svg-inline-loader": "^0.8.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /components/LoadingIcon.vue: -------------------------------------------------------------------------------- 1 | 11 | 56 | -------------------------------------------------------------------------------- /gql/fragments/Shopify.gql: -------------------------------------------------------------------------------- 1 | fragment lineItems on CheckoutLineItemConnection { 2 | edges { 3 | node { 4 | id 5 | title 6 | quantity 7 | variant { 8 | id 9 | title 10 | priceV2 { 11 | amount 12 | } 13 | image { 14 | ...productBlockImage 15 | } 16 | product { 17 | title 18 | handle 19 | vendor 20 | id 21 | } 22 | } 23 | } 24 | } 25 | } 26 | 27 | fragment checkout on Checkout { 28 | id 29 | webUrl 30 | completedAt 31 | lineItems(first: 50) { 32 | ...lineItems 33 | } 34 | subtotalPriceV2 { 35 | amount 36 | currencyCode 37 | } 38 | totalPriceV2 { 39 | amount 40 | currencyCode 41 | } 42 | } 43 | 44 | fragment productImage on Image { 45 | transformedSrc(maxWidth: 1920, maxHeight: 1920, crop: CENTER) 46 | altText 47 | id 48 | } 49 | 50 | fragment productBlockImage on Image { 51 | transformedSrc(maxWidth: 600, maxHeight: 600, crop: CENTER) 52 | altText 53 | id 54 | } 55 | 56 | fragment priceRange on ProductPriceRange { 57 | maxVariantPrice { 58 | amount 59 | currencyCode 60 | } 61 | minVariantPrice { 62 | amount 63 | currencyCode 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE 81 | .idea 82 | 83 | # Service worker 84 | sw.* 85 | 86 | # Environment Vars 87 | .env 88 | 89 | # OS Junk 90 | .DS_Store 91 | -------------------------------------------------------------------------------- /plugins/google-gtag.client.js: -------------------------------------------------------------------------------- 1 | import _get from "lodash/get" 2 | 3 | export default ({ store, app: { router, context } }, inject) => { 4 | // Remove any empty tracking codes 5 | let codes = _get(store, "state.siteMeta.gaTrackingCodes", []) 6 | codes = codes.filter(Boolean) 7 | 8 | // Abort if no codes 9 | if (!codes.length) { 10 | if (context.isDev) console.log("No Google Anlaytics tracking codes set") 11 | inject("gtag", () => {}) 12 | return 13 | } 14 | 15 | // Abort if in Dev mode, but inject dummy functions so $gtag events don't throw errors 16 | if (context.isDev) { 17 | inject("gtag", () => {}) 18 | return 19 | } 20 | 21 | // Abort if we already added script to head 22 | let gtagScript = document.getElementById("gtag") 23 | if (gtagScript) { 24 | return 25 | } 26 | 27 | // Add script tag to head 28 | let script = document.createElement("script") 29 | script.async = true 30 | script.id = "gtag" 31 | script.src = "//www.googletagmanager.com/gtag/js" 32 | document.head.appendChild(script) 33 | 34 | // Include Google gtag code and inject it (so this.$gtag works in pages/components) 35 | window.dataLayer = window.dataLayer || [] 36 | function gtag() { 37 | dataLayer.push(arguments) 38 | } 39 | inject("gtag", gtag) 40 | gtag("js", new Date()) 41 | 42 | // Add tracking codes from Vuex store 43 | codes.forEach(code => { 44 | gtag("config", code, { 45 | send_page_view: false // Necessary to avoid duplicated page track on first page load 46 | }) 47 | 48 | // After each router transition, log page event to Google for each code 49 | router.afterEach(to => { 50 | gtag("config", code, { page_path: to.fullPath }) 51 | }) 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shophaus 2 | 3 | ## Build Setup 4 | 5 | ```bash 6 | # install dependencies 7 | $ npm install 8 | 9 | # serve with hot reload at localhost:3000 10 | $ npm run dev 11 | 12 | # build for production and launch server 13 | $ npm run build 14 | $ npm start 15 | 16 | # generate static project 17 | $ npm run generate 18 | ``` 19 | 20 | For detailed explanation on how things work, checkout [Nuxt.js docs](https://nuxtjs.org). 21 | 22 | ## Important Readings 23 | 24 | Built around the Storefront API 25 | https://help.shopify.com/en/api/storefront-api 26 | 27 | Integrated with Google Tag Manager for Enhanced Ecommerce 28 | https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-ecommerce 29 | 30 | ## Install Instructions 31 | 32 | Note that for the Shopify API there is no "cart", they refer to that as a "Checkout". We have tried to follow that convention. 33 | 34 | 1. Install required packages. See dependencies in `package.json` file for list. 35 | 1. In Nuxt config, set Shopify endpoint and headers, get your Shopify access token here (where?). 36 | 1. Copy settings from `nuxt.config.js` 37 | 38 | ## Required files 39 | 40 | 1. All files in `~/gql` 41 | 1. The Shopify store module at `~/store/shopify.js` 42 | 1. The Shopify utility file at `~/utils/shopify.js` 43 | 44 | ### Optional files 45 | 46 | 1. The pages in `~/pages` are useful, but are technically optional. 47 | 1. All the components in `~/components/shopify` are useful, but are technically optional. 48 | 49 | ## TODO list 50 | 51 | TODO improvements: 52 | 53 | 1. Get cart page built 54 | 1. Clear checkout on completion (Webhooks? Some other way? If site referrer URL is the shopify url?) 55 | 1. Document all the store actions 56 | 1. Document Loading property and it's increments events 57 | 1. Figure out how to query for Shopify menus? 58 | 1. Get `this.gtag` working in all places (remove from cart, update cart quantity etc) 59 | 1. Build Shopify theme 60 | 1. User Login/Account 61 | 1. Theme Settings 62 | 1. Checkout confirmation? 63 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | import pkg from "./package" 2 | 3 | export default { 4 | mode: "universal", 5 | 6 | /* 7 | ** Headers of the page 8 | */ 9 | head: { 10 | meta: [ 11 | { charset: "utf-8" }, 12 | { 13 | name: "viewport", 14 | content: "width=device-width, initial-scale=1" 15 | } 16 | ], 17 | link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.png" }] 18 | }, 19 | 20 | /* 21 | ** Plugins to load before mounting the App 22 | */ 23 | plugins: [ 24 | { src: "~/plugins/global-component-loader.js" }, 25 | { src: "~/plugins/google-gtag.client.js", mode: "client" } 26 | //{ src: "~/plugins/shopify.js", mode: "client" } 27 | ], 28 | 29 | /* 30 | ** Nuxt.js modules 31 | */ 32 | modules: [ 33 | "@nuxtjs/apollo", 34 | [ 35 | "nuxt-vuex-localstorage", 36 | { 37 | localStorage: ["shopify"], 38 | mode: "debug" 39 | } 40 | ] 41 | ], 42 | 43 | /* 44 | ** Apollo options. Used for Graph QL queries 45 | ** See: https://www.apollographql.com/docs/link/links/http.html#options 46 | */ 47 | apollo: { 48 | clientConfigs: { 49 | default: { 50 | httpEndpoint: 51 | "http://stackhaus-backend.flywheelsites.com/graphql", 52 | persisting: false 53 | }, 54 | shopify: { 55 | httpEndpoint: 56 | "https://your-store-name-here.myshopify.com/api/2019-07/graphql.json", 57 | httpLinkOptions: { 58 | headers: { 59 | "Content-Type": "application/json", 60 | "X-Shopify-Storefront-Access-Token": 61 | "12345678abcd" 62 | } 63 | }, 64 | persisting: false 65 | } 66 | } 67 | }, 68 | 69 | /* 70 | ** Customize router 71 | */ 72 | router: { 73 | linkExactActiveClass: "exact-active-link", 74 | linkActiveClass: "active-link" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /pages/shop/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 75 | 76 | 83 | -------------------------------------------------------------------------------- /components/shopify/BlockProduct.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 98 | 99 | 105 | -------------------------------------------------------------------------------- /components/shopify/SelectVariant.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 96 | 97 | 103 | -------------------------------------------------------------------------------- /components/shopify/Price.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 107 | 108 | 116 | -------------------------------------------------------------------------------- /components/shopify/AddToCart.vue: -------------------------------------------------------------------------------- 1 | 19 | 117 | 121 | -------------------------------------------------------------------------------- /pages/shop/_collection/products/_product.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 121 | 122 | 149 | -------------------------------------------------------------------------------- /gql/queries/Shopify.gql: -------------------------------------------------------------------------------- 1 | #import "~/gql/fragments/Shopify.gql" 2 | 3 | # query Shop { 4 | # shop { 5 | # name 6 | # primaryDomain { 7 | # url 8 | # host 9 | # } 10 | # } 11 | # } 12 | 13 | # query ProductById($id: ID!) { 14 | # node(id: $id) { 15 | # id 16 | # ... on Product { 17 | # title 18 | # descriptionHtml 19 | # variants(first: 250) { 20 | # edges { 21 | # node { 22 | # price 23 | # title 24 | # availableForSale 25 | # id 26 | # vendor 27 | # } 28 | # } 29 | # } 30 | # } 31 | # } 32 | # } 33 | # 34 | # query Products { 35 | # products(first: 99) { 36 | # edges { 37 | # node { 38 | # id 39 | # title 40 | # } 41 | # } 42 | # } 43 | # } 44 | 45 | # SEE https://help.shopify.com/en/api/storefront-api/reference/queryroot#productbyhandle-2019-07 46 | query ProductByHandle($handle: String!) { 47 | productByHandle(handle: $handle) { 48 | id 49 | title 50 | handle 51 | productType 52 | tags 53 | descriptionHtml 54 | availableForSale 55 | vendor 56 | images(first: 5) { 57 | edges { 58 | node { 59 | ...productImage 60 | } 61 | } 62 | } 63 | collections(first: 1) { 64 | edges { 65 | node { 66 | handle 67 | title 68 | } 69 | } 70 | } 71 | metafields(first: 5) { 72 | edges { 73 | node { 74 | description 75 | key 76 | value 77 | namespace 78 | id 79 | } 80 | } 81 | } 82 | priceRange { 83 | ...priceRange 84 | } 85 | variants(first: 10) { 86 | edges { 87 | node { 88 | # metafields 89 | id 90 | title 91 | availableForSale 92 | compareAtPriceV2 { 93 | amount 94 | currencyCode 95 | } 96 | priceV2 { 97 | amount 98 | currencyCode 99 | } 100 | image { 101 | ...productImage 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | 109 | #SEE https://help.shopify.com/en/api/storefront-api/reference/queryroot#collections-2019-07 110 | query Collections($first: Int) { 111 | collections(first: $first) { 112 | edges { 113 | node { 114 | title 115 | id 116 | handle 117 | } 118 | } 119 | } 120 | } 121 | 122 | # SEE https://help.shopify.com/en/api/storefront-api/reference/queryroot#collectionbyhandle-2019-07 123 | query CollectionByHandle($handle: String!) { 124 | collectionByHandle(handle: $handle) { 125 | id 126 | title 127 | handle 128 | image { 129 | ...productImage 130 | } 131 | products(first: 25) { 132 | edges { 133 | node { 134 | title 135 | id 136 | handle 137 | availableForSale 138 | description(truncateAt: 30) 139 | images(first: 5) { 140 | edges { 141 | node { 142 | ...productBlockImage 143 | } 144 | } 145 | } 146 | priceRange { 147 | ...priceRange 148 | } 149 | } 150 | } 151 | } 152 | } 153 | } 154 | 155 | # SEE https://help.shopify.com/en/api/storefront-api/reference/interface/node 156 | query CheckoutById($checkoutId: ID!) { 157 | node(id: $checkoutId) { 158 | ... on Checkout { 159 | ...checkout 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /store/shopify.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | import _get from "lodash/get" 3 | import _findIndex from "lodash/findIndex" 4 | import _sumBy from "lodash/sumBy" 5 | import _cloneDeep from "lodash/cloneDeep" 6 | 7 | // GQL queries and mutations 8 | import { CheckoutCreate, ReplaceCheckout } from "~/gql/mutations/Shopify.gql" 9 | import { CheckoutById } from "~/gql/queries/Shopify.gql" 10 | 11 | // Helper utlities 12 | import { 13 | stripLineItems, 14 | simplifyLineItems, 15 | formatMoney 16 | } from "~/utils/Shopify.js" 17 | 18 | // Define State defaults 19 | export const state = () => ({ 20 | loading: 0, 21 | checkout: { 22 | id: "", 23 | webUrl: "", 24 | completedAt: null 25 | }, 26 | cart: { 27 | count: 0, 28 | subTotal: {}, 29 | lineItems: [] 30 | }, 31 | expire: 24 * 60, // 60 days 32 | version: 1 33 | }) 34 | 35 | // Define mutations 36 | export const mutations = { 37 | SET_CHECKOUT(state, checkout = {}) { 38 | state.checkout = checkout 39 | 40 | // Set line items and totals 41 | let lineItems = stripLineItems(checkout.lineItems) 42 | Vue.set(state.cart, "subTotal", checkout.subtotalPriceV2) 43 | Vue.set(state.cart, "lineItems", lineItems) 44 | 45 | // Set cart count 46 | let totalQty = _sumBy(lineItems, "quantity") 47 | Vue.set(state.cart, "count", totalQty) 48 | }, 49 | START_LOADING(state) { 50 | state.loading += 1 51 | }, 52 | FINISH_LOADING(state) { 53 | // Don't let this go below zero, so it can't get stuck in loading 54 | if (state.loading > 0) { 55 | state.loading -= 1 56 | } 57 | }, 58 | CLEAR_CHECKOUT(state) { 59 | state.loading = 0 60 | 61 | Vue.set(state, "checkout", { 62 | id: "", 63 | webUrl: "", 64 | completedAt: null 65 | }) 66 | 67 | Vue.set(state, "cart", { 68 | count: 0, 69 | subTotal: {}, 70 | lineItems: [] 71 | }) 72 | } 73 | } 74 | 75 | // Define actions 76 | export const actions = { 77 | // Add a product to your cart/checkout. 78 | // This will create a checkout if you don't have one. 79 | async ADD_TO_CART({ state, commit, dispatch }, { variantId, quantity }) { 80 | commit("START_LOADING") 81 | let client = this.app.apolloProvider.clients.shopify 82 | 83 | if (!state.checkout.id) { 84 | // If we don't have a checkout ID, create one (which also adds variant to cart) 85 | await dispatch("CREATE_CHECKOUT", { variantId, quantity }) 86 | } else { 87 | // NOTE Shopify API doesn't allow updating a checkout, only replacing all lineItems. 88 | 89 | // Deep clone all lineItems to avoid mutating store by reference 90 | // SEE https://www.samanthaming.com/tidbits/35-es6-way-to-clone-an-array 91 | const lineItems = _cloneDeep(state.cart.lineItems) 92 | 93 | // See if variantId is already in cart 94 | let currentVariantIndex = _findIndex(lineItems, obj => { 95 | return (obj.variant.id = variantId) 96 | }) 97 | 98 | if (currentVariantIndex >= 0) { 99 | // Update exsisting variant quantity 100 | lineItems[currentVariantIndex].quantity += quantity 101 | } else { 102 | // Add new variant to cart 103 | lineItems.push({ 104 | variant: { id: variantId }, 105 | quantity: quantity 106 | }) 107 | } 108 | 109 | // Now replace the Checkout 110 | await dispatch("REPLACE_CHECKOUT", lineItems) 111 | } 112 | 113 | commit("FINISH_LOADING") 114 | }, 115 | 116 | // Creates a checkout in Shopify, and then stores the results as checkout and cart 117 | async CREATE_CHECKOUT({ state, commit }, { variantId, quantity }) { 118 | commit("START_LOADING") 119 | let client = this.app.apolloProvider.clients.shopify 120 | 121 | const checkout = await client.mutate({ 122 | mutation: CheckoutCreate, 123 | variables: { 124 | variantId: variantId, 125 | quantity: quantity 126 | } 127 | }) 128 | 129 | commit( 130 | "SET_CHECKOUT", 131 | _get(checkout, "data.checkoutCreate.checkout", {}) 132 | ) 133 | 134 | commit("FINISH_LOADING") 135 | }, 136 | 137 | // This allows you to set quantity of multple line items at once. Useful for cart pages. 138 | // To remove a product, set its quantity to 0 139 | async SET_QUANTITY({ state, dispatch }, lineItems) { 140 | // Accept a single line item, or multple in an array 141 | if (typeof lineItems === "object") { 142 | lineItems = [lineItems] 143 | } 144 | 145 | await dispatch("REPLACE_CHECKOUT", lineItems) 146 | }, 147 | 148 | // This replaces the enitre checout, then stores the updated checkout and cart data 149 | // The Vuex cart is only updated from this action 150 | async REPLACE_CHECKOUT({ state, commit }, lineItems = []) { 151 | commit("START_LOADING") 152 | 153 | let client = this.app.apolloProvider.clients.shopify 154 | lineItems = simplifyLineItems(lineItems) 155 | 156 | // Remove any line items that have quantity set to 0 157 | lineItems = lineItems.filter(obj => obj.quantity > 0) 158 | 159 | const checkout = await client.mutate({ 160 | mutation: ReplaceCheckout, 161 | variables: { 162 | checkoutId: state.checkout.id, 163 | lineItems: lineItems 164 | } 165 | }) 166 | 167 | commit( 168 | "SET_CHECKOUT", 169 | _get(checkout, "data.checkoutLineItemsReplace.checkout", {}) 170 | ) 171 | 172 | commit("FINISH_LOADING") 173 | }, 174 | 175 | // This is used to make sure the current checkout hasn't completed. 176 | // It's a good idea to run this on Layout mounted() 177 | async GET_CHECKOUT({ state, commit }) { 178 | commit("START_LOADING") 179 | let client = this.app.apolloProvider.clients.shopify 180 | 181 | if (state.checkout.completedAt) { 182 | commit("CLEAR_CHECKOUT") 183 | } else if (state.checkout.id) { 184 | const response = await client.query({ 185 | query: CheckoutById, 186 | variables: { 187 | checkoutId: state.checkout.id 188 | } 189 | }) 190 | 191 | // Check that this checkout wasn't completed already 192 | const checkout = _get(response, "data.node", {}) 193 | 194 | if (checkout.completedAt) { 195 | commit("CLEAR_CHECKOUT") 196 | } else { 197 | commit("SET_CHECKOUT", checkout) 198 | } 199 | } else { 200 | commit("CLEAR_CHECKOUT") 201 | } 202 | 203 | commit("FINISH_LOADING") 204 | } 205 | } 206 | 207 | // Define some getters here 208 | export const getters = { 209 | cartSubTotal: state => { 210 | let output = "" 211 | if (state.cart.subTotal.amount) { 212 | output = formatMoney( 213 | state.cart.subTotal.amount, 214 | state.cart.subTotal.currencyCode 215 | ) 216 | } 217 | 218 | return output 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /components/ResponsiveImage.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 244 | 245 | 284 | --------------------------------------------------------------------------------