├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── components ├── AppLogo.vue ├── ImageViewer.vue ├── Price.vue ├── ProductList.vue ├── README.md └── SanityImage.vue ├── css ├── global.css └── typography.css ├── layouts ├── README.md └── default.vue ├── nuxt.config.js ├── package-lock.json ├── package.json ├── pages ├── README.md ├── category │ ├── _category.vue │ └── index.vue ├── index.vue ├── product │ ├── _product.vue │ └── index.vue └── vendor │ ├── _vendor.vue │ └── index.vue ├── plugins ├── README.md └── globalData.js ├── sanity.js ├── static ├── README.md └── favicon.ico ├── store ├── README.md └── index.js ├── utils └── localize.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | parserOptions: { 8 | parser: 'babel-eslint' 9 | }, 10 | extends: [ 11 | 'eslint:recommended', 12 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 13 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 14 | 'plugin:vue/recommended', 15 | 'plugin:prettier/recommended' 16 | ], 17 | // required to lint *.vue files 18 | plugins: ['vue', 'prettier'], 19 | // add your custom rules here 20 | rules: { 21 | semi: [2, 'never'], 22 | 'vue/max-attributes-per-line': 'off', 23 | 'prettier/prettier': 'error' 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # logs 5 | npm-debug.log 6 | 7 | # Nuxt build 8 | .nuxt 9 | 10 | # Nuxt generate 11 | dist 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Transglobal Candy Store 2 | 3 | A frontend example in Vue.js and Nuxt.js for the Sanity.io e-commerce schema 4 | 5 | 🔗 [Read the blog post](https://www.sanity.io/blog/e-commerce-vue-nuxt-snipcart) 6 | 7 | ![Short animated preview of the app](https://public.sanity.io/github-assets/snipcart-for-github.gif "Logo Title Text 1") 8 | 9 | 10 | ## Quickstart on local 11 | 12 | ``` bash 13 | # install dependencies 14 | $ npm install 15 | 16 | # serve with hot reload at localhost:3000 17 | $ npm run dev 18 | ``` 19 | 20 | Tips: 21 | - Make sure you are running on http://localhost:3000. If not sanity and snipcart will fail due to CORS origins. 22 | - Vue.js requires a recent Node version so if it fails on startup you might need an upgrade. 23 | 24 | ## Using your own sanity.io data 25 | 26 | You're about five minutes away from running this example with your own data. You'll need to set up a Sanity studio for this so: 27 | 28 | - If you don't have Sanity CLI version 0.130.0 or later, install or upgrade it with `npm install -g @sanity/cli` 29 | - Initialize a new project with `sanity init` and select the e-commerce schema 30 | - `sanity start` will start your studio and let you start adding your products! 31 | - Go to `sanity.json` and locate your `projectId` and `dataset` 32 | 33 | Head back to this project and in `sanity.js` change the `projectId` and `dataset` values to the ones you found above 34 | 35 | Tips: 36 | - Remember to add CORS manage.sanity.io (ex. http://localhost:3000 to run locally) 37 | - You can `sanity deploy` your editor to share it with others 38 | 39 | ## Install your own snipcart 40 | - Go to http://snipcart.com 41 | - Register and copy your API-key from snipcart 42 | - In `nuxt.config.js` paste it into `data-api-key` 43 | - Remember to add your domain/url in your Snicart settings (https://app.snipcart.com/dashboard/account/domains) 44 | 45 | ## Build production server or static project 46 | ``` bash 47 | # build for production and launch server 48 | $ npm run build 49 | $ npm start 50 | 51 | # generate static project 52 | $ npm run generate 53 | ``` 54 | 55 | If you want to host this on Netlify, as a static build, follow [these steps](https://www.sanity.io/blog/tutorial-host-your-sanity-based-next-js-project-on-netlify#3-deploy-your-blog-on-netlify) while switching out the `generate` command above and changing the output directory from `out` to `dist`. Note: Nuxt is intended to run as a universal/isomorphic app and will make calls to the Sanity CDN. 56 | 57 | The queries are by default limited to 100 items. This project is just an example, but 58 | it is possible to expand it with pagination or forever-scroll. To get more items, 59 | just add ex [0..1000] to the end of your query https://www.sanity.io/docs/data-store/query-cheat-sheet#slice-operations 60 | 61 | For detailed explanations on how Nuxt.js work, checkout the [Nuxt.js docs](https://github.com/nuxt/nuxt.js). 62 | -------------------------------------------------------------------------------- /components/AppLogo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | -------------------------------------------------------------------------------- /components/ImageViewer.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 63 | 64 | 124 | -------------------------------------------------------------------------------- /components/Price.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | 27 | -------------------------------------------------------------------------------- /components/ProductList.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 99 | 100 | 244 | -------------------------------------------------------------------------------- /components/README.md: -------------------------------------------------------------------------------- 1 | # COMPONENTS 2 | 3 | The components directory contains your Vue.js Components. 4 | Nuxt.js doesn't supercharge these components. 5 | 6 | **This directory is not required, you can delete it if you don't want to use it.** 7 | -------------------------------------------------------------------------------- /components/SanityImage.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 33 | -------------------------------------------------------------------------------- /css/global.css: -------------------------------------------------------------------------------- 1 | h1.title { 2 | font-family: "Quicksand", "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; /* 1 */ 3 | display: block; 4 | font-weight: 500; 5 | letter-spacing: -0.04em; 6 | font-size: 3em; 7 | line-height: 1em; 8 | } 9 | 10 | @media only screen and (min-width: 500px) { 11 | h1.title { 12 | font-size: 4em; 13 | } 14 | } 15 | 16 | h2.subtitle { 17 | font-weight: 300; 18 | font-size: 2em; 19 | word-spacing: 5px; 20 | padding-bottom: 40px; 21 | } 22 | 23 | @media only screen and (min-width: 500px) { 24 | h2.subtitle { 25 | font-size: 3em; 26 | } 27 | } 28 | 29 | a { 30 | color: inherit; 31 | } 32 | 33 | a:active { 34 | color: #3987d9; 35 | } 36 | 37 | button { 38 | border: none; 39 | display: inline-block; 40 | appearance: none; 41 | color: #000; 42 | background-color: #fff; 43 | padding: 0.25em 0.5em; 44 | margin: 0 0.25em; 45 | border: 2px solid #000; 46 | outline: none; 47 | } 48 | 49 | button:active { 50 | background-color: #000; 51 | color: #fff; 52 | } 53 | -------------------------------------------------------------------------------- /css/typography.css: -------------------------------------------------------------------------------- 1 | .blockContent { 2 | color: red; 3 | } -------------------------------------------------------------------------------- /layouts/README.md: -------------------------------------------------------------------------------- 1 | # LAYOUTS 2 | 3 | This directory contains your Application Layouts. 4 | 5 | More information about the usage of this directory in the documentation: 6 | https://nuxtjs.org/guide/views#layouts 7 | 8 | **This directory is not required, you can delete it if you don't want to use it.** 9 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 86 | 87 | 229 | 230 | 250 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | import client from "./sanity.js" 2 | 3 | export default { 4 | /* 5 | ** Headers of the page 6 | */ 7 | head: { 8 | title: "ecommerce-frontend", 9 | meta: [ 10 | { charset: "utf-8" }, 11 | { name: "viewport", content: "width=device-width, initial-scale=1" }, 12 | { 13 | hid: "description", 14 | name: "description", 15 | content: "Sanity frontend example for E-commerce in Vue.js" 16 | } 17 | ], 18 | link: [ 19 | { rel: "icon", type: "image/x-icon", href: "/favicon.ico" }, 20 | // Snipcart styling 21 | { 22 | href: "https://cdn.snipcart.com/themes/2.0/base/snipcart.min.css", 23 | type: "text/css", 24 | rel: "stylesheet" 25 | } 26 | ], 27 | script: [ 28 | // jQuery. Only needed for snipcart 29 | { 30 | src: "https://ajax.googleapis.com/ajax/libs/jquery/2.2.2/jquery.min.js" 31 | }, 32 | // Snipcart js 33 | { 34 | src: "https://cdn.snipcart.com/scripts/2.0/snipcart.js", 35 | id: "snipcart", 36 | "data-autopop": "false", 37 | // Change me. Read more at http://snipcart.com 38 | "data-api-key": 39 | "ODRkNmJhZDktOTk5YS00Y2Y1LTk5Y2ItMTkzNTlkZTYxNzhmNjM2NTk1NTI2OTgyMTc1MTUy" 40 | } 41 | ] 42 | }, 43 | /* 44 | ** Customize the progress bar color 45 | */ 46 | loading: { color: "#3B8070" }, 47 | /* 48 | ** Load categories and vendors 49 | */ 50 | plugins: ["~/plugins/globalData"], 51 | /* 52 | ** Global CSS 53 | */ 54 | css: ["~/css/global.css"], 55 | /* 56 | ** Build configuration 57 | */ 58 | build: { 59 | /* 60 | ** postcss 61 | */ 62 | postcss: [require("postcss-cssnext")()], 63 | /* 64 | ** Run ESLint on save 65 | */ 66 | extend(config, { isDev, isClient }) { 67 | if (isDev && isClient) { 68 | config.module.rules.push({ 69 | enforce: "pre", 70 | test: /\.(js|vue)$/, 71 | loader: "eslint-loader", 72 | exclude: /(node_modules)/ 73 | }) 74 | } 75 | } 76 | }, 77 | generate: { 78 | routes: async function() { 79 | const paths = await client.fetch(`{ 80 | "product": *[_type == "product"].slug.current, 81 | "category": *[_type == "category"].slug.current, 82 | "vendor": *[_type == "vendor"].slug.current 83 | }`) 84 | return Object.keys(paths).reduce( 85 | (acc, key) => [ 86 | ...acc, 87 | ...paths[key].reduce((acc, curr) => [...acc, `${key}/${curr}`], []) 88 | ], 89 | [] 90 | ) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecommerce-frontend", 3 | "version": "1.0.0", 4 | "description": "Sanity frontend example for E-commerce in Vue.js", 5 | "author": "kristoffer@sanity.io", 6 | "private": true, 7 | "scripts": { 8 | "dev": "nuxt", 9 | "build": "nuxt build", 10 | "start": "nuxt start", 11 | "generate": "nuxt generate", 12 | "lint": "eslint --ext .js,.vue --ignore-path .gitignore .", 13 | "lint:fix": "npm run lint -- --fix", 14 | "precommit": "npm run lint" 15 | }, 16 | "dependencies": { 17 | "@sanity/block-content-to-html": "^1.3.5", 18 | "@sanity/client": "^0.128.3", 19 | "@sanity/image-url": "^0.128.12", 20 | "numeral": "^2.0.6", 21 | "nuxt": "^2.0.0", 22 | "postcss": "^6.0.22", 23 | "postcss-cssnext": "^3.1.0", 24 | "vue-line-clamp": "^1.2.3" 25 | }, 26 | "devDependencies": { 27 | "babel-eslint": "^8.2.3", 28 | "eslint": "^4.19.1", 29 | "eslint-config-prettier": "^3.6.0", 30 | "eslint-friendly-formatter": "^4.0.1", 31 | "eslint-loader": "^2.0.0", 32 | "eslint-plugin-prettier": "^3.0.1", 33 | "eslint-plugin-vue": "^4.5.0", 34 | "prettier": "^1.12.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pages/README.md: -------------------------------------------------------------------------------- 1 | # PAGES 2 | 3 | This directory contains your Application Views and Routes. 4 | The framework reads all the .vue files inside this directory and creates the router of your application. 5 | 6 | More information about the usage of this directory in the documentation: 7 | https://nuxtjs.org/guide/routing 8 | -------------------------------------------------------------------------------- /pages/category/_category.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 57 | 58 | 80 | -------------------------------------------------------------------------------- /pages/category/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 53 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 28 | 29 | 49 | -------------------------------------------------------------------------------- /pages/product/_product.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 102 | 103 | 219 | -------------------------------------------------------------------------------- /pages/product/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 35 | -------------------------------------------------------------------------------- /pages/vendor/_vendor.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 52 | -------------------------------------------------------------------------------- /pages/vendor/index.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 43 | 44 | 82 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # PLUGINS 2 | 3 | This directory contains your Javascript plugins that you want to run before instantiating the root vue.js application. 4 | 5 | More information about the usage of this directory in the documentation: 6 | https://nuxtjs.org/guide/plugins 7 | 8 | **This directory is not required, you can delete it if you don't want to use it.** 9 | -------------------------------------------------------------------------------- /plugins/globalData.js: -------------------------------------------------------------------------------- 1 | import sanity from "~/sanity.js" 2 | 3 | function isParentOf(category, possibleParent) { 4 | if (possibleParent._id === category._id) { 5 | return false 6 | } 7 | return (category.parents || []).some( 8 | parent => parent._ref === possibleParent._id 9 | ) 10 | } 11 | 12 | const attachCategories = (category, allCategories) => { 13 | return { 14 | ...category, 15 | children: allCategories.filter(otherCategory => 16 | isParentOf(otherCategory, category) 17 | ) 18 | } 19 | } 20 | 21 | const query = ` 22 | { 23 | "categories": *[_type == "category"] { 24 | _id, 25 | title, 26 | slug, 27 | parents 28 | }, 29 | "vendors": *[_type == "vendor"] { 30 | title, 31 | slug, 32 | logo, 33 | "productQty": count(*[_type == "product" && references(^._id)]) 34 | } 35 | } 36 | ` 37 | 38 | /** 39 | * We do this to achieve server side rendering for 40 | * content displayed by layouts components 41 | * ( layouts does not have an asyncData() method ) 42 | */ 43 | export default ({ store }) => { 44 | return sanity.fetch(query).then(data => { 45 | const categories = data.categories.map(category => 46 | attachCategories(category, data.categories) 47 | ) 48 | data.categoryTree = categories.filter( 49 | category => (category.parents || []).length === 0 50 | ) 51 | store.commit("globalData", data) 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /sanity.js: -------------------------------------------------------------------------------- 1 | const sanityClient = require("@sanity/client") 2 | 3 | module.exports = sanityClient({ 4 | // Find your project ID and dataset in `sanity.json` in your studio project or on https://manage.sanity.io 5 | projectId: "ru2xdibx", 6 | dataset: "production", 7 | useCdn: true 8 | // useCdn == true gives fast, cheap responses using a globally distributed cache. 9 | // Set this to false if your application require the freshest possible 10 | // data always (potentially slightly slower and a bit more expensive). 11 | }) 12 | -------------------------------------------------------------------------------- /static/README.md: -------------------------------------------------------------------------------- 1 | # STATIC 2 | 3 | This directory contains your static files. 4 | Each file inside this directory is mapped to /. 5 | 6 | Example: /static/robots.txt is mapped as /robots.txt. 7 | 8 | More information about the usage of this directory in the documentation: 9 | https://nuxtjs.org/guide/assets#static 10 | 11 | **This directory is not required, you can delete it if you don't want to use it.** 12 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/example-ecommerce-snipcart-vue/31d155443b80b61330957421d50193f8573eef06/static/favicon.ico -------------------------------------------------------------------------------- /store/README.md: -------------------------------------------------------------------------------- 1 | # STORE 2 | 3 | This directory contains your Vuex Store files. 4 | Vuex Store option is implemented in the Nuxt.js framework. 5 | Creating a index.js file in this directory activate the option in the framework automatically. 6 | 7 | More information about the usage of this directory in the documentation: 8 | https://nuxtjs.org/guide/vuex-store 9 | 10 | **This directory is not required, you can delete it if you don't want to use it.** 11 | -------------------------------------------------------------------------------- /store/index.js: -------------------------------------------------------------------------------- 1 | import Vuex from "vuex" 2 | 3 | const createStore = () => { 4 | return new Vuex.Store({ 5 | mutations: { 6 | globalData(state, value) { 7 | state.globalData = value 8 | } 9 | } 10 | }) 11 | } 12 | 13 | export default createStore 14 | -------------------------------------------------------------------------------- /utils/localize.js: -------------------------------------------------------------------------------- 1 | export default function localize(value, languages = ["en", "no", "es"]) { 2 | if (Array.isArray(value)) { 3 | return value.map(v => localize(v, languages)) 4 | } else if (typeof value === "object") { 5 | if (/^locale[A-Z]/.test(value._type)) { 6 | const language = languages.find(lang => value[lang]) 7 | return value[language] 8 | } 9 | 10 | return Object.keys(value).reduce((result, key) => { 11 | result[key] = localize(value[key], languages) 12 | return result 13 | }, {}) 14 | } 15 | return value 16 | } 17 | --------------------------------------------------------------------------------