├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── gridsome.config.js ├── gridsome.server.js ├── package.json ├── src ├── components │ ├── Account │ │ ├── AccountDetails.vue │ │ └── AccountOrders.vue │ ├── Cart │ │ ├── CartCheckoutForm.vue │ │ └── CartItemsTable.vue │ ├── Login │ │ ├── Login.vue │ │ ├── Register.vue │ │ └── Reset.vue │ └── Navbar.vue ├── favicon.png ├── layouts │ └── Default.vue ├── main.js ├── pages │ ├── Account.vue │ ├── Cart.vue │ ├── Collections.vue │ ├── Index.vue │ └── Login.vue ├── store.js ├── styles │ ├── _custom.scss │ ├── _notification.scss │ ├── _variables.scss │ └── main.scss └── templates │ ├── Collection.vue │ ├── Page.vue │ └── Product.vue └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['travisreynolds-vue'], 3 | rules: { 4 | 'vue-a11y/label-has-for': [ 2, { 5 | required: { 6 | some: [ 'nesting', 'id' ] 7 | }, 8 | allowChildren: false 9 | }], 10 | 'vue/no-v-html': 0 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .cache 3 | .DS_Store 4 | src/.temp 5 | node_modules 6 | dist 7 | .env* 8 | .env.* 9 | static/* 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Travis Reynolds 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Extended Shopify starter for Gridsome 2 | 3 | This is an extended starter, copied from the [boilerplate starter](https://github.com/travis-r6s/gridsome-starter-shopify), which includes customer login and account pages as well as a persisted shopping cart. [View Demo](https://gridsome-starter-shopify-account.vercel.app) 4 | 5 | > Give it a go, create an account - take this as written confirmation of the fact that I will _not_ harvest any personal data. Or, of course, you can just use a dummy email and password 🤷🏻‍♂️. 6 | 7 | This starter uses a source plugin ([`gridsome-source-shopify`](https://gridsome.org/plugins/gridsome-source-shopify)) to pull data from Shopify's Storefront API, and load it into Gridsome's data store - which you can then use to create pages (note that this only runs at build time). Pages created in this starter include product pages, category pages, and CMS pages such as `/about-us`. 8 | 9 | It also uses the [Vue Apollo](https://apollo.vuejs.org) plugin client-side, to send queries/mutations (i.e. `createCheckout`, `customerCreate`) to the [Storefront API](https://help.shopify.com/en/api/storefront-api) when running in the browser. This API allows accessing customer account data, as shown in this starter. 10 | 11 | ## Shopify Setup 12 | 13 | You will need to create a private app in Shopify, and **only** give it access to the Storefront API - make sure to select **all** permissions, as this starter requires permissions to create and login customers. 14 | 15 | Make sure to note the Storefront API key, and your store name. 16 | 17 | ## Installation 18 | 19 | Install the Gridsome CLI. 20 | 21 | ```bash 22 | npm install -g @gridsome/cli # or 23 | yarn global add @gridsome/cli 24 | ``` 25 | 26 | ## Create Project 27 | 28 | You can either directly download this repository, or use Gridsome's CLI to download and install dependencies for you. 29 | 30 | ```bash 31 | # Clone repository 32 | git clone https://github.com/travis-r6s/gridsome-starter-shopify-account.git 33 | npm install # or 34 | yarn install 35 | 36 | # Download with CLI 37 | gridsome create my-gridsome-site travis-r6s/gridsome-starter-shopify-account 38 | ``` 39 | 40 | ## Developing 41 | 42 | You will need to add your Storefront API shop name (`https://.myshopify.com`) & token to an env file before running this starter. 43 | I also recommend you add some collections in Shopify to best show off this starter. 44 | 45 | ``` 46 | # Note env's are prefixed with GRIDSOME_ to make them available to apollo client side 47 | GRIDSOME_SHOPIFY_STOREFRONT= 48 | GRIDSOME_SHOPIFY_STOREFRONT_TOKEN= 49 | ``` 50 | 51 | Or you can manually edit the [Shopify Source Plugin's](https://gridsome.org/plugins/gridsome-source-shopify) configurations in `gridsome.config.js`. 52 | 53 | Enter the site folder, and run `gridsome develop` to start a local development server. 54 | 55 | Happy coding! 56 | -------------------------------------------------------------------------------- /gridsome.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteName: 'Gridsome + Shopify 😍', 3 | siteDescription: 'A full-featured Shopify starter kit for Gridsome, including an account page.', 4 | siteUrl: 'https://gridsome-starter-shopify-account.vercel.app', 5 | templates: { 6 | ShopifyProduct: [ 7 | { 8 | path: '/product/:handle', 9 | component: './src/templates/Product.vue' 10 | } 11 | ], 12 | ShopifyCollection: [ 13 | { 14 | path: '/collection/:handle', 15 | component: './src/templates/Collection.vue' 16 | } 17 | ], 18 | ShopifyPage: [ 19 | { 20 | path: '/:handle', 21 | component: './src/templates/Page.vue' 22 | } 23 | ] 24 | }, 25 | plugins: [ 26 | { 27 | use: 'gridsome-source-shopify', 28 | options: { 29 | storeName: process.env.GRIDSOME_SHOPIFY_STOREFRONT, 30 | storefrontToken: process.env.GRIDSOME_SHOPIFY_STOREFRONT_TOKEN 31 | } 32 | }, 33 | { 34 | use: 'gridsome-plugin-flexsearch', 35 | options: { 36 | flexsearch: { 37 | profile: 'match' 38 | }, 39 | collections: [ 40 | { 41 | typeName: 'ShopifyProduct', 42 | indexName: 'Product', 43 | fields: ['title', 'handle', 'description'] 44 | }, 45 | { 46 | typeName: 'ShopifyCollection', 47 | indexName: 'Collection', 48 | fields: ['title', 'handle', 'description'] 49 | } 50 | ], 51 | searchFields: ['title', 'handle', 'tags'] 52 | } 53 | }, 54 | 'gridsome-plugin-purgecss', 55 | { 56 | use: '@gridsome/plugin-critical', 57 | options: { 58 | paths: ['/', '/collections', '/collection/*'], 59 | width: 1300, 60 | height: 900 61 | } 62 | }, 63 | { 64 | use: '@gridsome/plugin-sitemap', 65 | options: { 66 | exclude: ['/account'], 67 | config: { 68 | '/product/*': { 69 | changefreq: 'daily', 70 | priority: 0.5 71 | }, 72 | '/collection/*': { 73 | changefreq: 'weekly', 74 | priority: 0.5 75 | }, 76 | '/collections': { 77 | changefreq: 'monthly', 78 | priority: 0.7 79 | } 80 | } 81 | } 82 | }, 83 | { 84 | use: '@gridsome/plugin-google-analytics', 85 | options: { 86 | id: process.env.GRIDSOME_ANALYTICS_ID 87 | } 88 | }, 89 | { 90 | use: 'gridsome-plugin-manifest', 91 | options: { 92 | background_color: '#000000', 93 | icon_path: './src/favicon.png', 94 | name: 'Gridsome + Shopify 😍', 95 | short_name: 'Shopify Starter + 🔐', 96 | theme_color: '#000000', 97 | lang: 'en' 98 | } 99 | }, 100 | { 101 | use: 'gridsome-plugin-service-worker', 102 | options: { 103 | staleWhileRevalidate: { 104 | cacheName: 'static-resources', 105 | routes: [/\.(?:css)$/] 106 | } 107 | } 108 | } 109 | ] 110 | } 111 | -------------------------------------------------------------------------------- /gridsome.server.js: -------------------------------------------------------------------------------- 1 | // Server API makes it possible to hook into various parts of Gridsome 2 | // on server-side and add custom data to the GraphQL data layer. 3 | // Learn more: https://gridsome.org/docs/server-api 4 | 5 | // Changes here require a server restart. 6 | // To restart press CTRL + C in terminal and run `gridsome develop` 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gridsome-starter-shopify-account", 3 | "private": true, 4 | "scripts": { 5 | "build": "gridsome build", 6 | "develop": "gridsome develop", 7 | "explore": "gridsome explore" 8 | }, 9 | "dependencies": { 10 | "@gridsome/plugin-critical": "^0.1.6", 11 | "@gridsome/plugin-google-analytics": "^0.1.1", 12 | "@gridsome/plugin-sitemap": "^0.4.0", 13 | "apollo-boost": "^0.4.9", 14 | "bulma": "^0.9.0", 15 | "currency.js": "^1.2.2", 16 | "graphql-tag": "^2.10.3", 17 | "gridsome": "^0.7.18", 18 | "gridsome-plugin-flexsearch": "^1.0.1", 19 | "gridsome-plugin-guess-js": "^0.1.1", 20 | "gridsome-plugin-manifest": "^0.3.5", 21 | "gridsome-plugin-purgecss": "^1.0.12", 22 | "gridsome-plugin-service-worker": "^0.1.5", 23 | "gridsome-source-shopify": "^0.1.13", 24 | "isomorphic-fetch": "^2.2.1", 25 | "js-cookie": "^2.2.1", 26 | "typeface-karla": "^0.0.72", 27 | "typeface-prata": "^0.0.72", 28 | "v-lazy-image": "^1.4.0", 29 | "vue-apollo": "^3.0.3", 30 | "vue-notification": "^1.3.20", 31 | "vuex": "^3.5.1", 32 | "vuex-persist": "^2.2.0" 33 | }, 34 | "devDependencies": { 35 | "eslint-config-travisreynolds-vue": "1.1.8", 36 | "node-sass": "^4.14.1", 37 | "sass-loader": "^9.0.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Account/AccountDetails.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 31 | -------------------------------------------------------------------------------- /src/components/Account/AccountOrders.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 58 | -------------------------------------------------------------------------------- /src/components/Cart/CartCheckoutForm.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 66 | -------------------------------------------------------------------------------- /src/components/Cart/CartItemsTable.vue: -------------------------------------------------------------------------------- 1 | 91 | 92 | 127 | -------------------------------------------------------------------------------- /src/components/Login/Login.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 132 | -------------------------------------------------------------------------------- /src/components/Login/Register.vue: -------------------------------------------------------------------------------- 1 | 123 | 124 | 186 | -------------------------------------------------------------------------------- /src/components/Login/Reset.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 107 | -------------------------------------------------------------------------------- /src/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 120 | 121 | 149 | 150 | 156 | -------------------------------------------------------------------------------- /src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/travis-r6s/gridsome-starter-shopify-advanced/0510df3b161ad515a6f055f83df741e910d00b20/src/favicon.png -------------------------------------------------------------------------------- /src/layouts/Default.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // This is the main.js file. Import global CSS and scripts here. 2 | // The Client API can be used here. Learn more: gridsome.org/docs/client-api 3 | 4 | // Layouts 5 | import DefaultLayout from '~/layouts/Default.vue' 6 | 7 | // Plugins 8 | import Notifications from 'vue-notification/dist/ssr.js' 9 | import { VLazyImagePlugin } from 'v-lazy-image' 10 | import VueApollo from 'vue-apollo' 11 | import createStore from './store' 12 | 13 | // Dependencies 14 | import ApolloClient from 'apollo-boost' 15 | import fetch from 'isomorphic-fetch' 16 | 17 | // Styles 18 | import '~/styles/main.scss' 19 | import 'typeface-karla' 20 | import 'typeface-prata' 21 | 22 | export default function (Vue, { appOptions, isClient, router }) { 23 | // Set default layout as a global component 24 | Vue.component('Layout', DefaultLayout) 25 | 26 | // Import global plugins 27 | Vue.use(Notifications) 28 | Vue.use(VLazyImagePlugin) 29 | Vue.use(VueApollo) 30 | 31 | // Create Apollo client 32 | const apolloClient = new ApolloClient({ 33 | fetch, 34 | uri: `https://${process.env.GRIDSOME_SHOPIFY_STOREFRONT}.myshopify.com/api/2020-04/graphql.json`, 35 | headers: { 36 | 'X-Shopify-Storefront-Access-Token': process.env.GRIDSOME_SHOPIFY_STOREFRONT_TOKEN 37 | } 38 | }) 39 | 40 | // Add client to vue-apollo provider 41 | const apolloProvider = new VueApollo({ 42 | defaultClient: apolloClient 43 | }) 44 | 45 | // Add provider to vue app 46 | appOptions.apolloProvider = apolloProvider 47 | 48 | // Add Vuex store 49 | const store = createStore(Vue, { isClient }) 50 | appOptions.store = store 51 | 52 | // Authentication & Route handling 53 | if (isClient) { 54 | router.beforeEach((to, from, next) => { 55 | const tokenExists = store.getters.isAuthenticated 56 | if (to.path.includes('/account') && !tokenExists) next('/login') 57 | else if (to.path.includes('/login') && tokenExists) next('/') 58 | else next() 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/pages/Account.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 90 | -------------------------------------------------------------------------------- /src/pages/Cart.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 91 | 92 | 98 | -------------------------------------------------------------------------------- /src/pages/Collections.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 61 | 62 | 63 | query ShopifyProducts { 64 | allShopifyCollection (limit: 100) { 65 | edges { 66 | node { 67 | id 68 | path 69 | title 70 | descriptionHtml 71 | image { 72 | altText 73 | src: transformedSrc(maxWidth: 400, maxHeight: 300, crop: CENTER) 74 | placeholder: transformedSrc(maxWidth: 100, maxHeight: 75, crop: CENTER) 75 | } 76 | } 77 | } 78 | } 79 | } 80 | 81 | 82 | 87 | -------------------------------------------------------------------------------- /src/pages/Index.vue: -------------------------------------------------------------------------------- 1 | 88 | 89 | 100 | 101 | 102 | query ShopifyProducts { 103 | allShopifyCollection (limit: 1) { 104 | edges { 105 | node { 106 | id 107 | path 108 | title 109 | descriptionHtml 110 | image { 111 | altText 112 | src: transformedSrc(maxWidth: 800, maxHeight: 800, crop: CENTER) 113 | placeholder: transformedSrc(maxWidth: 200, maxHeight: 200, crop: CENTER) 114 | } 115 | } 116 | } 117 | } 118 | allShopifyProduct (limit: 6) { 119 | edges { 120 | node { 121 | id 122 | title 123 | path 124 | descriptionHtml 125 | priceRange { 126 | minVariantPrice { 127 | amount(format: true) 128 | } 129 | } 130 | images (limit: 1) { 131 | id 132 | altText 133 | src: transformedSrc (maxWidth: 400, maxHeight: 300, crop: CENTER) 134 | placeholder: transformedSrc(maxWidth: 100, maxHeight: 75, crop: CENTER) 135 | } 136 | } 137 | } 138 | } 139 | } 140 | 141 | 142 | 151 | -------------------------------------------------------------------------------- /src/pages/Login.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 37 | 38 | 46 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | // Plugins 2 | import Cookies from 'js-cookie' 3 | import currency from 'currency.js' 4 | import Vuex from 'vuex' 5 | import VuexPersistence from 'vuex-persist' 6 | 7 | export default function createStore (Vue, { isClient }) { 8 | // Use Vuex plugin 9 | Vue.use(Vuex) 10 | 11 | // Create Vuex store 12 | const store = new Vuex.Store({ 13 | state: { 14 | cart: [], 15 | token: {} 16 | }, 17 | mutations: { 18 | updateCart: (state, cart) => { state.cart = cart }, 19 | setToken: (state, token) => { state.token = token } 20 | }, 21 | actions: { 22 | addToCart: ({ state, commit }, newItem) => { 23 | const cart = state.cart 24 | const itemExists = cart.find(item => item.variantId === newItem.variantId) 25 | 26 | if (itemExists) itemExists.qty += newItem.qty 27 | else cart.push(newItem) 28 | 29 | const updatedCart = cart.map(item => { 30 | const total = currency(item.price, { formatWithSymbol: true, symbol: '£' }).multiply(item.qty).format() 31 | return { ...item, total } 32 | }) 33 | 34 | commit('updateCart', updatedCart) 35 | }, 36 | updateItemQty: ({ state, commit }, { itemId, qty }) => { 37 | const cart = state.cart 38 | const item = cart.find(item => item.variantId === itemId) 39 | 40 | item.qty = qty 41 | item.total = currency(item.price, { formatWithSymbol: true, symbol: '£' }).multiply(qty).format() 42 | 43 | commit('updateCart', cart) 44 | }, 45 | removeFromCart: ({ state, commit }, itemId) => { 46 | const cart = state.cart 47 | const updatedCart = cart.filter(item => item.variantId !== itemId) 48 | 49 | commit('updateCart', updatedCart) 50 | }, 51 | login: ({ commit }, token) => { 52 | commit('setToken', token) 53 | }, 54 | logout: ({ commit }) => { 55 | commit('setToken', {}) 56 | commit('updateCart', []) 57 | } 58 | }, 59 | getters: { 60 | isAuthenticated: ({ token }) => !!token.accessToken, 61 | accessToken: ({ token }) => token.accessToken, 62 | cartTotal: ({ cart }) => cart.reduce((total, item) => total.add(currency(item.price).multiply(item.qty)), currency(0, { formatWithSymbol: true, symbol: '£' })) 63 | } 64 | }) 65 | 66 | // Run vuex-persist if we are running on the client 67 | if (isClient) { 68 | // Tokens 69 | new VuexPersistence({ 70 | restoreState: key => Cookies.getJSON(key), 71 | saveState: (key, { token }) => { 72 | if (token) { 73 | const expires = new Date(token.expiresAt) 74 | Cookies.set(key, { token }, { expires }) 75 | } else { 76 | Cookies.set(key, { token }) 77 | } 78 | }, 79 | modules: ['token'], 80 | filter: mutation => mutation.type === 'setToken' 81 | }).plugin(store) 82 | 83 | // Cart 84 | new VuexPersistence({ 85 | storage: window.localStorage, 86 | modules: ['cart'], 87 | filter: mutation => mutation.type === 'updateCart' 88 | }).plugin(store) 89 | } 90 | 91 | return store 92 | } 93 | -------------------------------------------------------------------------------- /src/styles/_custom.scss: -------------------------------------------------------------------------------- 1 | // Button 2 | .button { 3 | font-weight: $weight-semibold; 4 | text-transform: uppercase; 5 | font-size: $size-small !important; 6 | height: 50px !important; 7 | } 8 | 9 | // Navbar { 10 | .navbar { 11 | text-transform: uppercase; 12 | font-size: 0.8rem; 13 | } 14 | 15 | // Cart 16 | .cart { 17 | .input { 18 | height: 50px !important; 19 | } 20 | .field.has-addons { 21 | .button { 22 | padding: 0 25px !important; 23 | border: 1px solid $grey-lighter; 24 | &.is-white { 25 | border: none; 26 | } 27 | } 28 | } 29 | .item-total { 30 | padding-top: 14px; 31 | } 32 | } 33 | 34 | // Loading spinner - loading.io/css 35 | .lds-ring { 36 | display: inline-block; 37 | position: relative; 38 | width: 80px; 39 | height: 80px; 40 | div { 41 | box-sizing: border-box; 42 | display: block; 43 | position: absolute; 44 | width: 64px; 45 | height: 64px; 46 | margin: 8px; 47 | border: 8px solid #fff; 48 | border-radius: 50%; 49 | animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; 50 | border-color: $black transparent transparent transparent; 51 | &:nth-child(1) { 52 | animation-delay: -0.45s; 53 | } 54 | &:nth-child(2) { 55 | animation-delay: -0.3s; 56 | } 57 | &:nth-child(3) { 58 | animation-delay: -0.15s; 59 | } 60 | } 61 | } 62 | 63 | 64 | @keyframes lds-ring { 65 | 0% { 66 | transform: rotate(0deg); 67 | } 68 | 100% { 69 | transform: rotate(360deg); 70 | } 71 | } 72 | 73 | // Lazy Image 74 | .v-lazy-image { 75 | filter: blur(10px); 76 | transition: filter 0.7s; 77 | } 78 | .v-lazy-image-loaded { 79 | filter: blur(0); 80 | } 81 | -------------------------------------------------------------------------------- /src/styles/_notification.scss: -------------------------------------------------------------------------------- 1 | .vue-notification-group { 2 | display: block; 3 | position: fixed; 4 | z-index: 5000; 5 | } 6 | 7 | .vue-notification-wrapper { 8 | display: block; 9 | overflow: hidden; 10 | width: 100%; 11 | margin: 0; 12 | padding: 0; 13 | } 14 | 15 | .notification-title { 16 | font-weight: 600; 17 | } 18 | 19 | .vue-notification-template { 20 | display: block; 21 | box-sizing: border-box; 22 | background: white; 23 | text-align: left; 24 | } 25 | 26 | .vue-notification { 27 | display: block; 28 | box-sizing: border-box; 29 | text-align: left; 30 | font-size: 12px; 31 | padding: 10px; 32 | margin: 0 5px 5px; 33 | 34 | color: white; 35 | background: #44A4FC; 36 | border-left: 5px solid #187FE7; 37 | } 38 | 39 | .vue-notification.primary { 40 | background: $primary; 41 | border-left-color: $primary; 42 | } 43 | 44 | .vue-notification.warn { 45 | background: #ffb648; 46 | border-left-color: #f48a06; 47 | } 48 | 49 | .vue-notification.error { 50 | background: #E54D42; 51 | border-left-color: #B82E24; 52 | } 53 | 54 | .vue-notification.success { 55 | background: #68CD86; 56 | border-left-color: #42A85F; 57 | } 58 | 59 | .vn-fade-enter-active, .vn-fade-leave-active, .vn-fade-move { 60 | transition: all .5s; 61 | } 62 | 63 | .vn-fade-enter, .vn-fade-leave-to { 64 | opacity: 0; 65 | } 66 | -------------------------------------------------------------------------------- /src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | @import "bulma/sass/utilities/initial-variables"; 2 | @import "bulma/sass/utilities/functions"; 3 | 4 | $primary: #161616; 5 | 6 | $info: $cyan; 7 | $success: $green; 8 | $warning: $yellow; 9 | $danger: $red; 10 | 11 | $light: $white-ter; 12 | $dark: $grey-darker; 13 | 14 | // Invert colors 15 | 16 | $orange-invert: findColorInvert($orange); 17 | $yellow-invert: findColorInvert($yellow); 18 | $green-invert: findColorInvert($green); 19 | $turquoise-invert: findColorInvert($turquoise); 20 | $cyan-invert: findColorInvert($cyan); 21 | $blue-invert: findColorInvert($blue); 22 | $purple-invert: findColorInvert($purple); 23 | $red-invert: findColorInvert($red); 24 | 25 | $primary-invert: $turquoise-invert; 26 | $info-invert: $cyan-invert; 27 | $success-invert: $green-invert; 28 | $warning-invert: $yellow-invert; 29 | $danger-invert: $red-invert; 30 | $light-invert: $dark; 31 | $dark-invert: $light; 32 | 33 | // General colors 34 | 35 | $background: $white-ter; 36 | 37 | $border: $grey-lighter; 38 | $border-hover: $grey-light; 39 | 40 | // Text colors 41 | 42 | $text: $black-bis; 43 | $text-invert: findColorInvert($text); 44 | $text-light: $grey-dark; 45 | $text-strong: $black; 46 | 47 | // Code colors 48 | 49 | $code: $red; 50 | $code-background: $background; 51 | 52 | $pre: $text; 53 | $pre-background: $background; 54 | 55 | // Link colors 56 | 57 | $link: $text; 58 | $link-invert: $text-invert; 59 | $link-visited: $text; 60 | 61 | $link-hover: $grey-light; 62 | $link-hover-border: $grey-light; 63 | 64 | $link-focus: $grey-darker; 65 | $link-focus-border: $grey-darker; 66 | 67 | $link-active: $grey-darker; 68 | $link-active-border: $grey-dark; 69 | 70 | // Typography 71 | 72 | $family-primary: "Karla", $family-sans-serif; 73 | $family-secondary: "Prata", $family-sans-serif; 74 | $family-code: $family-monospace; 75 | 76 | $size-small: $size-7; 77 | $size-normal: $size-6; 78 | $size-medium: $size-5; 79 | $size-large: $size-4; 80 | 81 | // Lists and maps 82 | $custom-colors: null; 83 | $custom-shades: null; 84 | 85 | $colors: mergeColorMaps(("white": ($white, $black), "black": ($black, $white), "light": ($light, $light-invert), "dark": ($dark, $dark-invert), "primary": ($primary, $primary-invert), "link": ($link, $link-invert), "info": ($info, $info-invert), "success": ($success, $success-invert), "warning": ($warning, $warning-invert), "danger": ($danger, $danger-invert)), $custom-colors); 86 | $shades: mergeColorMaps(("black-bis": $black-bis, "black-ter": $black-ter, "grey-darker": $grey-darker, "grey-dark": $grey-dark, "grey": $grey, "grey-light": $grey-light, "grey-lighter": $grey-lighter, "white-ter": $white-ter, "white-bis": $white-bis), $custom-shades); 87 | 88 | $sizes: $size-1 $size-2 $size-3 $size-4 $size-5 $size-6 $size-7; 89 | 90 | // Miscellaneous 91 | $radius: 0px; 92 | 93 | // Button 94 | $button-padding-horizontal: 2.8rem; 95 | 96 | // Table 97 | $table-cell-border: none; 98 | -------------------------------------------------------------------------------- /src/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import "./variables"; 2 | @import "./custom"; 3 | @import "bulma/bulma"; 4 | 5 | // Plugins 6 | @import "./notification" 7 | -------------------------------------------------------------------------------- /src/templates/Collection.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 62 | 63 | 64 | query Collection ($id: ID!) { 65 | shopifyCollection (id: $id) { 66 | id 67 | title 68 | descriptionHtml 69 | products { 70 | id 71 | title 72 | path 73 | descriptionHtml 74 | priceRange { 75 | minVariantPrice { 76 | amount(format: true, currency: "GBP") 77 | } 78 | } 79 | images (limit: 1) { 80 | id 81 | altText 82 | src: transformedSrc (maxWidth: 400, maxHeight: 300, crop: CENTER) 83 | placeholder: transformedSrc(maxWidth: 100, maxHeight: 75, crop: CENTER) 84 | } 85 | } 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /src/templates/Page.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 29 | 30 | 31 | query Page ($id: ID!) { 32 | shopifyPage (id: $id) { 33 | id 34 | title 35 | body 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /src/templates/Product.vue: -------------------------------------------------------------------------------- 1 | 93 | 94 | 146 | 147 | 148 | query Product ($id: ID!) { 149 | shopifyProduct (id: $id) { 150 | id 151 | path 152 | descriptionHtml 153 | title 154 | tags 155 | images(limit: 4) { 156 | id 157 | altText 158 | src: transformedSrc(maxWidth: 600, maxHeight: 400, crop: CENTER) 159 | placeholder: transformedSrc(maxWidth: 150, maxHeight: 100, crop: CENTER) 160 | thumbnail: transformedSrc(maxWidth: 150, maxHeight: 150, crop: CENTER) 161 | } 162 | options { 163 | id 164 | name 165 | values 166 | } 167 | variants { 168 | id 169 | title 170 | price { 171 | amount(format: true) 172 | } 173 | selectedOptions { 174 | name 175 | value 176 | } 177 | image { 178 | id 179 | altText 180 | thumbnail: transformedSrc(maxWidth: 150, maxHeight: 150, crop: CENTER) 181 | } 182 | } 183 | } 184 | } 185 | 186 | 187 | 203 | --------------------------------------------------------------------------------