├── .gitignore ├── LICENSE.txt ├── README.md ├── babel.config.js ├── config.js ├── environments.js ├── firebase.json ├── package.json ├── pre-build.js ├── public ├── favicon.ico └── og.jpeg └── src ├── App.vue ├── assets └── scss │ ├── global │ ├── _animations.scss │ ├── _global.scss │ ├── _mixins.scss │ ├── _normalize.scss │ ├── _typography.scss │ └── _variables.scss │ ├── partials │ ├── _cart-button.scss │ ├── _cart.scss │ ├── _product-detail.scss │ └── _product-list.scss │ └── styles.scss ├── components ├── Cart │ ├── Cart.html │ ├── Cart.js │ └── Cart.vue ├── CartButton │ ├── CartButton.html │ ├── CartButton.js │ └── CartButton.vue ├── ProductDetail │ ├── ProductDetail.html │ ├── ProductDetail.js │ └── ProductDetail.vue └── ProductList │ ├── ProductList.html │ ├── ProductList.js │ └── ProductList.vue ├── index.pug ├── main.js └── services ├── GraphSQL.js └── ShopifyClient.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | package-lock.json 4 | /dist 5 | /public/index.html 6 | 7 | # Log files 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Editor directories and files 13 | .idea 14 | .vscode 15 | *.suo 16 | *.ntvs* 17 | *.njsproj 18 | *.sln 19 | *.sw* 20 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 The Young Astronauts Corp. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included 14 | in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VueShopify 2 | A Vue.js application to interface with Shopify through the Storefront API. 3 | 4 | ## Getting Started 5 | 6 | ### Installing and Running Locally 7 | Edit the following settings in `config.js` 8 | * `shopifyDomain` 9 | * `shopifyToken` 10 | 11 | Then in your terminal, run the following commands 12 | ``` 13 | $ npm install 14 | $ npm install 15 | $ npm run serve 16 | ``` 17 | Finally, visit [http://localhost:8080/](http://localhost:8080/) in your browser. 18 | 19 | ## Building For Production 20 | ``` 21 | $ npm run build 22 | ``` 23 | 24 | ## Configuration Details 25 | #### `config.js` 26 | ``` 27 | shopifyDomain: "YOUR_SHOPIFY_STORE_NAME.myshopify.com", // required 28 | 29 | shopifyToken: "SHOPIFY_STOREFRONT_TOKEN", // required 30 | 31 | collectionHandle: "SHOPIFY_COLLECTION_HANDLE", // optional, if not set it i will pull all products 32 | 33 | localStorageKey: "FOO_BAR", // used as a key for local storage to remember a user's checkout ID 34 | 35 | showUnavailableProducts: false, // if true, it will show products that are sold out 36 | 37 | productListColumns: 3, // how many columns of products to show 38 | 39 | productListColumnsMobile: 1, // how many columns of products to show on mobile 40 | 41 | loadingColor: '#41b883', // color of the loading icon 42 | 43 | googleAnalyticsId: 'UA-XXXXXXX-XX', // if unset, Google Analytics tracking will not fire 44 | ``` 45 | 46 | #### `environments.js` 47 | 48 | Since this is a SPA (single page application) there are certain SEO/OpenGraph settings that need to be configured and compiled outside of the Vue.js application so web crawlers such as Facebook & Twitter can obtain the relevant data. 49 | 50 | This file is used when rendering index.pug during the pre-build.js script (which is run when serving and building). 51 | 52 | The default settings can be overriden by multiple environments as you will see in the file. 53 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@vue/app'] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | shopifyDomain: "YOUR_SHOPIFY_STORE_NAME.myshopify.com", 3 | shopifyToken: "SHOPIFY_STOREFRONT_TOKEN", 4 | collectionHandle: null, 5 | localStorageKey: "vue-shopify", 6 | showUnavailableProducts: true, 7 | productListColumns: 3, 8 | productListColumnsMobile: 1, 9 | loadingColor: '#41b883', 10 | googleAnalyticsId: null, 11 | } -------------------------------------------------------------------------------- /environments.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | defaults: { 3 | title: "VueShopify", 4 | url: "https://www.MY-DOMAIN.com/", 5 | image: 'og.jpeg', 6 | description: '', 7 | keywords: '', 8 | twitterCreator: '', 9 | }, 10 | dev: { 11 | url: "http://localhost:8080/", 12 | title: "[DEV] VueShopify", 13 | 14 | }, 15 | prod: { 16 | url: "https://www.MY-DOMAIN.com/", 17 | } 18 | } -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "dist", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-shopify", 3 | "version": "1.0.0", 4 | "author": "The Young Astronauts Corp.", 5 | "description": "A Vue.js application to interface with Shopify through the Storefront API.", 6 | "private": true, 7 | "scripts": { 8 | "serve": "node pre-build.js --dev && vue-cli-service serve", 9 | "build": "node pre-build.js --prod && vue-cli-service build", 10 | "lint": "vue-cli-service lint" 11 | }, 12 | "dependencies": { 13 | "@fortawesome/fontawesome-svg-core": "^1.2.8", 14 | "@fortawesome/free-brands-svg-icons": "^5.5.0", 15 | "@fortawesome/free-solid-svg-icons": "^5.5.0", 16 | "@fortawesome/vue-fontawesome": "^0.1.3", 17 | "axios": "^0.18.0", 18 | "vue": "^2.5.17", 19 | "vue-analytics": "^5.16.1", 20 | "vue-loading-spinner": "^1.0.11", 21 | "vue-router": "^3.0.2" 22 | }, 23 | "devDependencies": { 24 | "@vue/cli-plugin-babel": "^3.2.0", 25 | "@vue/cli-plugin-eslint": "^3.2.0", 26 | "@vue/cli-service": "^3.2.0", 27 | "babel-eslint": "^10.0.1", 28 | "eslint": "^5.8.0", 29 | "eslint-plugin-vue": "^5.0.0-0", 30 | "html-webpack-plugin": "^3.2.0", 31 | "node-sass": "^4.9.0", 32 | "pug": "^2.0.3", 33 | "sass-loader": "^7.0.1", 34 | "vue-template-compiler": "^2.5.17" 35 | }, 36 | "eslintConfig": { 37 | "root": true, 38 | "env": { 39 | "node": true 40 | }, 41 | "extends": [ 42 | "plugin:vue/essential", 43 | "eslint:recommended" 44 | ], 45 | "rules": { 46 | "no-console": "off", 47 | "no-useless-escape": "off" 48 | }, 49 | "parserOptions": { 50 | "parser": "babel-eslint" 51 | } 52 | }, 53 | "postcss": { 54 | "plugins": { 55 | "autoprefixer": {} 56 | } 57 | }, 58 | "browserslist": [ 59 | "> 1%", 60 | "last 2 versions", 61 | "not ie <= 8" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /pre-build.js: -------------------------------------------------------------------------------- 1 | const pug = require('pug'); 2 | const environments = require('./environments.js') 3 | const fs = require('fs'); 4 | 5 | const compiledFunction = pug.compileFile('./src/index.pug', { 6 | pretty: true 7 | }); 8 | 9 | const env = process.argv[2].replace('--', '') || 'dev'; 10 | const defaults = environments.defaults; 11 | const overrides = environments[env]; 12 | const config = Object.assign(defaults, overrides) 13 | 14 | const html = compiledFunction(config) 15 | 16 | fs.writeFile("./public/index.html", html, function(err) { 17 | if(err) { 18 | return console.log(err); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylersavery/vue-shopify/cd13c22be8e31dae832feff1975aa7173dd440b3/public/favicon.ico -------------------------------------------------------------------------------- /public/og.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylersavery/vue-shopify/cd13c22be8e31dae832feff1975aa7173dd440b3/public/og.jpeg -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /src/assets/scss/global/_animations.scss: -------------------------------------------------------------------------------- 1 | /* fade transition */ 2 | 3 | .fade-enter-active, .fade-leave-active { 4 | transition: opacity .5s; 5 | } 6 | .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ { 7 | opacity: 0; 8 | } 9 | 10 | 11 | /* bounce transition */ 12 | 13 | .bounce-enter-active { 14 | animation: bounce-in .5s; 15 | } 16 | .bounce-leave-active { 17 | animation: bounce-in .5s reverse; 18 | } 19 | @keyframes bounce-in { 20 | 0% { 21 | transform: scale(0); 22 | } 23 | 50% { 24 | transform: scale(1.5); 25 | } 26 | 100% { 27 | transform: scale(1); 28 | } 29 | } 30 | 31 | 32 | /* create other transitions here */ -------------------------------------------------------------------------------- /src/assets/scss/global/_global.scss: -------------------------------------------------------------------------------- 1 | html, body { 2 | overflow: hidden; 3 | @include fontSanSerif; 4 | } 5 | 6 | h1, h2, h3, h4, h5, h6 { 7 | @include fontSerif; 8 | } 9 | 10 | #app { 11 | width: 100%; 12 | height: 100vh; 13 | display: block; 14 | position: relative; 15 | overflow: auto; 16 | padding: $pagePadding; 17 | 18 | &.scroll-disabled { 19 | overflow: hidden; 20 | } 21 | 22 | &.click-disabled { 23 | pointer-events: none; 24 | } 25 | 26 | .spinner { 27 | position: fixed; 28 | pointer-events: none; 29 | left: 50%; 30 | top:50%; 31 | transform: translate(-50%, -50%) 32 | } 33 | 34 | .grid { 35 | @include grid; 36 | } 37 | 38 | .btn { 39 | display: inline-block; 40 | text-decoration: none; 41 | background-color: $colorFg; 42 | color: $colorBg; 43 | position: relative; 44 | 45 | padding: 8px 12px; 46 | font-weight: bold; 47 | margin: 0 8px 0 0; 48 | border: 1px solid #000; 49 | transition: all .2s; 50 | border-radius: 3px; 51 | cursor: pointer; 52 | &:hover, &.active { 53 | color: $colorFg; 54 | background-color: $colorBg; 55 | } 56 | 57 | &.active { 58 | cursor: default; 59 | } 60 | 61 | &.btn-small { 62 | font-size: 14px; 63 | padding: 4px 6px; 64 | margin: 2px 4px 0 0; 65 | } 66 | } 67 | 68 | .fixed-container { 69 | position: fixed; 70 | width: 100%; 71 | height: 100%; 72 | top: 0; 73 | left:0; 74 | background-color :$colorBg; 75 | overflow: auto; 76 | padding: $pagePadding; 77 | 78 | .close-fixed-container { 79 | position: absolute; 80 | font-size: 28px; 81 | top: 0px; 82 | left: 8px; 83 | cursor: pointer; 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/assets/scss/global/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin phone { 2 | @media (max-width: 599px) { @content; } 3 | } 4 | 5 | @mixin grid { 6 | display: grid; 7 | column-gap: 10px; 8 | row-gap: 10px; 9 | &.grid-1 { 10 | grid-template-columns: repeat(1, 1fr); 11 | } 12 | &.grid-2 { 13 | grid-template-columns: repeat(2, 1fr); 14 | } 15 | &.grid-3 { 16 | grid-template-columns: repeat(3, 1fr); 17 | } 18 | &.grid-4 { 19 | grid-template-columns: repeat(4, 1fr); 20 | } 21 | &.grid-5 { 22 | grid-template-columns: repeat(5, 1fr); 23 | } 24 | &.grid-6 { 25 | grid-template-columns: repeat(6, 1fr); 26 | } 27 | 28 | @include phone { 29 | &.grid-1-mobile { 30 | grid-template-columns: repeat(1, 1fr); 31 | } 32 | &.grid-2-mobile { 33 | grid-template-columns: repeat(2, 1fr); 34 | } 35 | &.grid-3-mobile { 36 | grid-template-columns: repeat(3, 1fr); 37 | } 38 | &.grid-4-mobile { 39 | grid-template-columns: repeat(4, 1fr); 40 | } 41 | &.grid-5-mobile { 42 | grid-template-columns: repeat(5, 1fr); 43 | } 44 | &.grid-6-mobile { 45 | grid-template-columns: repeat(6, 1fr); 46 | } 47 | } 48 | } 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/assets/scss/global/_normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | box-sizing: border-box; 15 | } 16 | 17 | *, *:before, *:after { 18 | box-sizing: inherit; 19 | } 20 | 21 | /* Sections 22 | ========================================================================== */ 23 | 24 | /** 25 | * Remove the margin in all browsers. 26 | */ 27 | 28 | body { 29 | margin: 0; 30 | } 31 | 32 | /** 33 | * Render the `main` element consistently in IE. 34 | */ 35 | 36 | main { 37 | display: block; 38 | } 39 | 40 | /** 41 | * Correct the font size and margin on `h1` elements within `section` and 42 | * `article` contexts in Chrome, Firefox, and Safari. 43 | */ 44 | 45 | h1 { 46 | font-size: 2em; 47 | margin: 0.67em 0; 48 | } 49 | 50 | /* Grouping content 51 | ========================================================================== */ 52 | 53 | /** 54 | * 1. Add the correct box sizing in Firefox. 55 | * 2. Show the overflow in Edge and IE. 56 | */ 57 | 58 | hr { 59 | box-sizing: content-box; /* 1 */ 60 | height: 0; /* 1 */ 61 | overflow: visible; /* 2 */ 62 | } 63 | 64 | /** 65 | * 1. Correct the inheritance and scaling of font size in all browsers. 66 | * 2. Correct the odd `em` font sizing in all browsers. 67 | */ 68 | 69 | pre { 70 | font-family: monospace, monospace; /* 1 */ 71 | font-size: 1em; /* 2 */ 72 | } 73 | 74 | /* Text-level semantics 75 | ========================================================================== */ 76 | 77 | /** 78 | * Remove the gray background on active links in IE 10. 79 | */ 80 | 81 | a { 82 | background-color: transparent; 83 | } 84 | 85 | /** 86 | * 1. Remove the bottom border in Chrome 57- 87 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 88 | */ 89 | 90 | abbr[title] { 91 | border-bottom: none; /* 1 */ 92 | text-decoration: underline; /* 2 */ 93 | text-decoration: underline dotted; /* 2 */ 94 | } 95 | 96 | /** 97 | * Add the correct font weight in Chrome, Edge, and Safari. 98 | */ 99 | 100 | b, 101 | strong { 102 | font-weight: bolder; 103 | } 104 | 105 | /** 106 | * 1. Correct the inheritance and scaling of font size in all browsers. 107 | * 2. Correct the odd `em` font sizing in all browsers. 108 | */ 109 | 110 | code, 111 | kbd, 112 | samp { 113 | font-family: monospace, monospace; /* 1 */ 114 | font-size: 1em; /* 2 */ 115 | } 116 | 117 | /** 118 | * Add the correct font size in all browsers. 119 | */ 120 | 121 | small { 122 | font-size: 80%; 123 | } 124 | 125 | /** 126 | * Prevent `sub` and `sup` elements from affecting the line height in 127 | * all browsers. 128 | */ 129 | 130 | sub, 131 | sup { 132 | font-size: 75%; 133 | line-height: 0; 134 | position: relative; 135 | vertical-align: baseline; 136 | } 137 | 138 | sub { 139 | bottom: -0.25em; 140 | } 141 | 142 | sup { 143 | top: -0.5em; 144 | } 145 | 146 | /* Embedded content 147 | ========================================================================== */ 148 | 149 | /** 150 | * Remove the border on images inside links in IE 10. 151 | */ 152 | 153 | img { 154 | border-style: none; 155 | } 156 | 157 | /* Forms 158 | ========================================================================== */ 159 | 160 | /** 161 | * 1. Change the font styles in all browsers. 162 | * 2. Remove the margin in Firefox and Safari. 163 | */ 164 | 165 | button, 166 | input, 167 | optgroup, 168 | select, 169 | textarea { 170 | font-family: inherit; /* 1 */ 171 | font-size: 100%; /* 1 */ 172 | line-height: 1.15; /* 1 */ 173 | margin: 0; /* 2 */ 174 | } 175 | 176 | /** 177 | * Show the overflow in IE. 178 | * 1. Show the overflow in Edge. 179 | */ 180 | 181 | button, 182 | input { /* 1 */ 183 | overflow: visible; 184 | } 185 | 186 | /** 187 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 188 | * 1. Remove the inheritance of text transform in Firefox. 189 | */ 190 | 191 | button, 192 | select { /* 1 */ 193 | text-transform: none; 194 | } 195 | 196 | /** 197 | * Correct the inability to style clickable types in iOS and Safari. 198 | */ 199 | 200 | button, 201 | [type="button"], 202 | [type="reset"], 203 | [type="submit"] { 204 | -webkit-appearance: button; 205 | } 206 | 207 | /** 208 | * Remove the inner border and padding in Firefox. 209 | */ 210 | 211 | button::-moz-focus-inner, 212 | [type="button"]::-moz-focus-inner, 213 | [type="reset"]::-moz-focus-inner, 214 | [type="submit"]::-moz-focus-inner { 215 | border-style: none; 216 | padding: 0; 217 | } 218 | 219 | /** 220 | * Restore the focus styles unset by the previous rule. 221 | */ 222 | 223 | button:-moz-focusring, 224 | [type="button"]:-moz-focusring, 225 | [type="reset"]:-moz-focusring, 226 | [type="submit"]:-moz-focusring { 227 | outline: 1px dotted ButtonText; 228 | } 229 | 230 | /** 231 | * Correct the padding in Firefox. 232 | */ 233 | 234 | fieldset { 235 | padding: 0.35em 0.75em 0.625em; 236 | } 237 | 238 | /** 239 | * 1. Correct the text wrapping in Edge and IE. 240 | * 2. Correct the color inheritance from `fieldset` elements in IE. 241 | * 3. Remove the padding so developers are not caught out when they zero out 242 | * `fieldset` elements in all browsers. 243 | */ 244 | 245 | legend { 246 | box-sizing: border-box; /* 1 */ 247 | color: inherit; /* 2 */ 248 | display: table; /* 1 */ 249 | max-width: 100%; /* 1 */ 250 | padding: 0; /* 3 */ 251 | white-space: normal; /* 1 */ 252 | } 253 | 254 | /** 255 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 256 | */ 257 | 258 | progress { 259 | vertical-align: baseline; 260 | } 261 | 262 | /** 263 | * Remove the default vertical scrollbar in IE 10+. 264 | */ 265 | 266 | textarea { 267 | overflow: auto; 268 | } 269 | 270 | /** 271 | * 1. Add the correct box sizing in IE 10. 272 | * 2. Remove the padding in IE 10. 273 | */ 274 | 275 | [type="checkbox"], 276 | [type="radio"] { 277 | box-sizing: border-box; /* 1 */ 278 | padding: 0; /* 2 */ 279 | } 280 | 281 | /** 282 | * Correct the cursor style of increment and decrement buttons in Chrome. 283 | */ 284 | 285 | [type="number"]::-webkit-inner-spin-button, 286 | [type="number"]::-webkit-outer-spin-button { 287 | height: auto; 288 | } 289 | 290 | /** 291 | * 1. Correct the odd appearance in Chrome and Safari. 292 | * 2. Correct the outline style in Safari. 293 | */ 294 | 295 | [type="search"] { 296 | -webkit-appearance: textfield; /* 1 */ 297 | outline-offset: -2px; /* 2 */ 298 | } 299 | 300 | /** 301 | * Remove the inner padding in Chrome and Safari on macOS. 302 | */ 303 | 304 | [type="search"]::-webkit-search-decoration { 305 | -webkit-appearance: none; 306 | } 307 | 308 | /** 309 | * 1. Correct the inability to style clickable types in iOS and Safari. 310 | * 2. Change font properties to `inherit` in Safari. 311 | */ 312 | 313 | ::-webkit-file-upload-button { 314 | -webkit-appearance: button; /* 1 */ 315 | font: inherit; /* 2 */ 316 | } 317 | 318 | /* Interactive 319 | ========================================================================== */ 320 | 321 | /* 322 | * Add the correct display in Edge, IE 10+, and Firefox. 323 | */ 324 | 325 | details { 326 | display: block; 327 | } 328 | 329 | /* 330 | * Add the correct display in all browsers. 331 | */ 332 | 333 | summary { 334 | display: list-item; 335 | } 336 | 337 | /* Misc 338 | ========================================================================== */ 339 | 340 | /** 341 | * Add the correct display in IE 10+. 342 | */ 343 | 344 | template { 345 | display: none; 346 | } 347 | 348 | /** 349 | * Add the correct display in IE 10. 350 | */ 351 | 352 | [hidden] { 353 | display: none; 354 | } 355 | 356 | -------------------------------------------------------------------------------- /src/assets/scss/global/_typography.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto+Slab:400,700|Roboto:400,700'); 2 | 3 | @mixin fontSerif{ 4 | font-family: 'Roboto Slab', serif; 5 | } 6 | 7 | @mixin fontSanSerif{ 8 | font-family: 'Roboto', sans-serif; 9 | } -------------------------------------------------------------------------------- /src/assets/scss/global/_variables.scss: -------------------------------------------------------------------------------- 1 | $colorBg: #fff; 2 | $colorFg: #000; 3 | 4 | $pagePadding: 12px; -------------------------------------------------------------------------------- /src/assets/scss/partials/_cart-button.scss: -------------------------------------------------------------------------------- 1 | .cart-btn{ 2 | position: fixed !important; 3 | top: $pagePadding; 4 | right: $pagePadding; 5 | } -------------------------------------------------------------------------------- /src/assets/scss/partials/_cart.scss: -------------------------------------------------------------------------------- 1 | .cart { 2 | position: relative; 3 | 4 | .line-item{ 5 | display: block; 6 | position: relative; 7 | width: 100%; 8 | min-height: 300px; 9 | border-bottom: 2px solid $colorFg; 10 | 11 | .grid { 12 | align-items: center; 13 | } 14 | 15 | .cell { 16 | .image { 17 | text-align: center; 18 | padding: 10px 0; 19 | img{ 20 | height: 280px; 21 | } 22 | } 23 | 24 | .title { 25 | font-size: 24px; 26 | @include fontSerif; 27 | font-weight: bold; 28 | @include phone { 29 | text-align: center; 30 | } 31 | } 32 | 33 | .variant-title { 34 | padding-top: 12px; 35 | font-size: 16px; 36 | font-weight: bold; 37 | @include phone { 38 | text-align: center; 39 | } 40 | } 41 | 42 | .price { 43 | font-size: 20px; 44 | text-align: right; 45 | span { 46 | font-weight: bold; 47 | } 48 | @include phone { 49 | text-align: center; 50 | } 51 | } 52 | 53 | .quantity { 54 | padding: 16px 0; 55 | text-align: right; 56 | font-size: 20px; 57 | 58 | span { 59 | margin-right: 10px; 60 | font-weight: bold; 61 | } 62 | a{ 63 | top: -2px; 64 | } 65 | @include phone { 66 | text-align: center; 67 | } 68 | } 69 | 70 | .total { 71 | text-align: right; 72 | font-size: 20px; 73 | span { 74 | font-weight: bold; 75 | } 76 | @include phone { 77 | text-align: center; 78 | } 79 | } 80 | 81 | .remove{ 82 | text-align: right; 83 | padding: 10px 0; 84 | a.btn { 85 | margin-right: 0 !important; 86 | } 87 | @include phone { 88 | text-align: center; 89 | } 90 | } 91 | } 92 | } 93 | 94 | .cart-footer { 95 | padding: 24px 0; 96 | text-align: right; 97 | .total { 98 | font-size: 24px; 99 | padding: 12px 0; 100 | span { 101 | font-weight: bold; 102 | } 103 | } 104 | a.btn { 105 | margin-right: 0 !important; 106 | } 107 | 108 | @include phone { 109 | text-align: center; 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /src/assets/scss/partials/_product-detail.scss: -------------------------------------------------------------------------------- 1 | .product-detail { 2 | position: relative; 3 | 4 | width: 100%; 5 | padding: $pagePadding; 6 | 7 | @include grid; 8 | align-items: center; 9 | 10 | .product-image { 11 | display: block; 12 | img{ 13 | width: 100%; 14 | } 15 | } 16 | 17 | .thumbnails { 18 | list-style: none; 19 | li { 20 | position: relative; 21 | display: inline-block; 22 | width: 64px; 23 | height: 64px; 24 | background-size: contain; 25 | background-position: center center; 26 | background-repeat: no-repeat; 27 | cursor: pointer; 28 | margin-right: 8px; 29 | 30 | &.selected { 31 | border:1px solid #000; 32 | cursor: default; 33 | } 34 | 35 | 36 | 37 | } 38 | } 39 | 40 | ul.product-variants { 41 | list-style: none; 42 | padding: 0; 43 | 44 | li { 45 | &.sold-out { 46 | pointer-events: none; 47 | opacity: 0.25; 48 | } 49 | } 50 | } 51 | 52 | .product-price { 53 | font-size: 24px; 54 | padding: 8px 0; 55 | } 56 | } -------------------------------------------------------------------------------- /src/assets/scss/partials/_product-list.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .products { 4 | position: relative; 5 | width: 100%; 6 | 7 | @include grid; 8 | 9 | .product { 10 | cursor: pointer; 11 | 12 | &.sold-out { 13 | pointer-events: none; 14 | opacity: .5; 15 | } 16 | 17 | .product-image { 18 | display: block; 19 | img { 20 | width: 100%; 21 | } 22 | } 23 | 24 | .product-title { 25 | 26 | } 27 | .product-description { 28 | 29 | } 30 | .product-price { 31 | 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/assets/scss/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'global/normalize'; 2 | @import 'global/variables'; 3 | @import 'global/typography'; 4 | @import 'global/mixins'; 5 | @import 'global/animations'; 6 | @import 'global/global'; 7 | 8 | @import 'partials/product-list'; 9 | @import 'partials/product-detail'; 10 | @import 'partials/cart'; 11 | @import 'partials/cart-button'; -------------------------------------------------------------------------------- /src/components/Cart/Cart.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 |
8 |
9 | 10 |
11 |
12 |
13 |
{{lineItem.title}}
14 |
{{lineItem.variantTitle}}
15 |
16 |
17 |
Price: ${{lineItem.price}}
18 |
19 | Quantity: {{lineItem.quantity}} 20 | 21 | 22 |
23 |
Total: ${{lineItem.totalPrice}}
24 |
25 | Remove 26 |
27 |
28 |
29 |
30 | 31 | 36 | 37 |
38 | 39 |
40 | 41 |
42 |
43 | -------------------------------------------------------------------------------- /src/components/Cart/Cart.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'Cart', 3 | components: {}, 4 | props: { 5 | count: { 6 | default: 0 7 | }, 8 | lineItems: { 9 | default: [] 10 | }, 11 | checkoutUrl: String, 12 | totalPrice: String 13 | }, 14 | data() { 15 | return {} 16 | }, 17 | 18 | mounted() { 19 | console.log("lineItems") 20 | console.log(this.lineItems) 21 | 22 | }, 23 | methods: { 24 | 25 | remove(lineItem, event){ 26 | event.preventDefault(); 27 | this.$emit('remove-from-cart', lineItem.id) 28 | }, 29 | incrementQuantity(lineItem, event) { 30 | event.preventDefault(); 31 | let quantity = lineItem.quantity+1; 32 | this.$emit('update-quantity', lineItem.id, lineItem.variantId, quantity) 33 | }, 34 | decrementQuantity(lineItem, event){ 35 | event.preventDefault(); 36 | let quantity = lineItem.quantity-1; 37 | if(quantity < 1){ 38 | this.remove(lineItem, event) 39 | return 40 | } 41 | this.$emit('update-quantity', lineItem.id, lineItem.variantId, quantity) 42 | } 43 | 44 | }, 45 | 46 | } -------------------------------------------------------------------------------- /src/components/Cart/Cart.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/CartButton/CartButton.html: -------------------------------------------------------------------------------- 1 | View Cart ({{count}}) -------------------------------------------------------------------------------- /src/components/CartButton/CartButton.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'CartButton', 3 | components: {}, 4 | props: { 5 | count: { 6 | default: 0 7 | } 8 | }, 9 | data() { 10 | return {} 11 | }, 12 | 13 | mounted() { 14 | 15 | }, 16 | methods: { 17 | showCart(){ 18 | this.$emit('reveal-cart') 19 | } 20 | }, 21 | 22 | } -------------------------------------------------------------------------------- /src/components/CartButton/CartButton.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/ProductDetail/ProductDetail.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 |
7 | 8 |
9 | 10 |
    11 |
  • 12 |
13 | 14 |
15 |
16 |

{{product.title}}

17 |

{{product.description}}

18 |
    19 |
  • 20 | {{variant.title}} 21 |
  • 22 |
23 | 24 |
${{selectedVariant.price}}
25 | Add To Cart 26 |
27 | 28 |
29 |
30 | 31 |
32 |
33 | -------------------------------------------------------------------------------- /src/components/ProductDetail/ProductDetail.js: -------------------------------------------------------------------------------- 1 | import config from '../../../config.js'; 2 | import ShopifyClient from '../../services/ShopifyClient' 3 | 4 | export default { 5 | name: 'ProductDetail', 6 | components: {}, 7 | props: { 8 | product: Object, 9 | checkoutId: String 10 | }, 11 | data() { 12 | return { 13 | selectedVariant: null 14 | } 15 | }, 16 | 17 | mounted() { 18 | console.log("product", this.product) 19 | this.shopifyClient = new ShopifyClient(config.shopifyDomain, config.shopifyToken) 20 | this.selectedVariant = this.product.variants[0] 21 | }, 22 | 23 | methods: { 24 | selectVariant(variant, event) { 25 | event.preventDefault(); 26 | this.selectedVariant = variant 27 | console.log(this.selectedVariant) 28 | }, 29 | addToCart(variantId, event) { 30 | event.preventDefault(); 31 | 32 | this.shopifyClient.addToCart(variantId, this.checkoutId, successResponse => { 33 | console.log(successResponse) 34 | if(config.googleAnalyticsId){ 35 | this.$ga.event('cart', 'added', 'variantId', variantId) 36 | } 37 | this.$emit('cart-updated') 38 | }, errorResponse => { 39 | console.log(errorResponse) 40 | }) 41 | }, 42 | updateSelectedImage(image) { 43 | this.product.selectedImage = image 44 | } 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /src/components/ProductDetail/ProductDetail.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/ProductList/ProductList.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 |
7 | 8 |

{{product.title}}

9 |
${{product.price}}
10 |
11 |
-------------------------------------------------------------------------------- /src/components/ProductList/ProductList.js: -------------------------------------------------------------------------------- 1 | import config from '../../../config.js'; 2 | import ShopifyClient from '../../services/ShopifyClient' 3 | 4 | export default { 5 | name: 'ProductList', 6 | components: {}, 7 | props: { 8 | checkoutId: String 9 | }, 10 | data() { 11 | return { 12 | products: [], 13 | gridColumnsClass: 'grid-3', 14 | gridColumnsClassMobile: 'grid-1-mobile' 15 | } 16 | }, 17 | mounted() { 18 | this.$emit('reveal-loader') 19 | 20 | this.shopifyClient = new ShopifyClient(config.shopifyDomain, config.shopifyToken); 21 | 22 | if(config.collectionHandle) { 23 | this.shopifyClient.productsFromCollection(config.collectionHandle, products => { 24 | console.log(products) 25 | this.products = products 26 | this.$emit('hide-loader') 27 | }) 28 | } else { 29 | this.shopifyClient.allProducts(products => { 30 | console.log(products) 31 | this.products = products 32 | this.$emit('hide-loader') 33 | }) 34 | } 35 | 36 | if(config.productListColumns){ 37 | this.gridColumnsClass = 'grid-' + config.productListColumns 38 | } 39 | if(config.productListColumnsMobile){ 40 | this.gridColumnsClassMobile = 'grid-' + config.productListColumnsMobile + '-mobile' 41 | } 42 | 43 | }, 44 | methods: { 45 | 46 | showProductDetails(product, event){ 47 | event.preventDefault(); 48 | this.$emit('reveal-loader') 49 | let productId = product.id; 50 | 51 | this.shopifyClient.productDetails(productId, product => { 52 | console.log("Product", product); 53 | this.$emit('reveal-product-details', product) 54 | this.$emit('hide-loader') 55 | }, errorResponse => { 56 | console.log(errorResponse); 57 | }) 58 | }, 59 | 60 | addToCart(product, event){ 61 | event.preventDefault(); 62 | 63 | let variantId = product.variants[0].id; 64 | this.shopifyClient.addToCart(variantId, this.checkoutId, successResponse => { 65 | console.log(successResponse) 66 | }, errorResponse => { 67 | console.log(errorResponse) 68 | }) 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/components/ProductList/ProductList.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | head 4 | meta(charset='utf-8') 5 | meta(http-equiv='X-UA-Compatible', content='IE=edge') 6 | meta(name='viewport', content='width=device-width,initial-scale=1.0') 7 | meta(property="description" content=description) 8 | meta(property="keywords" content=keywords) 9 | 10 | link(rel='icon', href!='<%= BASE_URL %>favicon.ico') 11 | title #{title} 12 | 13 | meta(property="og:title" content=title) 14 | meta(property="og:site_name" content=title) 15 | meta(property="og:type" content="website") 16 | meta(property="og:url" content=url) 17 | meta(property="og:image" content!="<%= BASE_URL %>"+image) 18 | meta(property="og:description" content=description) 19 | 20 | meta(property="twitter:card" content="summary") 21 | meta(property="twitter:site" content=url) 22 | meta(property="twitter:title" content=title) 23 | meta(property="twitter:description" content=description) 24 | meta(property="twitter:creator" content="@"+twitterCreator) 25 | meta(property="twitter:image:src" content!="<%= BASE_URL %>"+image) 26 | 27 | body 28 | noscript 29 | strong 30 | | We're sorry but this site doesn't work properly without JavaScript enabled. Please enable it to continue. 31 | #app -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import VueAnalytics from 'vue-analytics' 4 | import { library } from '@fortawesome/fontawesome-svg-core' 5 | import { faTimes, faArrowUp, faArrowDown } from '@fortawesome/free-solid-svg-icons' 6 | import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' 7 | 8 | import config from '../config' 9 | 10 | library.add(faTimes, faArrowUp, faArrowDown) 11 | 12 | Vue.component('font-awesome-icon', FontAwesomeIcon) 13 | 14 | Vue.config.productionTip = false 15 | 16 | if(config.googleAnalyticsId){ 17 | Vue.use(VueAnalytics, { 18 | id: config.googleAnalyticsId 19 | }) 20 | } 21 | 22 | new Vue({ 23 | render: h => h(App), 24 | }).$mount('#app') 25 | -------------------------------------------------------------------------------- /src/services/GraphSQL.js: -------------------------------------------------------------------------------- 1 | const allCollectionsQuery = ` { 2 | shop { 3 | collections(first:20) { 4 | edges{ 5 | node { 6 | id 7 | handle 8 | title 9 | } 10 | } 11 | } 12 | } 13 | }`; 14 | 15 | const productsFromCollectionQuery = `query { 16 | shop { 17 | name 18 | description 19 | collectionByHandle(handle: "$collectionHandle") { 20 | products(first:20) { 21 | pageInfo { 22 | hasNextPage 23 | hasPreviousPage 24 | } 25 | edges { 26 | node { 27 | id 28 | title 29 | description 30 | availableForSale 31 | options { 32 | name 33 | values 34 | } 35 | variants(first: 250) { 36 | pageInfo { 37 | hasNextPage 38 | hasPreviousPage 39 | } 40 | edges { 41 | node { 42 | id 43 | title 44 | selectedOptions { 45 | name 46 | value 47 | } 48 | image { 49 | src 50 | } 51 | price 52 | } 53 | } 54 | } 55 | images(first: 250) { 56 | pageInfo { 57 | hasNextPage 58 | hasPreviousPage 59 | } 60 | edges { 61 | node { 62 | src 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | }`; 72 | 73 | const allProductsQuery = `query { 74 | shop { 75 | name 76 | description 77 | products(first:20) { 78 | pageInfo { 79 | hasNextPage 80 | hasPreviousPage 81 | } 82 | edges { 83 | node { 84 | id 85 | title 86 | description 87 | availableForSale 88 | options { 89 | name 90 | values 91 | } 92 | variants(first: 250) { 93 | pageInfo { 94 | hasNextPage 95 | hasPreviousPage 96 | } 97 | edges { 98 | node { 99 | id 100 | title 101 | selectedOptions { 102 | name 103 | value 104 | } 105 | image { 106 | src 107 | } 108 | price 109 | } 110 | } 111 | } 112 | images(first: 250) { 113 | pageInfo { 114 | hasNextPage 115 | hasPreviousPage 116 | } 117 | edges { 118 | node { 119 | src 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } 127 | }`; 128 | 129 | 130 | const createCheckoutIdQuery = `mutation { 131 | checkoutCreate(input: {}) { 132 | userErrors { 133 | message 134 | field 135 | } 136 | checkout { 137 | id 138 | } 139 | } 140 | }`; 141 | 142 | const addToCartQuery = `mutation { 143 | checkoutLineItemsAdd(lineItems: [{ variantId: "$variantId", quantity: 1 }], checkoutId: "$checkoutId") {, 144 | checkout { 145 | id 146 | lineItems(first:2) { 147 | edges { 148 | node { 149 | id 150 | title 151 | quantity 152 | } 153 | } 154 | } 155 | } 156 | } 157 | }`; 158 | 159 | const productDetailsQuery = `query { 160 | node(id: "$productId") { 161 | id 162 | ... on Product { 163 | id 164 | title 165 | description 166 | options { 167 | name 168 | values 169 | } 170 | variants(first: 250) { 171 | pageInfo { 172 | hasNextPage 173 | hasPreviousPage 174 | } 175 | edges { 176 | node { 177 | id 178 | title 179 | availableForSale 180 | selectedOptions { 181 | name 182 | value 183 | } 184 | image { 185 | src 186 | } 187 | price 188 | } 189 | } 190 | } 191 | images(first: 250) { 192 | pageInfo { 193 | hasNextPage 194 | hasPreviousPage 195 | } 196 | edges { 197 | node { 198 | src 199 | } 200 | } 201 | } 202 | } 203 | } 204 | }`; 205 | 206 | const cartQuery = `{ 207 | node(id: "$checkoutId") { 208 | ... on Checkout { 209 | webUrl 210 | subtotalPrice 211 | totalTax 212 | totalPrice 213 | lineItems (first:250) { 214 | pageInfo { 215 | hasNextPage 216 | hasPreviousPage 217 | } 218 | edges { 219 | node { 220 | id 221 | 222 | title 223 | variant { 224 | id 225 | title 226 | image { 227 | src 228 | } 229 | price 230 | } 231 | quantity 232 | } 233 | } 234 | } 235 | } 236 | } 237 | } 238 | `; 239 | 240 | const removeFromCartQuery = `mutation { 241 | checkoutLineItemsRemove(lineItemIds: [$lineItemId], checkoutId: "$checkoutId") {, 242 | userErrors { 243 | message 244 | field 245 | } 246 | checkout { 247 | id 248 | } 249 | } 250 | }`; 251 | 252 | const updateQuantityQuery = ` mutation { 253 | checkoutLineItemsUpdate(checkoutId: "$checkoutId", lineItems: [{ id: "$lineItemId", variantId: "$variantId", quantity: $quantity }]) { 254 | userErrors { 255 | message 256 | field 257 | } 258 | checkout { 259 | id 260 | } 261 | } 262 | }`; 263 | 264 | 265 | export default class GraphSql { 266 | 267 | construct() { } 268 | 269 | replaceAll(string, find, replace){ 270 | return string.replace( 271 | new RegExp(find.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"), "g"), 272 | replace 273 | ); 274 | } 275 | 276 | allProductsQuery() { 277 | return allProductsQuery; 278 | } 279 | 280 | productsFromCollectionQuery(collectionHandle) { 281 | let string = productsFromCollectionQuery 282 | string = this.replaceAll(string, '$collectionHandle', collectionHandle) 283 | 284 | return string 285 | } 286 | 287 | addToCartQuery(variantId, checkoutId) { 288 | let string = addToCartQuery; 289 | string = this.replaceAll(string, '$variantId', variantId); 290 | string = this.replaceAll(string, '$checkoutId', checkoutId); 291 | 292 | return string 293 | } 294 | 295 | createCheckoutIdQuery() { 296 | return createCheckoutIdQuery; 297 | } 298 | 299 | productDetailsQuery(productId) { 300 | let string = productDetailsQuery; 301 | string = this.replaceAll(string, '$productId', productId); 302 | 303 | return string 304 | } 305 | 306 | cartQuery(checkoutId) { 307 | let string = cartQuery; 308 | string = this.replaceAll(string, '$checkoutId', checkoutId); 309 | return string; 310 | } 311 | 312 | removeFromCartQuery(lineItemId, checkoutId) { 313 | let string = removeFromCartQuery; 314 | string = this.replaceAll(string, '$lineItemId', lineItemId); 315 | string = this.replaceAll(string, '$checkoutId', checkoutId); 316 | 317 | return string; 318 | 319 | } 320 | 321 | updateQuantityQuery(lineItemId, variantId, quantity, checkoutId) { 322 | let string = updateQuantityQuery; 323 | string = this.replaceAll(string, '$lineItemId', lineItemId); 324 | string = this.replaceAll(string, '$variantId', variantId); 325 | string = this.replaceAll(string, '$quantity', quantity); 326 | string = this.replaceAll(string, '$checkoutId', checkoutId); 327 | 328 | return string 329 | } 330 | 331 | 332 | allCollectionsQuery() { 333 | return allCollectionsQuery; 334 | } 335 | 336 | } -------------------------------------------------------------------------------- /src/services/ShopifyClient.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import GraphSql from './GraphSQL' 3 | 4 | export default class ShopifyClient { 5 | constructor(shopifyDomain, shopifyToken) { 6 | 7 | this.shopifyDomain = shopifyDomain; 8 | this.shopifyToken = shopifyToken; 9 | 10 | this.graphSql = new GraphSql() 11 | 12 | } 13 | 14 | query(gsqlData, successCallback, errorCallback) { 15 | let config = { 16 | headers: { 17 | "X-Shopify-Storefront-Access-Token": this.shopifyToken, 18 | "content-Type": "application/graphql" 19 | } 20 | } 21 | 22 | axios.post("https://" + this.shopifyDomain + "/api/graphql", gsqlData, config) 23 | .then(successResponse => { 24 | successCallback(successResponse) 25 | }) 26 | .catch(errorResponse => { 27 | console.log("Error", errorResponse) 28 | errorCallback(errorResponse) 29 | }) 30 | } 31 | 32 | createCheckoutId(successCallback, errorCallback) { 33 | let query = this.graphSql.createCheckoutIdQuery() 34 | this.query(query, responseSuccess => { 35 | console.log(responseSuccess) 36 | let checkoutId = responseSuccess.data.data.checkoutCreate.checkout.id 37 | successCallback(checkoutId) 38 | }, responseError => { 39 | errorCallback(responseError) 40 | }) 41 | } 42 | 43 | getCart(checkoutId, successCallback, errorCallback) { 44 | let query = this.graphSql.cartQuery(checkoutId) 45 | this.query(query, responseSuccess => { 46 | 47 | console.log('success', responseSuccess) 48 | 49 | let lineItems = responseSuccess.data.data.node.lineItems.edges; 50 | 51 | let normalizedLineItems = []; 52 | let count = 0; 53 | let totalPrice = 0; 54 | for(let l of lineItems){ 55 | 56 | console.log("lineitem:", l) 57 | 58 | let subtotal = parseFloat(l.node.variant.price) * parseInt(l.node.quantity) 59 | 60 | let image = l.node.variant.image.src 61 | 62 | let lineItem = { 63 | id: l.node.id, 64 | variantId: l.node.variant.id, 65 | title: l.node.title, 66 | quantity: l.node.quantity, 67 | image: image, 68 | price: this._formatCurrency(l.node.variant.price), 69 | variantTitle: this._shortenVariantTitle(l.node.variant.title), 70 | description: l.node.description, 71 | totalPrice: this._formatCurrency(subtotal) 72 | } 73 | 74 | count += parseInt(l.node.quantity) 75 | totalPrice += subtotal 76 | 77 | normalizedLineItems.push(lineItem) 78 | } 79 | 80 | let cart = { 81 | count: count, 82 | lineItems: normalizedLineItems, 83 | totalPrice: this._formatCurrency(totalPrice), 84 | checkoutUrl: responseSuccess.data.data.node.webUrl 85 | } 86 | 87 | successCallback(cart) 88 | }, responseError => { 89 | errorCallback(responseError) 90 | }) 91 | } 92 | 93 | removeFromCart(lineItemId, checkoutId, successCallback, errorCallback){ 94 | let query = this.graphSql.removeFromCartQuery(lineItemId, checkoutId) 95 | this.query(query, responseSuccess => { 96 | successCallback(responseSuccess) 97 | }, responseError => { 98 | errorCallback(responseError) 99 | }) 100 | } 101 | 102 | updateQuantity(lineItemId, variantId, quantity, checkoutId, successCallback, errorCallback) { 103 | let query = this.graphSql.updateQuantityQuery(lineItemId, variantId, quantity, checkoutId) 104 | this.query(query, responseSuccess => { 105 | successCallback(responseSuccess) 106 | }, responseError => { 107 | errorCallback(responseError) 108 | }) 109 | } 110 | 111 | _normalizeProduct(p){ 112 | let images = [] 113 | 114 | for(let img of p.node.images.edges){ 115 | images.push(img.node.src) 116 | } 117 | 118 | let variants = [] 119 | for(let v of p.node.variants.edges) { 120 | let variant = { 121 | id: v.node.id, 122 | image: v.node.image.src, 123 | price: v.node.price, 124 | title: this._shortenVariantTitle(v.node.title), 125 | availableForSale: v.node.availableForSale 126 | } 127 | variants.push(variant) 128 | } 129 | 130 | let price = p.price ? p.price : variants[0].price 131 | price = price.replace('.00', '') 132 | 133 | let product = { 134 | id: p.node.id, 135 | description: p.node.description != "" ? p.node.description : null, 136 | title: p.node.title, 137 | price: price, 138 | images: images, 139 | selectedImage: images[0], 140 | variants: variants, 141 | availableForSale: p.node.availableForSale 142 | } 143 | 144 | return product 145 | } 146 | 147 | _formatCurrency(num) { 148 | return parseFloat(num).toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,'); 149 | } 150 | 151 | _shortenVariantTitle(title) { 152 | title = title.toUpperCase() 153 | switch(title) { 154 | case "SMALL": 155 | return "S" 156 | case "MEDIUM": 157 | return "M" 158 | case "LARGE": 159 | return "L" 160 | case "X-LARGE": 161 | return "XL" 162 | case "XX-LARGE": 163 | return "XXL" 164 | } 165 | 166 | return title 167 | } 168 | 169 | allProducts(successCallback, errorCallback) { 170 | let query = this.graphSql.allProductsQuery(); 171 | this.query(query, responseSuccess => { 172 | let products = responseSuccess.data.data.shop.products.edges; 173 | let normalizedProducts = []; 174 | for(let p of products) { 175 | let normalizedProduct = this._normalizeProduct(p) 176 | normalizedProducts.push(normalizedProduct) 177 | 178 | } 179 | successCallback(normalizedProducts) 180 | }, responseError => { 181 | errorCallback(responseError); 182 | }) 183 | 184 | } 185 | 186 | productsFromCollection(collectionHandle, successCallback, errorCallback) { 187 | let query = this.graphSql.productsFromCollectionQuery(collectionHandle); 188 | 189 | this.query(query, responseSuccess => { 190 | let products = responseSuccess.data.data.shop.collectionByHandle.products.edges; 191 | let normalizedProducts = []; 192 | for(let p of products) { 193 | 194 | let normalizedProduct = this._normalizeProduct(p) 195 | normalizedProducts.push(normalizedProduct) 196 | } 197 | successCallback(normalizedProducts) 198 | }, responseError => { 199 | errorCallback(responseError); 200 | }) 201 | 202 | } 203 | 204 | 205 | productDetails(productId, successCallback, errorCallback) { 206 | let query = this.graphSql.productDetailsQuery(productId) 207 | this.query(query, responseSuccess => { 208 | console.log("PRODUCT DETAIL", responseSuccess) 209 | let normalizedProduct = this._normalizeProduct(responseSuccess.data.data); 210 | successCallback(normalizedProduct) 211 | }, responseError => { 212 | errorCallback(responseError) 213 | }) 214 | } 215 | 216 | addToCart(variantId, checkoutId, successCallback, errorCallback) { 217 | let query = this.graphSql.addToCartQuery(variantId, checkoutId) 218 | this.query(query, responseSuccess => { 219 | successCallback(responseSuccess) 220 | }, responseError => { 221 | errorCallback(responseError) 222 | }) 223 | } 224 | } --------------------------------------------------------------------------------