├── netlify.toml ├── .github └── FUNDING.yml ├── static ├── bag.jpg ├── icon.png ├── shoe1.jpg ├── callout.jpg ├── favicon.ico ├── products │ ├── 1.jpg │ ├── 2.jpg │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ ├── 9.png │ ├── 10.jpg │ ├── 11.png │ ├── 12.png │ ├── 13.png │ ├── 14.png │ ├── 15.png │ ├── 16.jpg │ ├── 17.png │ ├── 18.jpg │ └── 19.png ├── icon-package.svg ├── icon-cal.svg ├── icon-service.svg └── storedata.json ├── hooks ├── index.js └── build.js ├── plugins ├── workbox-caching-extensions.js ├── currency-filter.js ├── workbox-extensions.js ├── README.md ├── vueAnalytics.js ├── workbox-routing-extensions.js └── client-sw.js ├── assets ├── README.md └── main.scss ├── middleware └── README.md ├── pages ├── men.vue ├── women.vue ├── all.vue ├── index.vue ├── cart.vue └── product │ └── _id.vue ├── pwa.config.js ├── utils ├── oneSignal.js ├── routes.js └── precacheManifest.js ├── components ├── AppFooterLinks.vue ├── AppFooter.vue ├── AppCartSteps.vue ├── AppSalesBoxes.vue ├── AppTextlockup.vue ├── AppNav.vue ├── AppFeaturedProducts.vue ├── AppStoreGrid.vue ├── AppCard.vue ├── AppCartDisplay.vue └── AppLoader.vue ├── package.json ├── layouts └── default.vue ├── .gitignore ├── nuxt.config.js ├── functions └── index.js ├── store └── index.js └── README.md /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | functions = "functions" -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [sdras] 4 | -------------------------------------------------------------------------------- /static/bag.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/bag.jpg -------------------------------------------------------------------------------- /static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/icon.png -------------------------------------------------------------------------------- /static/shoe1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/shoe1.jpg -------------------------------------------------------------------------------- /static/callout.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/callout.jpg -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /static/products/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/products/1.jpg -------------------------------------------------------------------------------- /static/products/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/products/2.jpg -------------------------------------------------------------------------------- /static/products/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/products/3.jpg -------------------------------------------------------------------------------- /static/products/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/products/4.jpg -------------------------------------------------------------------------------- /static/products/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/products/5.png -------------------------------------------------------------------------------- /static/products/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/products/6.png -------------------------------------------------------------------------------- /static/products/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/products/7.png -------------------------------------------------------------------------------- /static/products/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/products/8.png -------------------------------------------------------------------------------- /static/products/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/products/9.png -------------------------------------------------------------------------------- /hooks/index.js: -------------------------------------------------------------------------------- 1 | import build from './build' 2 | 3 | export default nuxtConfig => ({ build: build(nuxtConfig) }) 4 | -------------------------------------------------------------------------------- /static/products/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/products/10.jpg -------------------------------------------------------------------------------- /static/products/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/products/11.png -------------------------------------------------------------------------------- /static/products/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/products/12.png -------------------------------------------------------------------------------- /static/products/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/products/13.png -------------------------------------------------------------------------------- /static/products/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/products/14.png -------------------------------------------------------------------------------- /static/products/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/products/15.png -------------------------------------------------------------------------------- /static/products/16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/products/16.jpg -------------------------------------------------------------------------------- /static/products/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/products/17.png -------------------------------------------------------------------------------- /static/products/18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/products/18.jpg -------------------------------------------------------------------------------- /static/products/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerardofelipe/ecommerce-netlify-pwa/HEAD/static/products/19.png -------------------------------------------------------------------------------- /plugins/workbox-caching-extensions.js: -------------------------------------------------------------------------------- 1 | self.__precacheManifest = [].concat(self.__precacheManifest || []) 2 | workbox.precaching.precacheAndRoute(self.__precacheManifest, { 3 | cacheId: 'ecommerce-netlify-prod', 4 | directoryIndex: '/', 5 | }) 6 | -------------------------------------------------------------------------------- /plugins/currency-filter.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | 3 | Vue.filter("dollar", function(value) { 4 | // Using a template literal here, that's why there are two dollar signs. 5 | // The first is an actual dollar. 6 | return `$${parseFloat(value).toFixed(2)}` 7 | }) 8 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # ASSETS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your un-compiled assets such as LESS, SASS, or JavaScript. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked). 8 | -------------------------------------------------------------------------------- /plugins/workbox-extensions.js: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------- 2 | // Messages 3 | // -------------------------------------------------- 4 | self.addEventListener('message', event => { 5 | // Skip over the SW waiting lifecycle stage 6 | if (event.data && event.data.type === 'SKIP_WAITING') skipWaiting() 7 | }) 8 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # PLUGINS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains Javascript plugins that you want to run before mounting the root Vue.js application. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/plugins). 8 | -------------------------------------------------------------------------------- /middleware/README.md: -------------------------------------------------------------------------------- 1 | # MIDDLEWARE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your application middleware. 6 | Middleware let you define custom functions that can be run before rendering either a page or a group of pages. 7 | 8 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware). 9 | -------------------------------------------------------------------------------- /pages/men.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | -------------------------------------------------------------------------------- /pages/women.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | -------------------------------------------------------------------------------- /pages/all.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | -------------------------------------------------------------------------------- /plugins/vueAnalytics.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueAnalytics from 'vue-analytics' 3 | 4 | export default (ctx, inject) => { 5 | Vue.use(VueAnalytics, { 6 | id: 'UA-149950722-1', 7 | checkDuplicatedScript: true, 8 | debug: { 9 | enabled: false, 10 | trace: false, 11 | sendHitTask: process.env.NODE_ENV === 'production', 12 | }, 13 | router: ctx.app.router, 14 | }) 15 | 16 | ctx.$ga = Vue.$ga 17 | inject('ga', Vue.$ga) 18 | } 19 | -------------------------------------------------------------------------------- /pwa.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | manifest: { 3 | theme_color: '#cccccc', 4 | background_color: '#cccccc', 5 | }, 6 | meta: { nativeUI: true }, 7 | workbox: { 8 | swURL: '/OneSignalSDKWorker.js', 9 | skipWaiting: true, // @TODO Enabled until a solution to OneSignal's automatic skipwaiting is found 10 | offlineAnalytics: true, 11 | workboxExtensions: '@/plugins/workbox-extensions.js', 12 | cachingExtensions: '@/plugins/workbox-caching-extensions.js', 13 | runtimeCaching: [{ urlPattern: 'https://js.stripe.com/v3', handler: 'StaleWhileRevalidate' }], 14 | routingExtensions: '@/plugins/workbox-routing-extensions.js', 15 | }, 16 | } 17 | 18 | export default config 19 | -------------------------------------------------------------------------------- /utils/oneSignal.js: -------------------------------------------------------------------------------- 1 | export const addOneSignalCartTags = (item = {}) => { 2 | if (!item.id) return 3 | OneSignal.push(() => { 4 | OneSignal.sendTags({ 5 | [item.id]: item.quantity || '', 6 | cart_update: Date.now(), 7 | }).then(function(tagsSent) { 8 | // Callback called when tags have finished sending 9 | // console.log('tagsSent: ', tagsSent) 10 | }) 11 | }) 12 | } 13 | 14 | export const clearOneSignalCartTags = (idsToClean = []) => { 15 | OneSignal.push(() => { 16 | OneSignal.deleteTags([...idsToClean, cart_update]).then(function(tagsDeleted) { 17 | // Callback called when tags have finished sending 18 | // console.log('tagsDeleted: ', tagsDeleted) 19 | }) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /utils/routes.js: -------------------------------------------------------------------------------- 1 | import data from '../static/storedata' 2 | import crypto from 'crypto' 3 | import get from 'lodash/get' 4 | 5 | const dynamicURLs = data.map(el => `product/${el.id}`) 6 | 7 | export const dynamicRoutes = () => { 8 | return new Promise(resolve => { 9 | resolve(dynamicURLs) 10 | }) 11 | } 12 | 13 | export const getTemplatedURLs = nuxt => { 14 | const routes = get(nuxt, 'options.router.routes', []) 15 | const staticURLs = routes.map(({ path }) => path).filter(route => !route.includes('/:')) 16 | 17 | return [...staticURLs, ...dynamicURLs.map(url => `/${url}`)].reduce((acc, url) => { 18 | // Add revision version to the url 19 | acc[url] = crypto.randomBytes(7).toString('hex') 20 | return acc 21 | }, {}) 22 | } 23 | -------------------------------------------------------------------------------- /components/AppFooterLinks.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 26 | 27 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 31 | 32 | 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecommerce-netlify-pwa", 3 | "version": "1.0.0", 4 | "description": "The sdras/ecommerce-netlify PWAtized!", 5 | "author": "gerardofelipe", 6 | "private": true, 7 | "scripts": { 8 | "dev": "nuxt", 9 | "build": "nuxt build", 10 | "start": "nuxt start", 11 | "generate": "nuxt generate" 12 | }, 13 | "dependencies": { 14 | "@nuxtjs/onesignal": "^3.0.0-beta.16", 15 | "@nuxtjs/pwa": "^3.0.0-beta.19", 16 | "axios": "^0.19.0", 17 | "dotenv": "^8.0.0", 18 | "gsap": "^2.1.3", 19 | "node-sass": "^4.12.0", 20 | "normalize.css": "^8.0.1", 21 | "nuxt": "^2.0.0", 22 | "sass-loader": "^7.1.0", 23 | "stripe": "^7.4.0", 24 | "sweetalert2": "^8.18.6", 25 | "uuid": "^3.3.2", 26 | "vue-analytics": "^5.17.2", 27 | "vue-star-rating": "^1.6.1", 28 | "vue-stripe-elements-plus": "^0.2.10" 29 | }, 30 | "devDependencies": { 31 | "nodemon": "^1.18.9", 32 | "prepend-file": "^1.3.1", 33 | "workbox-build": "^4.3.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | 23 | 60 | -------------------------------------------------------------------------------- /utils/precacheManifest.js: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | import { writeFile } from 'fs-extra' 3 | import path from 'path' 4 | import prependFile from 'prepend-file' 5 | 6 | const createStrHash = str => 7 | crypto 8 | .createHash('md5') 9 | .update(str, 'utf8') 10 | .digest('hex') 11 | 12 | const buildPrecacheManifest = manifestEntries => 13 | `self.__precacheManifest = [\n${manifestEntries.map(JSON.stringify).join(',\n')}\n];` 14 | 15 | const buildPrecacheManifestFileName = precacheManifestStr => 16 | `precache-manifest.${createStrHash(precacheManifestStr)}.js` 17 | 18 | const prependPrecacheManifestToSW = fileName => { 19 | prependFile('static/sw.js', `importScripts('_nuxt/${fileName}')\n`, err => { 20 | if (err) throw new Error('Add precache-manifest file import to SW failed') 21 | console.info('Precache-manifest file import added to SW successfully') 22 | }) 23 | } 24 | 25 | export const createPrecacheManifest = async manifestEntries => { 26 | const precacheManifestStr = buildPrecacheManifest(manifestEntries) 27 | const precacheManifestFileName = buildPrecacheManifestFileName(precacheManifestStr) 28 | 29 | await writeFile(path.join('.nuxt/dist/client', precacheManifestFileName), precacheManifestStr) 30 | 31 | prependPrecacheManifestToSW(precacheManifestFileName) 32 | } 33 | -------------------------------------------------------------------------------- /hooks/build.js: -------------------------------------------------------------------------------- 1 | // import { oneSignalWorkerHack } from '../utils/oneSignal' 2 | import { createPrecacheManifest } from '../utils/precacheManifest' 3 | import { getTemplatedURLs } from '../utils/routes' 4 | import { getManifest } from 'workbox-build' 5 | import { copy } from 'fs-extra' 6 | import path from 'path' 7 | 8 | export const oneSignalWorkerHack = () => { 9 | const src = path.join('static', 'OneSignalSDKWorker.js') 10 | const dest = path.join('static', 'OneSignalSDKUpdaterWorker.js') 11 | copy(src, dest, { overwrite: true }, err => { 12 | if (err) console.error(err) 13 | }) 14 | } 15 | 16 | export default nuxtConfig => ({ 17 | /** 18 | * 'buld:done' 19 | * {@link node_modules/nuxt/lib/core/builder.js} 20 | */ 21 | async done(nuxt) { 22 | oneSignalWorkerHack() 23 | try { 24 | const { manifestEntries } = await getManifest({ 25 | templatedURLs: getTemplatedURLs(nuxt), 26 | globDirectory: '.', 27 | globIgnores: ['**/sw.js', 'OneSignalSDK'], 28 | globPatterns: ['static/**/*.{js,png,html,css,svg,ico,jpg}', '.nuxt/dist/client/**/*.{js,json,png}'], 29 | dontCacheBustURLsMatching: /^_nuxt\//, 30 | modifyURLPrefix: { 31 | 'static/': '', 32 | '.nuxt/dist/client': '_nuxt', 33 | }, 34 | }) 35 | 36 | await createPrecacheManifest(manifestEntries) 37 | } catch (ex) { 38 | console.error(ex) 39 | } 40 | }, 41 | }) 42 | -------------------------------------------------------------------------------- /components/AppFooter.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 32 | 33 | -------------------------------------------------------------------------------- /components/AppCartSteps.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 29 | 30 | -------------------------------------------------------------------------------- /pages/cart.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 51 | 52 | -------------------------------------------------------------------------------- /plugins/workbox-routing-extensions.js: -------------------------------------------------------------------------------- 1 | // Cache the Google Fonts stylesheets with a stale-while-revalidate strategy 2 | const googleFontsCSSStrategy = new workbox.strategies.StaleWhileRevalidate({ cacheName: 'google-fonts' }) 3 | workbox.routing.registerRoute(/^https:\/\/fonts\.googleapis\.com/, googleFontsCSSStrategy) 4 | 5 | // Cache the underlying font files with a cache-first strategy for 1 year 6 | const googleFontsStrategy = new workbox.strategies.CacheFirst({ 7 | cacheName: 'google-fonts', 8 | plugins: [ 9 | new workbox.cacheableResponse.Plugin({ 10 | statuses: [0, 200], 11 | }), 12 | new workbox.expiration.Plugin({ 13 | maxAgeSeconds: 60 * 60 * 24 * 365, 14 | maxEntries: 30, 15 | }), 16 | ], 17 | }) 18 | workbox.routing.registerRoute(/^https:\/\/fonts\.gstatic\.com/, googleFontsStrategy) 19 | 20 | // -------------------------------------------------- 21 | // Manual cache on install event 22 | // -------------------------------------------------- 23 | self.addEventListener('install', event => { 24 | const googleFontsCSSUrl = 'https://fonts.googleapis.com/css?family=Montserrat:300,600|PT+Serif&display=swap' 25 | 26 | const googleFontsUrls = [ 27 | 'https://fonts.gstatic.com/s/montserrat/v14/JTURjIg1_i6t8kCHKm45_bZF3gnD_g.woff2', 28 | 'https://fonts.gstatic.com/s/montserrat/v14/JTURjIg1_i6t8kCHKm45_cJD3gnD_g.woff2', 29 | 'https://fonts.gstatic.com/s/ptserif/v11/EJRVQgYoZZY2vCFuvAFWzr8.woff2', 30 | ] 31 | 32 | event.waitUntil( 33 | Promise.all([ 34 | googleFontsCSSStrategy.makeRequest({ request: googleFontsCSSUrl }), 35 | ...googleFontsUrls.map(url => googleFontsStrategy.makeRequest({ request: url })), 36 | ]), 37 | ) 38 | }) 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE / Editor 81 | .idea 82 | .editorconfig 83 | 84 | # Service worker 85 | sw.* 86 | 87 | # Mac OSX 88 | .DS_Store 89 | 90 | # Prettier 91 | .prettierrc 92 | 93 | # OneSignal 94 | OneSignalSDK* 95 | -------------------------------------------------------------------------------- /components/AppSalesBoxes.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 36 | 37 | -------------------------------------------------------------------------------- /components/AppTextlockup.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 26 | 27 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | import hooks from './hooks' 2 | import pwaConfig from './pwa.config' 3 | import { dynamicRoutes } from './utils/routes' 4 | 5 | export default { 6 | mode: 'universal', 7 | /* 8 | ** Headers of the page 9 | */ 10 | head: { 11 | title: process.env.npm_package_name || '', 12 | script: [{ src: 'https://js.stripe.com/v3/' }], 13 | meta: [ 14 | { charset: 'utf-8' }, 15 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 16 | { 17 | hid: 'description', 18 | name: 'description', 19 | content: process.env.npm_package_description || '' 20 | } 21 | ], 22 | link: [ 23 | { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }, 24 | { 25 | rel: 'stylesheet', 26 | href: 27 | 'https://fonts.googleapis.com/css?family=Montserrat:300,600|PT+Serif&display=swap' 28 | } 29 | ] 30 | }, 31 | generate: { 32 | routes: dynamicRoutes 33 | }, 34 | /* 35 | ** Customize the progress-bar color 36 | */ 37 | loading: { color: '#fff' }, 38 | /* 39 | ** Global CSS 40 | */ 41 | css: ['normalize.css', { src: '~/assets/main.scss', lang: 'sass' }], 42 | /* 43 | ** Plugins to load before mounting the App 44 | */ 45 | plugins: [ 46 | '~/plugins/currency-filter.js', 47 | { src: '~/plugins/client-sw.js', mode: 'client' }, 48 | { src: '~/plugins/vueAnalytics.js', mode: 'client' }, 49 | ], 50 | /* 51 | ** Nuxt.js modules 52 | */ 53 | modules: [ 54 | '@nuxtjs/onesignal', 55 | '@nuxtjs/pwa' 56 | ], 57 | /* 58 | ** Build configuration 59 | */ 60 | build: { 61 | /* 62 | ** You can extend webpack config here 63 | */ 64 | extend(config, ctx) {} 65 | }, 66 | pwa: pwaConfig, 67 | hooks: hooks(this), 68 | oneSignal: { 69 | init: { 70 | appId: '7209773f-0abc-44c3-9055-02520e841f90', // prod 71 | // appId: '99747ce7-56d2-4584-8a49-341e97d9de65', // dev 72 | allowLocalhostAsSecureOrigin: true, 73 | notifyButton: { 74 | enable: true, 75 | showCredit: false, 76 | }, 77 | } 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config() 2 | 3 | const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY), 4 | headers = { 5 | "Access-Control-Allow-Origin": "*", 6 | "Access-Control-Allow-Headers": "Content-Type" 7 | } 8 | 9 | exports.handler = async (event, context) => { 10 | if (!event.body || event.httpMethod !== "POST") { 11 | return { 12 | statusCode: 400, 13 | headers, 14 | body: JSON.stringify({ 15 | status: "invalid http method" 16 | }) 17 | } 18 | } 19 | 20 | const data = JSON.parse(event.body) 21 | console.log(data) 22 | 23 | if (!data.stripeToken || !data.stripeAmt || !data.stripeIdempotency) { 24 | console.error("Required information is missing.") 25 | 26 | return { 27 | statusCode: 400, 28 | headers, 29 | body: JSON.stringify({ 30 | status: "missing information" 31 | }) 32 | } 33 | } 34 | 35 | // stripe payment processing begins here 36 | try { 37 | await stripe.customers 38 | .create({ 39 | email: data.stripeEmail, 40 | source: data.stripeToken 41 | }) 42 | .then(customer => { 43 | console.log( 44 | `starting the charges, amt: ${data.stripeAmt}, email: ${ 45 | data.stripeEmail 46 | }` 47 | ) 48 | return stripe.charges 49 | .create( 50 | { 51 | currency: "usd", 52 | amount: data.stripeAmt, 53 | receipt_email: data.stripeEmail, 54 | customer: customer.id, 55 | description: "Sample Charge" 56 | }, 57 | { 58 | idempotency_key: data.stripeIdempotency 59 | } 60 | ) 61 | .then(result => { 62 | console.log(`Charge created: ${result}`) 63 | }) 64 | }) 65 | 66 | return { 67 | statusCode: 200, 68 | headers, 69 | body: JSON.stringify({ 70 | status: "it works! beep boop" 71 | }) 72 | } 73 | } catch (err) { 74 | console.log(err) 75 | 76 | return { 77 | statusCode: 400, 78 | headers, 79 | body: JSON.stringify({ 80 | status: err 81 | }) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /components/AppNav.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 36 | 37 | -------------------------------------------------------------------------------- /components/AppFeaturedProducts.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 28 | 29 | 112 | -------------------------------------------------------------------------------- /plugins/client-sw.js: -------------------------------------------------------------------------------- 1 | const commonAlertConfig = { 2 | toast: true, 3 | type: 'info', 4 | position: 'bottom-end', 5 | showCloseButton: true, 6 | background: '#efefef', 7 | } 8 | 9 | const firstInstallAlertConfig = { 10 | ...commonAlertConfig, 11 | title: 'Offline navigation is available now!', 12 | showConfirmButton: false, 13 | timer: 5000, 14 | } 15 | 16 | const updateAvailableAlertConfig = { 17 | ...commonAlertConfig, 18 | title: 'Skyline Ivy Store Update Available', 19 | html: '

We just installed a new version of the store, refresh to update!

', 20 | confirmButtonText: 'Reload Page', 21 | } 22 | 23 | const importSwal = async () => { 24 | try { 25 | const { default: Swal } = await import('sweetalert2') 26 | return Swal 27 | } catch (ex) { 28 | console.error(ex) 29 | } 30 | } 31 | 32 | // @TODO Disabled until a solution to OneSignal's automatic skipwaiting is found 33 | const skipWaiting = workbox => { 34 | workbox.addEventListener('controlling', event => { 35 | window.location.reload() 36 | }) 37 | 38 | workbox.messageSW({ type: 'SKIP_WAITING' }) 39 | } 40 | 41 | // @nuxtjs/pwa module registers the SW and exposes the Workbox instance in window.$workbox promise 42 | const clientSWCustomCode = async () => { 43 | if (!'serviceWorker' in navigator || !window.$workbox) return 44 | try { 45 | const workbox = await window.$workbox 46 | 47 | const Swal = await importSwal() 48 | 49 | // @TODO Disabled until a solution to OneSignal's automatic skipwaiting is found 50 | // if (!Swal) { 51 | // skipWaiting(workbox) // skip waiting without ask 52 | // return 53 | // } 54 | 55 | workbox.addEventListener('activated', async ({ isUpdate }) => { 56 | if (isUpdate) return 57 | 58 | Swal.fire(firstInstallAlertConfig) 59 | .then() 60 | .catch(err => console.error(err)) 61 | }) 62 | 63 | // @TODO Disabled until a solution to OneSignal's automatic skipwaiting is found 64 | // workbox.addEventListener('waiting', async event => { 65 | // Swal.fire(updateAvailableAlertConfig) 66 | // .then(reload => { 67 | // if (reload) skipWaiting(workbox) // skip waiting without ask 68 | // }) 69 | // .catch(err => { 70 | // console.error(err) 71 | // skipWaiting(workbox) // skip waiting without ask 72 | // }) 73 | // }) 74 | } catch (ex) { 75 | console.error(ex) 76 | } 77 | } 78 | 79 | export default clientSWCustomCode 80 | -------------------------------------------------------------------------------- /store/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import uuidv1 from 'uuid/v1' 3 | import data from '~/static/storedata.json' 4 | import {addOneSignalCartTags, clearOneSignalCartTags} from "../utils/oneSignal"; 5 | 6 | export const state = () => ({ 7 | cartUIStatus: 'idle', 8 | storedata: data, 9 | cart: [], 10 | }) 11 | 12 | export const getters = { 13 | featuredProducts: state => state.storedata.slice(0, 3), 14 | women: state => state.storedata.filter(el => el.gender === 'Female'), 15 | men: state => state.storedata.filter(el => el.gender === 'Male'), 16 | cartCount: state => { 17 | if (!state.cart.length) return 0 18 | return state.cart.reduce((ac, next) => ac + next.quantity, 0) 19 | }, 20 | cartTotal: state => { 21 | if (!state.cart.length) return 0 22 | return state.cart.reduce((ac, next) => ac + next.quantity * next.price, 0) 23 | }, 24 | } 25 | 26 | export const mutations = { 27 | updateCartUI: (state, payload) => { 28 | state.cartUIStatus = payload 29 | }, 30 | clearCart: state => { 31 | const ids = state.cart.map(({ id }) => id) 32 | clearOneSignalCartTags(ids) 33 | //this clears the cart 34 | ;(state.cart = []), (state.cartUIStatus = 'idle') 35 | }, 36 | addToCart: (state, payload) => { 37 | let itemfound = state.cart.find(el => el.id === payload.id) 38 | itemfound ? (itemfound.quantity += payload.quantity) : state.cart.push(payload) 39 | addOneSignalCartTags(itemfound || payload) 40 | }, 41 | } 42 | 43 | export const actions = { 44 | async postStripeFunction({ getters, commit }, payload) { 45 | commit('updateCartUI', 'loading') 46 | 47 | try { 48 | await axios 49 | .post( 50 | 'https://ecommerce-netlify.netlify.com/.netlify/functions/index', 51 | { 52 | stripeEmail: payload.stripeEmail, 53 | stripeAmt: Math.floor(getters.cartTotal * 100), //it expects the price in cents, as an integer 54 | stripeToken: 'tok_visa', //testing token, later we would use payload.data.token 55 | stripeIdempotency: uuidv1(), //we use this library to create a unique id 56 | }, 57 | { 58 | headers: { 59 | 'Content-Type': 'application/json', 60 | }, 61 | }, 62 | ) 63 | .then(res => { 64 | if (res.status === 200) { 65 | commit('updateCartUI', 'success') 66 | setTimeout(() => commit('clearCart'), 5000) 67 | } else { 68 | commit('updateCartUI', 'failure') 69 | // allow them to try again 70 | setTimeout(() => commit('updateCartUI', 'idle'), 5000) 71 | } 72 | 73 | console.log(JSON.stringify(res, null, 2)) 74 | }) 75 | } catch (err) { 76 | console.log(err) 77 | commit('updateCartUI', 'failure') 78 | } 79 | }, 80 | } 81 | -------------------------------------------------------------------------------- /components/AppStoreGrid.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 73 | 74 | -------------------------------------------------------------------------------- /components/AppCard.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 83 | 84 | -------------------------------------------------------------------------------- /components/AppCartDisplay.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 71 | 72 | -------------------------------------------------------------------------------- /components/AppLoader.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 84 | 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Can Sarah [Drasner's ecommerce-netlify](https://github.com/sdras/ecommerce-netlify) be more amazing? 2 | 3 | # Yeah, it can be PWAmazing! 4 | 5 | [![Netlify Status](https://api.netlify.com/api/v1/badges/fbf53d4a-d931-4768-a7ee-3d9fffb9f407/deploy-status)](https://app.netlify.com/sites/ecommerce-netlify-pwa/deploys) 6 | 7 | The motivation of this repository is to show the transformation of a project into a PWA in demo time for my [JSNight 2019.2](https://www.meetup.com/es-ES/canarias-javascript/events/265096702) and [JSDay Canarias 2019](https://jsdaycanarias.com/) talks 8 | 9 | We will use two different approaches, using the [@nuxtjs/pwa](https://pwa.nuxtjs.org/) module and [Google's Workbox Libraries](https://developers.google.com/web/tools/workbox) 10 | 11 | - Give it offline capabilities 12 | - Make it installable on different devices 13 | - Use of runtime cache 14 | - Different caching strategies 15 | - The use of precaching 16 | - Friendly handling of application updates 17 | - Offline Google Analytics with background synchronization 18 | - Push notifications using [OneSignal](https://onesignal.com/) 19 | - Client-side DB working even offline with automatic background synchronization [WIP] 20 | - ... 21 | - More to come! 22 | 23 | **Demo site is here:** [E-Commerce Store PWA](https://ecommerce-netlify-pwa.netlify.com/) 24 | 25 | **NOTE:** To get a better idea of why and how of some decisions and different ways of doing things, here is the order of the branches to follow for both approaches 26 | 27 | | Oder | @nuxtjs/pwa | | Oder | workbox-webpack-plugin | 28 | |:----: |------------------------------------ | --- |:----: |----------------------------- | 29 | | 0 | original | | 0 | original | 30 | | 1 | add-pwa-module | | 1 | inject-manifest-precache | 31 | | 2 | pwa-module-installable | | 2 | skip-waiting-confirmation | 32 | | 3 | pwa-module-basic-register-route | | 3 | inject-manifest-installable | 33 | | 4 | pwa-module-precache-dynamic-assets | | 4 | google-analytics | 34 | | 5 | pwa-module-offline | | | | 35 | | 6 | pwa-module-offline-analytics | | | | 36 | | 7 | pwa-module-push-notifications | | | | 37 | 38 | ### ORIGINAL README.MD 39 | > 40 | > # 🛍 Ecommerce Store with Netlify Functions and Stripe 41 | > 42 | > > A serverless function to process stripe payments with Nuxt, Netlify, and Lambda 43 | > 44 | > Demo site is here: [E-Commerce Store](https://ecommerce-netlify.netlify.com/) 45 | > 46 | > ![screenshot of site](https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/ecommerce-screenshot.jpg "E-Commerce Netlify Site") 47 | > 48 | > There are two articles explaining how this site is set up: 49 | > * Explanation of Netlify Functions and Stripe: [Let's Build a JAMstack E-Commerce Store with Netlify Functions](https://css-tricks.com/lets-build-a-jamstack-e-commerce-store-with-netlify-functions/) 50 | > * Explanation of dynamic routing in Nuxt for the individual product pages: [Creating Dynamic Routes in Nuxt Application](https://css-tricks.com/creating-dynamic-routes-in-a-nuxt-application/) 51 | > 52 | > ## Build Setup 53 | > 54 | > ``` bash 55 | > # install dependencies 56 | > $ yarn install or npm install 57 | > 58 | > # serve with hot reload at localhost:3000 59 | > $ yarn dev or npm dev 60 | > 61 | > # build for production and launch server 62 | > $ yarn build or npm build 63 | > $ yarn start or npm start 64 | > 65 | > # generate static project 66 | > $ yarn generate or npm generate 67 | > ``` 68 | > 69 | > For detailed explanation on how things work, checkout [Nuxt.js docs](https://nuxtjs.org). 70 | > 71 | -------------------------------------------------------------------------------- /pages/product/_id.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 86 | 87 | -------------------------------------------------------------------------------- /static/icon-package.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /static/icon-cal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /static/icon-service.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/main.scss: -------------------------------------------------------------------------------- 1 | /*------------ Variables -----------*/ 2 | 3 | $brandprimary: #d96528; 4 | $brandsecondary: #03c1c1; 5 | 6 | /*------------ Global -----------*/ 7 | 8 | body { 9 | border: 10px solid #ccc; 10 | min-height: 100vh; 11 | font-family: "Montserrat", -apple-system, BlinkMacSystemFont, "Segoe UI", 12 | Roboto, "Helvetica Neue", Arial, sans-serif; 13 | font-size: 16px; 14 | line-height: 1.4; 15 | word-spacing: 1px; 16 | -ms-text-size-adjust: 100%; 17 | -webkit-text-size-adjust: 100%; 18 | -moz-osx-font-smoothing: grayscale; 19 | -webkit-font-smoothing: antialiased; 20 | box-sizing: border-box; 21 | } 22 | 23 | h1, 24 | h2, 25 | h3 { 26 | font-family: "PT Serif", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 27 | "Helvetica Neue", Arial, sans-serif; 28 | font-weight: normal; 29 | } 30 | 31 | h1 { 32 | font-size: 40px; 33 | } 34 | 35 | p { 36 | margin: 20px 0; 37 | } 38 | 39 | *, 40 | *:before, 41 | *:after { 42 | box-sizing: border-box; 43 | margin: 0; 44 | } 45 | 46 | a, 47 | a:active, 48 | a:visited { 49 | color: $brandprimary; 50 | text-decoration: none; 51 | transition: 0.3s all ease; 52 | } 53 | 54 | button { 55 | border: 1px solid #ccc; 56 | background: white; 57 | padding: 10px 14px; 58 | cursor: pointer; 59 | color: black; 60 | font-weight: 700; 61 | font-family: "Montserrat", -apple-system, BlinkMacSystemFont, "Segoe UI", 62 | Roboto, "Helvetica Neue", Arial, sans-serif; 63 | transition: 0.3s all ease; 64 | &:hover { 65 | background: black; 66 | border: 1px solid black; 67 | color: white; 68 | } 69 | } 70 | 71 | button.purchase { 72 | background: $brandprimary; 73 | color: white; 74 | font-size: 16px; 75 | border: none; 76 | &:hover { 77 | background: #c14103; 78 | } 79 | } 80 | 81 | button.pay-with-stripe { 82 | background: black; 83 | color: white; 84 | font-size: 16px; 85 | border: none; 86 | &:hover { 87 | background: #c14103; 88 | } 89 | 90 | &:disabled { 91 | background: #999; 92 | } 93 | } 94 | 95 | .price { 96 | color: $brandprimary; 97 | font-size: 20px; 98 | margin: 5px 0; 99 | font-weight: normal; 100 | font-family: "PT Serif", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 101 | "Helvetica Neue", Arial, sans-serif; 102 | } 103 | 104 | hr { 105 | border-top: 1px solid #eee; 106 | margin: 30px 0; 107 | } 108 | 109 | label { 110 | font-weight: 600; 111 | text-transform: uppercase; 112 | font-size: 14px; 113 | letter-spacing: 0.1em; 114 | margin-top: 20px; 115 | display: inline-block; 116 | } 117 | 118 | input { 119 | font-family: "Montserrat", -apple-system, BlinkMacSystemFont, "Segoe UI", 120 | Roboto, "Helvetica Neue", Arial, sans-serif; 121 | font-size: 16px; 122 | padding: 5px 10px; 123 | } 124 | 125 | .center { 126 | text-align: center; 127 | } 128 | 129 | /*------------ Store Grid -----------*/ 130 | 131 | .storegrid { 132 | width: 95%; 133 | display: grid; 134 | grid-template-columns: 3fr 1fr; 135 | grid-template-rows: 1fr; 136 | grid-column-gap: 40px; 137 | grid-row-gap: 0px; 138 | } 139 | 140 | /* no grid support */ 141 | aside { 142 | float: left; 143 | width: 19.1489%; 144 | } 145 | 146 | .content { 147 | /*no grid support*/ 148 | float: right; 149 | width: 79.7872%; 150 | /* grid */ 151 | display: grid; 152 | grid-template-columns: repeat(3, 1fr); 153 | grid-gap: 10px; 154 | padding: 0 !important; 155 | } 156 | 157 | @media (max-width: 600px) { 158 | aside { 159 | width: 100% !important; 160 | margin-bottom: 10px !important; 161 | } 162 | 163 | .content { 164 | width: 100% !important; 165 | grid-template-columns: 1fr !important; 166 | } 167 | } 168 | 169 | @media (min-width: 601px) and (max-width: 900px) { 170 | .content { 171 | grid-template-columns: repeat(2, 1fr) !important; 172 | } 173 | } 174 | 175 | @media screen and (max-width: 550px) { 176 | .storegrid { 177 | width: 90%; 178 | display: grid; 179 | grid-template-columns: 2fr 1fr; 180 | grid-template-rows: 1fr; 181 | grid-column-gap: 10px; 182 | } 183 | } 184 | 185 | /* --- items animation --- */ 186 | 187 | .items-leave-active { 188 | transition: opacity 0.2s ease-out, scale 0.2s ease-out; 189 | } 190 | 191 | .items-move { 192 | transition: opacity 0.2s ease-out, scale 0.2s ease-out; 193 | } 194 | 195 | .items-enter-active { 196 | transition: opacity 0.2s ease-out, scale 0.2s ease-out; 197 | } 198 | 199 | .items-enter, 200 | .items-leave-to { 201 | opacity: 0; 202 | transform: scale(0.9); 203 | transform-origin: 50% 50%; 204 | } 205 | 206 | /* --- range --- */ 207 | 208 | input[type="range"].slider { 209 | -webkit-appearance: none; 210 | width: 100%; 211 | margin: 25px 0 5px; 212 | } 213 | 214 | input[type="range"].slider:focus { 215 | outline: none; 216 | } 217 | 218 | input[type="range"].slider::-webkit-slider-runnable-track { 219 | width: 100%; 220 | height: 4.3px; 221 | cursor: pointer; 222 | box-shadow: 0px 0px 0px rgba(0, 0, 0, 0), 0px 0px 0px rgba(13, 13, 13, 0); 223 | background: $brandprimary; 224 | border-radius: 13.7px; 225 | border: 0px solid rgba(1, 1, 1, 0); 226 | } 227 | 228 | input[type="range"].slider::-webkit-slider-thumb { 229 | box-shadow: 0px 0px 0px rgba(0, 0, 62, 0), 0px 0px 0px rgba(0, 0, 88, 0); 230 | border: 1.9px solid $brandprimary; 231 | height: 17px; 232 | width: 17px; 233 | border-radius: 31px; 234 | background: #ffffff; 235 | cursor: pointer; 236 | -webkit-appearance: none; 237 | margin-top: -6.35px; 238 | } 239 | 240 | input[type="range"].slider:focus::-webkit-slider-runnable-track { 241 | background: $brandprimary; 242 | } 243 | 244 | input[type="range"].slider::-moz-range-track { 245 | width: 100%; 246 | height: 4.3px; 247 | cursor: pointer; 248 | box-shadow: 0px 0px 0px rgba(0, 0, 0, 0), 0px 0px 0px rgba(13, 13, 13, 0); 249 | background: $brandprimary; 250 | border-radius: 13.7px; 251 | border: 0px solid rgba(1, 1, 1, 0); 252 | } 253 | 254 | input[type="range"].slider::-moz-range-thumb { 255 | box-shadow: 0px 0px 0px rgba(0, 0, 62, 0), 0px 0px 0px rgba(0, 0, 88, 0); 256 | border: 1.9px solid $brandprimary; 257 | height: 17px; 258 | width: 17px; 259 | border-radius: 31px; 260 | background: #ffffff; 261 | cursor: pointer; 262 | } 263 | 264 | input[type="range"].slider::-ms-track { 265 | width: 100%; 266 | height: 4.3px; 267 | cursor: pointer; 268 | background: transparent; 269 | border-color: transparent; 270 | color: transparent; 271 | } 272 | 273 | input[type="range"].slider::-ms-fill-lower { 274 | background: $brandprimary; 275 | border: 0px solid rgba(1, 1, 1, 0); 276 | border-radius: 27.4px; 277 | box-shadow: 0px 0px 0px rgba(0, 0, 0, 0), 0px 0px 0px rgba(13, 13, 13, 0); 278 | } 279 | 280 | input[type="range"].slider::-ms-fill-upper { 281 | background: $brandprimary; 282 | border: 0px solid rgba(1, 1, 1, 0); 283 | border-radius: 27.4px; 284 | box-shadow: 0px 0px 0px rgba(0, 0, 0, 0), 0px 0px 0px rgba(13, 13, 13, 0); 285 | } 286 | 287 | input[type="range"].slider::-ms-thumb { 288 | box-shadow: 0px 0px 0px rgba(0, 0, 62, 0), 0px 0px 0px rgba(0, 0, 88, 0); 289 | border: 1.9px solid $brandprimary; 290 | height: 17px; 291 | width: 17px; 292 | border-radius: 31px; 293 | background: #ffffff; 294 | cursor: pointer; 295 | height: 4.3px; 296 | } 297 | 298 | input[type="range"].slider:focus::-ms-fill-lower { 299 | background: $brandprimary; 300 | } 301 | 302 | input[type="range"].slider:focus::-ms-fill-upper { 303 | background: $brandprimary; 304 | } 305 | -------------------------------------------------------------------------------- /static/storedata.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "9d436e98-1dc9-4f21-9587-76d4c0255e33", 4 | "color": "Goldenrod", 5 | "description": "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis. Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci. Mauris lacinia sapien quis libero.", 6 | "gender": "Male", 7 | "name": "Desi Avramovitz", 8 | "review": "productize virtual markets", 9 | "starrating": 3, 10 | "price": 50.4, 11 | "img": "1.jpg" 12 | }, 13 | { 14 | "id": "bfa86b1c-9ebf-4555-987f-7b8bf7d27be7", 15 | "color": "Puce", 16 | "description": "Phasellus in felis. Donec semper sapien a libero. Nam dui.", 17 | "gender": "Male", 18 | "name": "Addy Alldre", 19 | "review": "deploy efficient mindshare", 20 | "starrating": 4, 21 | "price": 33.99, 22 | "img": "2.jpg" 23 | }, 24 | { 25 | "id": "eca6b1c7-6b8c-4416-aae0-98cecdc18e92", 26 | "color": "Orange", 27 | "description": "Curabitur in libero ut massa volutpat convallis. Morbi odio odio, elementum eu, interdum eu, tincidunt in, leo. Maecenas pulvinar lobortis est.", 28 | "gender": "Female", 29 | "name": "Bernie Gledhill", 30 | "review": "mesh sticky content", 31 | "starrating": 3, 32 | "price": 102.99, 33 | "img": "3.jpg" 34 | }, 35 | { 36 | "id": "440e2eac-67ed-4407-993a-d9f6b7f15087", 37 | "color": "Teal", 38 | "description": "Praesent blandit. Nam nulla. Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.\n\nMorbi porttitor lorem id ligula. Suspendisse ornare consequat lectus. In est risus, auctor sed, tristique in, tempus sit amet, sem.", 39 | "gender": "Female", 40 | "name": "Gwendolen Bickerstaffe", 41 | "review": "integrate front-end infomediaries", 42 | "starrating": 5, 43 | "price": 64.5, 44 | "img": "4.jpg" 45 | }, 46 | { 47 | "id": "38543b28-3b0e-4886-a108-b58419bcef94", 48 | "color": "Yellow", 49 | "description": "Etiam vel augue. Vestibulum rutrum rutrum neque. Aenean auctor gravida sem.\n\nPraesent id massa id nisl venenatis lacinia. Aenean sit amet justo. Morbi ut odio.\n\nCras mi pede, malesuada in, imperdiet et, commodo vulputate, justo. In blandit ultrices enim. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", 50 | "gender": "Male", 51 | "name": "Cristian Gilbanks", 52 | "review": "integrate extensible methodologies", 53 | "starrating": 5, 54 | "price": 30.99, 55 | "img": "5.png" 56 | }, 57 | { 58 | "id": "3d16728f-4b1d-449a-b002-4dc23f32c0cd", 59 | "color": "Red", 60 | "description": "Donec diam neque, vestibulum eget, vulputate ut, ultrices vel, augue. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec pharetra, magna vestibulum aliquet ultrices, erat tortor sollicitudin mi, sit amet lobortis sapien sapien non mi. Integer ac neque.\n\nDuis bibendum. Morbi non quam nec dui luctus rutrum. Nulla tellus.\n\nIn sagittis dui vel nisl. Duis ac nibh. Fusce lacus purus, aliquet at, feugiat non, pretium quis, lectus.", 61 | "gender": "Female", 62 | "name": "Kalila Gooms", 63 | "review": "utilize end-to-end functionalities", 64 | "starrating": 4, 65 | "price": 19.99, 66 | "img": "6.png" 67 | }, 68 | { 69 | "id": "0d5c91e6-8dbe-4c5b-b9fa-9e82986b1bdc", 70 | "color": "Maroon", 71 | "description": "Nulla ut erat id mauris vulputate elementum. Nullam varius. Nulla facilisi.\n\nCras non velit nec nisi vulputate nonummy. Maecenas tincidunt lacus at velit. Vivamus vel nulla eget eros elementum pellentesque.\n\nQuisque porta volutpat erat. Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla. Nunc purus.", 72 | "gender": "Male", 73 | "name": "Bartolemo Peckitt", 74 | "review": "enable cutting-edge e-services", 75 | "starrating": 3, 76 | "price": 130.0, 77 | "img": "7.png" 78 | }, 79 | { 80 | "id": "80175256-93a1-41b8-9895-a890ed0c61d1", 81 | "color": "Puce", 82 | "description": "Etiam vel augue. Vestibulum rutrum rutrum neque. Aenean auctor gravida sem.", 83 | "gender": "Female", 84 | "name": "Kathye Haith", 85 | "review": "innovate sexy portals", 86 | "starrating": 4, 87 | "price": 120.99, 88 | "img": "8.png" 89 | }, 90 | { 91 | "id": "b5438034-168a-4b51-9d2e-dd9fc336a242", 92 | "color": "Red", 93 | "description": "Integer tincidunt ante vel ipsum. Praesent blandit lacinia erat. Vestibulum sed magna at nunc commodo placerat.\n\nPraesent blandit. Nam nulla. Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.\n\nMorbi porttitor lorem id ligula. Suspendisse ornare consequat lectus. In est risus, auctor sed, tristique in, tempus sit amet, sem.", 94 | "gender": "Male", 95 | "name": "Armin Basilio", 96 | "review": "transform robust mindshare", 97 | "starrating": 5, 98 | "price": 20.99, 99 | "img": "9.png" 100 | }, 101 | { 102 | "id": "a5f39b32-e103-4e4a-ab73-0f719817b455", 103 | "color": "Fuscia", 104 | "description": "Phasellus sit amet erat. Nulla tempus. Vivamus in felis eu sapien cursus vestibulum.\n\nProin eu mi. Nulla ac enim. In tempor, turpis nec euismod scelerisque, quam turpis adipiscing lorem, vitae mattis nibh ligula nec sem.\n\nDuis aliquam convallis nunc. Proin at turpis a pede posuere nonummy. Integer non velit.", 105 | "gender": "Male", 106 | "name": "Cal Sterman", 107 | "review": "benchmark synergistic bandwidth", 108 | "starrating": 3, 109 | "price": 50.0, 110 | "img": "10.jpg" 111 | }, 112 | { 113 | "id": "19bdee56-525a-4cb7-bfeb-c6f46b6d639a", 114 | "color": "Mauv", 115 | "description": "Morbi porttitor lorem id ligula. Suspendisse ornare consequat lectus. In est risus, auctor sed, tristique in, tempus sit amet, sem.", 116 | "gender": "Female", 117 | "name": "Ardine Imlaw", 118 | "review": "repurpose robust e-business", 119 | "starrating": 4, 120 | "price": 65.0, 121 | "img": "11.png" 122 | }, 123 | { 124 | "id": "f8b11f7c-1306-4658-a66c-d1704e728349", 125 | "color": "Teal", 126 | "description": "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa. Donec dapibus. Duis at velit eu est congue elementum.", 127 | "gender": "Female", 128 | "name": "Noella Ruddom", 129 | "review": "redefine 24/365 eyeballs", 130 | "starrating": 3, 131 | "price": 72.0, 132 | "img": "12.png" 133 | }, 134 | { 135 | "id": "14fcaf19-465e-4275-8c2e-c69219e68f8c", 136 | "color": "Khaki", 137 | "description": "Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla. Sed vel enim sit amet nunc viverra dapibus. Nulla suscipit ligula in lacus.\n\nCurabitur at ipsum ac tellus semper interdum. Mauris ullamcorper purus sit amet nulla. Quisque arcu libero, rutrum ac, lobortis vel, dapibus at, diam.", 138 | "gender": "Female", 139 | "name": "Kirstyn Espadate", 140 | "review": "empower sticky web-readiness", 141 | "starrating": 4, 142 | "price": 59.99, 143 | "img": "13.png" 144 | }, 145 | { 146 | "id": "b1913a24-2556-4414-87cd-c6dcc3b98b53", 147 | "color": "Turquoise", 148 | "description": "Nullam sit amet turpis elementum ligula vehicula consequat. Morbi a ipsum. Integer a nibh.", 149 | "gender": "Male", 150 | "name": "Rourke Greet", 151 | "review": "deliver end-to-end solutions", 152 | "starrating": 5, 153 | "price": 49.99, 154 | "img": "14.png" 155 | }, 156 | { 157 | "id": "143badca-e3f0-452f-b973-c3917a5f0f70", 158 | "color": "Teal", 159 | "description": "Quisque porta volutpat erat. Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla. Nunc purus.\n\nPhasellus in felis. Donec semper sapien a libero. Nam dui.", 160 | "gender": "Female", 161 | "name": "Arden Stockbridge", 162 | "review": "orchestrate value-added infrastructures", 163 | "starrating": 4, 164 | "price": 100.0, 165 | "img": "15.png" 166 | }, 167 | { 168 | "id": "5cdef591-6b7f-4511-b59f-d0f55bdceeb9", 169 | "color": "Orange", 170 | "description": "Integer ac leo. Pellentesque ultrices mattis odio. Donec vitae nisi.", 171 | "gender": "Male", 172 | "name": "Sheffy Gunter", 173 | "review": "revolutionize vertical systems", 174 | "starrating": 5, 175 | "price": 33.99, 176 | "img": "16.jpg" 177 | }, 178 | { 179 | "id": "0bdf8654-7d44-46f5-bb1f-115b16b42344", 180 | "color": "Orange", 181 | "description": "Praesent blandit. Nam nulla. Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.\n\nMorbi porttitor lorem id ligula. Suspendisse ornare consequat lectus. In est risus, auctor sed, tristique in, tempus sit amet, sem.", 182 | "gender": "Female", 183 | "name": "Alexine Mulligan", 184 | "review": "architect real-time partnerships", 185 | "starrating": 3, 186 | "price": 13.99, 187 | "img": "17.png" 188 | }, 189 | { 190 | "id": "a89719a0-4ee2-4ba0-8a75-12a1654a66b1", 191 | "color": "Purple", 192 | "description": "In congue. Etiam justo. Etiam pretium iaculis justo.\n\nIn hac habitasse platea dictumst. Etiam faucibus cursus urna. Ut tellus.\n\nNulla ut erat id mauris vulputate elementum. Nullam varius. Nulla facilisi.", 193 | "gender": "Female", 194 | "name": "Micheline Charlson", 195 | "review": "innovate holistic markets", 196 | "starrating": 4, 197 | "price": 12.99, 198 | "img": "18.jpg" 199 | }, 200 | { 201 | "id": "8aa13b42-b074-43d2-8dcc-f3f1010f3adb", 202 | "color": "Purple", 203 | "description": "Phasellus in felis. Donec semper sapien a libero. Nam dui.\n\nProin leo odio, porttitor id, consequat in, consequat ut, nulla. Sed accumsan felis. Ut at dolor quis odio consequat varius.\n\nInteger ac leo. Pellentesque ultrices mattis odio. Donec vitae nisi.", 204 | "gender": "Male", 205 | "name": "Hughie Jeffryes", 206 | "review": "facilitate innovative portals", 207 | "starrating": 3, 208 | "price": 154.99, 209 | "img": "19.png" 210 | }, 211 | { 212 | "id": "c8b03500-5f07-4fb3-a933-1ee205ccd903", 213 | "color": "Puce", 214 | "description": "In hac habitasse platea dictumst. Etiam faucibus cursus urna. Ut tellus.\n\nNulla ut erat id mauris vulputate elementum. Nullam varius. Nulla facilisi.", 215 | "gender": "Male", 216 | "name": "Chet Corke", 217 | "review": "unleash synergistic models", 218 | "starrating": 5, 219 | "price": 33.99, 220 | "img": "1.jpg" 221 | }, 222 | { 223 | "id": "85641bb6-04e5-4393-be3e-1d4dea46cd54", 224 | "color": "Orange", 225 | "description": "Quisque porta volutpat erat. Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla. Nunc purus.\n\nPhasellus in felis. Donec semper sapien a libero. Nam dui.", 226 | "gender": "Female", 227 | "name": "Kamila Yggo", 228 | "review": "unleash strategic platforms", 229 | "starrating": 4, 230 | "price": 50.99, 231 | "img": "2.jpg" 232 | }, 233 | { 234 | "id": "1c8665b9-87cd-40d4-bc90-b28943825bd0", 235 | "color": "Purple", 236 | "description": "Quisque id justo sit amet sapien dignissim vestibulum. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla dapibus dolor vel est. Donec odio justo, sollicitudin ut, suscipit a, feugiat et, eros.\n\nVestibulum ac est lacinia nisi venenatis tristique. Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue. Aliquam erat volutpat.", 237 | "gender": "Female", 238 | "name": "Kandace Strickett", 239 | "review": "orchestrate visionary paradigms", 240 | "starrating": 4, 241 | "price": 50.99, 242 | "img": "3.jpg" 243 | }, 244 | { 245 | "id": "e0bce42b-e2cf-4aa2-805b-f4bbd8517d2a", 246 | "color": "Khaki", 247 | "description": "Phasellus in felis. Donec semper sapien a libero. Nam dui.", 248 | "gender": "Male", 249 | "name": "Kellby Arlott", 250 | "review": "enhance rich initiatives", 251 | "starrating": 4, 252 | "price": 19.99, 253 | "img": "4.jpg" 254 | }, 255 | { 256 | "id": "cc6998b6-4150-4234-b678-28460afc3a91", 257 | "color": "Purple", 258 | "description": "Integer tincidunt ante vel ipsum. Praesent blandit lacinia erat. Vestibulum sed magna at nunc commodo placerat.\n\nPraesent blandit. Nam nulla. Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.\n\nMorbi porttitor lorem id ligula. Suspendisse ornare consequat lectus. In est risus, auctor sed, tristique in, tempus sit amet, sem.", 259 | "gender": "Male", 260 | "name": "Bogey Cheyne", 261 | "review": "benchmark mission-critical convergence", 262 | "starrating": 5, 263 | "price": 110.0, 264 | "img": "5.png" 265 | }, 266 | { 267 | "id": "69d95efa-af3a-40d2-8da1-1c8f8700a131", 268 | "color": "Crimson", 269 | "description": "Maecenas tristique, est et tempus semper, est quam pharetra magna, ac consequat metus sapien ut nunc. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam. Suspendisse potenti.\n\nNullam porttitor lacus at turpis. Donec posuere metus vitae ipsum. Aliquam non mauris.\n\nMorbi non lectus. Aliquam sit amet diam in magna bibendum imperdiet. Nullam orci pede, venenatis non, sodales sed, tincidunt eu, felis.", 270 | "gender": "Female", 271 | "name": "Sybila Ehlerding", 272 | "review": "aggregate granular systems", 273 | "starrating": 3, 274 | "price": 19.99, 275 | "img": "6.png" 276 | }, 277 | { 278 | "id": "ab614297-fff2-4b34-bcf5-bc3c10127c63", 279 | "color": "Purple", 280 | "description": "Curabitur in libero ut massa volutpat convallis. Morbi odio odio, elementum eu, interdum eu, tincidunt in, leo. Maecenas pulvinar lobortis est.\n\nPhasellus sit amet erat. Nulla tempus. Vivamus in felis eu sapien cursus vestibulum.\n\nProin eu mi. Nulla ac enim. In tempor, turpis nec euismod scelerisque, quam turpis adipiscing lorem, vitae mattis nibh ligula nec sem.", 281 | "gender": "Male", 282 | "name": "Hadrian Hambatch", 283 | "review": "transition granular markets", 284 | "starrating": 3, 285 | "price": 190.99, 286 | "img": "7.png" 287 | }, 288 | { 289 | "id": "18d4cd08-134f-4df5-a19a-b782a59ae52b", 290 | "color": "Orange", 291 | "description": "Proin eu mi. Nulla ac enim. In tempor, turpis nec euismod scelerisque, quam turpis adipiscing lorem, vitae mattis nibh ligula nec sem.\n\nDuis aliquam convallis nunc. Proin at turpis a pede posuere nonummy. Integer non velit.\n\nDonec diam neque, vestibulum eget, vulputate ut, ultrices vel, augue. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec pharetra, magna vestibulum aliquet ultrices, erat tortor sollicitudin mi, sit amet lobortis sapien sapien non mi. Integer ac neque.", 292 | "gender": "Female", 293 | "name": "Franciska Hallett", 294 | "review": "generate customized e-services", 295 | "starrating": 4, 296 | "price": 20.0, 297 | "img": "8.png" 298 | }, 299 | { 300 | "id": "92f8048f-3ee2-4428-96b0-66fd54b6e98f", 301 | "color": "Orange", 302 | "description": "Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo. In blandit ultrices enim. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", 303 | "gender": "Male", 304 | "name": "Andrej Wilfing", 305 | "review": "e-enable viral web services", 306 | "starrating": 3, 307 | "price": 15.99, 308 | "img": "9.png" 309 | }, 310 | { 311 | "id": "4cee7b59-30dd-4378-b1c0-e4ad055a6a19", 312 | "color": "Aquamarine", 313 | "description": "Integer tincidunt ante vel ipsum. Praesent blandit lacinia erat. Vestibulum sed magna at nunc commodo placerat.\n\nPraesent blandit. Nam nulla. Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.\n\nMorbi porttitor lorem id ligula. Suspendisse ornare consequat lectus. In est risus, auctor sed, tristique in, tempus sit amet, sem.", 314 | "gender": "Female", 315 | "name": "Rori Marishenko", 316 | "review": "recontextualize B2B markets", 317 | "starrating": 3, 318 | "price": 190.0, 319 | "img": "10.jpg" 320 | }, 321 | { 322 | "id": "f92c7d89-78e9-4616-8876-e0088c53483e", 323 | "color": "Pink", 324 | "description": "Nulla ut erat id mauris vulputate elementum. Nullam varius. Nulla facilisi.", 325 | "gender": "Male", 326 | "name": "Silas Cornwell", 327 | "review": "architect collaborative initiatives", 328 | "starrating": 3, 329 | "price": 35.99, 330 | "img": "11.png" 331 | } 332 | ] 333 | --------------------------------------------------------------------------------