├── .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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
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 |
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 | }
--------------------------------------------------------------------------------