├── 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 |
2 |
5 |
6 |
7 |
20 |
21 |
--------------------------------------------------------------------------------
/pages/women.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
20 |
21 |
--------------------------------------------------------------------------------
/pages/all.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
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 |
2 |
3 |
4 | About
5 | Company
6 | Locations
7 | Contact
8 | Hours
9 |
10 |
11 | Twitter
12 | Facebook
13 | Instagram
14 | LinkedIn
15 |
16 |
20 |
21 |
22 |
23 |
26 |
27 |
--------------------------------------------------------------------------------
/pages/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | 50%
10 | Storewide Sale
11 | Summer
12 | All accessories
13 |
14 |
15 |
16 |
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 |
2 |
9 |
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 |
2 |
21 |
22 |
23 |
32 |
33 |
--------------------------------------------------------------------------------
/components/AppCartSteps.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
01
5 | Shopping Cart
6 |
7 |
8 |
02
9 | Check out
10 |
11 |
12 |
03
13 | Order Complete
14 |
15 |
16 |
17 |
18 |
29 |
30 |
--------------------------------------------------------------------------------
/pages/cart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Your Cart
6 |
7 |
10 |
11 |
14 |
15 |
16 | Success!
17 | Thank you for your purchase. You'll be receiving your items in 4 business days.
18 | Forgot something?
19 |
20 | Back to Home
21 |
22 |
23 |
24 |
25 | Oops, something went wrong. Redirecting you to your cart to try again.
26 |
27 |
28 |
29 |
30 |
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 |
2 |
3 |
4 |
5 |
{{ box.heading }}
6 |
{{ box.details }}
7 |
8 |
9 |
10 |
11 |
36 |
37 |
--------------------------------------------------------------------------------
/components/AppTextlockup.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | New
10 |
11 |
12 | Men Shoes
13 |
14 |
15 | Collection
16 |
17 |
18 | Street Style New Fashion
19 |
20 |
21 |
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 |
2 |
3 | Skyline Ivy
4 |
5 |
6 |
7 | Home
8 |
9 |
10 | All
11 |
12 |
13 | Women
14 |
15 |
16 | Men
17 |
18 |
19 | {{ cartCount }}
20 | Cart
21 |
22 |
23 |
24 |
25 |
26 |
27 |
36 |
37 |
--------------------------------------------------------------------------------
/components/AppFeaturedProducts.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Featured Products
5 |
6 |
7 |
8 |
9 |
{{ product.name }}
10 |
{{ product.price | dollar }}
11 |
12 | View Item >
13 |
14 |
15 |
16 |
17 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
17 |
{{ item.name }}
18 |
{{ item.price | dollar }}
19 |
20 | View Item >
21 |
22 |
23 |
24 |
44 |
45 |
46 |
47 |
73 |
74 |
--------------------------------------------------------------------------------
/components/AppCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Please enter your payment details:
5 | Email
6 |
7 |
8 |
9 | Credit Card
10 |
11 |
12 | Test using this credit card:
13 | 4242 4242 4242 4242 , and enter any 5 digits for the zip code
14 |
15 |
23 | Pay with credit card
28 |
29 |
30 |
31 |
32 |
Oh No!
33 |
Something went wrong!
34 |
Please try again
35 |
36 |
37 |
38 |
Please hold, we're filling up your cart with goodies
39 |
Placeholder loader
40 |
41 |
42 |
43 |
Success!
44 |
45 |
46 |
47 |
48 |
49 |
83 |
84 |
--------------------------------------------------------------------------------
/components/AppCartDisplay.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Product
7 | Price
8 | Quantity
9 | Total
10 |
11 |
12 |
13 |
14 | {{ item.name }}
15 |
16 |
17 | {{ item.price | dollar }}
18 |
19 |
20 | {{ item.quantity }}
21 |
22 | {{ item.quantity * item.price | dollar }}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | Subtotal:
32 |
33 |
Shipping:
34 |
Total:
35 |
36 |
37 |
38 | {{ cartTotal | dollar }}
39 |
40 |
Free Shipping
41 |
{{ cartTotal | dollar }}
42 |
43 |
44 |
45 |
46 |
47 |
48 | Your cart is empty, fill it up!
49 |
50 | Back Home
51 |
52 |
53 |
54 |
55 |
56 |
71 |
72 |
--------------------------------------------------------------------------------
/components/AppLoader.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
13 |
16 |
19 |
22 |
25 |
28 |
31 |
34 |
37 |
40 |
43 |
46 |
49 |
50 |
51 |
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 | [](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 | > 
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{ product.name }}
9 |
16 | {{ product.price | dollar }}
17 | {{ product.description }}
18 | Lorem ipsum, dolor sit amet consectetur adipisicing elit. Iusto velit dolores repudiandae animi quidem, eveniet quod dolor facilis dicta eligendi ullam error. Assumenda in fugiat natus enim similique nam itaque.
19 |
20 | 0 ? quantity-- : quantity = 0">-
21 |
22 | +
23 |
24 |
25 | Available in additional colors:
26 |
27 | {{ product.color }}
28 |
29 |
30 |
31 | Add to Cart
32 |
33 |
34 |
35 |
36 |
37 |
Reviews
38 |
39 |
46 |
{{ product.review }}
47 |
Lorem ipsum dolor sit amet consectetur adipisicing elit. Rerum iusto placeat consequatur voluptas sit mollitia ratione autem, atque sequi odio laborum, recusandae quia distinctio voluptatibus sint, quae aliquid possimus exercitationem.
48 |
49 |
50 |
51 |
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 |
--------------------------------------------------------------------------------