├── static ├── icon.png ├── favicon.ico ├── assets │ ├── logo.png │ ├── storybook │ │ ├── logo.png │ │ ├── Home │ │ │ ├── apple.png │ │ │ ├── bannerA.jpg │ │ │ ├── bannerB.jpg │ │ │ ├── bannerC.jpg │ │ │ ├── bannerD.png │ │ │ ├── bannerE.jpg │ │ │ ├── bannerF.jpg │ │ │ ├── bannerG.jpg │ │ │ ├── bannerH.jpg │ │ │ ├── bannerI.jpg │ │ │ ├── bannerJ.jpg │ │ │ ├── bannerK.jpg │ │ │ ├── google.png │ │ │ ├── imageA.jpg │ │ │ ├── imageB.jpg │ │ │ ├── imageC.jpg │ │ │ ├── imageD.jpg │ │ │ ├── bannerHM.jpg │ │ │ ├── newsletter.jpg │ │ │ ├── productA.jpg │ │ │ ├── productB.jpg │ │ │ ├── productC.jpg │ │ │ └── placeholderA.jpg │ │ ├── SfHero │ │ │ └── hero.png │ │ ├── SfStore │ │ │ ├── storeA.png │ │ │ └── storeB.png │ │ ├── checkout │ │ │ ├── debit.png │ │ │ ├── electron.png │ │ │ ├── product.png │ │ │ └── mastercard.png │ │ ├── Product │ │ │ ├── productA.jpg │ │ │ ├── productB.jpg │ │ │ └── productM.jpg │ │ ├── SfBanner │ │ │ ├── Banner1.jpg │ │ │ └── Banner2.jpg │ │ ├── SfImage │ │ │ ├── placeholder.png │ │ │ ├── product-109x164.jpg │ │ │ └── product-216x326.jpg │ │ ├── SfMegaMenu │ │ │ ├── bannerBeachBag.jpg │ │ │ ├── bannerSandals.jpg │ │ │ ├── bannerBeachBag-full.png │ │ │ └── bannerSandals-full.png │ │ ├── SfProductCard │ │ │ ├── no-product.jpg │ │ │ └── product_thumb.jpg │ │ ├── SfGroupedProduct │ │ │ ├── product-black.png │ │ │ ├── product-green.png │ │ │ └── product-white.png │ │ ├── SfFooter │ │ │ ├── facebook.svg │ │ │ ├── youtube.svg │ │ │ ├── twitter.svg │ │ │ └── pinterest.svg │ │ └── SfOptions │ │ │ ├── profile.svg │ │ │ ├── home.svg │ │ │ └── heart.svg │ └── logo.svg └── README.md ├── config ├── buildModules.js ├── axios.js ├── build.js ├── css.js ├── constants.js ├── plugins.js ├── toast.js ├── modules.js ├── proxy.js ├── sentry.js ├── env.js └── head.js ├── layouts ├── errorhoc.vue ├── README.md └── default.vue ├── plugins ├── vueAppend.js ├── vuelidate.js ├── queries.js ├── vueCountryRegionSelect.js ├── README.md └── axios.js ├── assets ├── sass │ ├── components │ │ ├── loader.scss │ │ ├── customerprofile.scss │ │ ├── orders.scss │ │ ├── myprofile.scss │ │ ├── wishlist.scss │ │ ├── checkout │ │ │ ├── ordersummary.scss │ │ │ ├── checkout.scss │ │ │ ├── orderreview.scss │ │ │ ├── payment.scss │ │ │ ├── personal.scss │ │ │ └── shipping.scss │ │ ├── shippingaddress.scss │ │ └── cartsidebar.scss │ ├── _mixins.scss │ ├── _variables.scss │ ├── app.scss │ ├── vendor │ │ └── toasted.scss │ ├── pages │ │ ├── register.scss │ │ ├── error.scss │ │ ├── login.scss │ │ ├── home.scss │ │ └── cart.scss │ └── layouts │ │ └── default.scss └── README.md ├── constants ├── checkouttype.js ├── index.js ├── color.js ├── menu.js ├── characteristics.js └── breadcrumbs.js ├── test ├── utils │ └── dotenv-test.js ├── components │ └── Logo.spec.js ├── home.spec.js ├── store │ └── customer.spec.js └── api │ └── address │ └── getAllAddresses.spec.js ├── set-heroku-env.sh ├── utils ├── queries │ ├── customer-logout.js │ ├── customer-login.js │ ├── category.js │ ├── homepage-content-widgets.js │ ├── get-customer.js │ ├── index.js │ ├── products-by-category.js │ └── product-by-slug.js ├── validation │ └── index.js ├── language │ ├── sample_data.json │ └── index.js ├── script │ ├── create-storefront-site.js │ ├── create-storefront-token.js │ ├── create-storefront-channel.js │ └── create-storefront-route.js ├── permission │ └── index.js ├── storage │ └── index.js └── auth │ └── index.js ├── .vscode ├── settings.json └── launch.json ├── sfui.scss ├── middleware ├── getCheckout.js ├── authenticated.js └── README.md ├── components ├── README.md ├── _profile │ ├── index.js │ ├── OrderHistory.vue │ ├── Wishlist.vue │ └── MyProfile.vue ├── checkout │ ├── index.js │ ├── basic │ │ └── SpDropdown.vue │ ├── OrderReview.vue │ ├── OrderSummary.vue │ └── PersonalDetails.vue ├── Loader.vue ├── Logo.vue ├── CustomerProfile.vue └── LayoutDefaultHeader.vue ├── jsconfig.json ├── .editorconfig ├── .babelrc ├── .prettierrc ├── pages ├── README.md ├── error.vue ├── login │ └── index.vue └── checkout │ └── index.vue ├── .env.test ├── netlify.toml ├── api ├── middleware │ └── index.js ├── index.js ├── controller │ ├── order.js │ ├── storefront.js │ ├── customer.js │ ├── address.js │ ├── cart.js │ ├── product.js │ └── checkout.js ├── utils │ └── axios.js └── routes │ └── index.js ├── store ├── README.md ├── order.js ├── storefront.js ├── customer.js └── address.js ├── functions ├── getCategories.js ├── createCart.js ├── getWishlist.js ├── createOrder.js ├── createWishlist.js ├── customerRegister.js ├── getHomePageContentWidgets.js ├── getProductBySlug.js ├── getAllAddresses.js ├── getCart.js ├── storefront.js ├── deleteAddress.js ├── deleteCartItem.js ├── getAllOrders.js ├── getCheckout.js ├── deleteWishlistItem.js ├── getPaymentMethodByOrder.js ├── addAddress.js ├── addCoupons.js ├── searchProductByKey.js ├── customerLogOut.js ├── getProductOption.js ├── addCartItem.js ├── getProductsByCategory.js ├── getProductsByIds.js ├── updateCartWithCustomerId.js ├── addToWishlistItem.js ├── updateAddress.js ├── updateCartItem.js ├── setBillingAddressToCheckout.js ├── setConsignmentToCheckout.js ├── updateShippingOption.js ├── updateConsignmentToCheckout.js ├── middleware │ └── permission.js ├── customerLogin.js └── processPayment.js ├── jest.config.js ├── nuxt.config.js ├── jest.setup.js ├── .gitignore ├── .eslintrc.js ├── package.json ├── .env.example └── vercel.json /static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/icon.png -------------------------------------------------------------------------------- /config/buildModules.js: -------------------------------------------------------------------------------- 1 | export default ['@nuxtjs/dotenv', '@nuxtjs/pwa', '@nuxtjs/eslint-module']; 2 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /layouts/errorhoc.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /plugins/vueAppend.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import vueAppend from 'vue-append'; 3 | Vue.use(vueAppend); 4 | -------------------------------------------------------------------------------- /plugins/vuelidate.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuelidate from 'vuelidate'; 3 | Vue.use(Vuelidate); 4 | -------------------------------------------------------------------------------- /static/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/logo.png -------------------------------------------------------------------------------- /assets/sass/components/loader.scss: -------------------------------------------------------------------------------- 1 | #loader { 2 | width: 100vw; 3 | z-index: 10000; 4 | padding: 1rem 0; 5 | } 6 | -------------------------------------------------------------------------------- /static/assets/storybook/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/logo.png -------------------------------------------------------------------------------- /static/assets/storybook/Home/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/apple.png -------------------------------------------------------------------------------- /config/axios.js: -------------------------------------------------------------------------------- 1 | export default { 2 | baseURL: process.env.BASE_URL || 'http://localhost:3000', 3 | debug: false, 4 | proxy: true 5 | }; 6 | -------------------------------------------------------------------------------- /config/build.js: -------------------------------------------------------------------------------- 1 | export default { 2 | /* 3 | ** You can extend webpack config here 4 | */ 5 | transpile: [/^@storefront-ui/] 6 | }; 7 | -------------------------------------------------------------------------------- /plugins/queries.js: -------------------------------------------------------------------------------- 1 | import queries from '@/utils/queries'; 2 | 3 | export default function (_, inject) { 4 | inject('queries', queries); 5 | } 6 | -------------------------------------------------------------------------------- /static/assets/storybook/Home/bannerA.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/bannerA.jpg -------------------------------------------------------------------------------- /static/assets/storybook/Home/bannerB.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/bannerB.jpg -------------------------------------------------------------------------------- /static/assets/storybook/Home/bannerC.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/bannerC.jpg -------------------------------------------------------------------------------- /static/assets/storybook/Home/bannerD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/bannerD.png -------------------------------------------------------------------------------- /static/assets/storybook/Home/bannerE.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/bannerE.jpg -------------------------------------------------------------------------------- /static/assets/storybook/Home/bannerF.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/bannerF.jpg -------------------------------------------------------------------------------- /static/assets/storybook/Home/bannerG.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/bannerG.jpg -------------------------------------------------------------------------------- /static/assets/storybook/Home/bannerH.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/bannerH.jpg -------------------------------------------------------------------------------- /static/assets/storybook/Home/bannerI.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/bannerI.jpg -------------------------------------------------------------------------------- /static/assets/storybook/Home/bannerJ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/bannerJ.jpg -------------------------------------------------------------------------------- /static/assets/storybook/Home/bannerK.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/bannerK.jpg -------------------------------------------------------------------------------- /static/assets/storybook/Home/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/google.png -------------------------------------------------------------------------------- /static/assets/storybook/Home/imageA.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/imageA.jpg -------------------------------------------------------------------------------- /static/assets/storybook/Home/imageB.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/imageB.jpg -------------------------------------------------------------------------------- /static/assets/storybook/Home/imageC.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/imageC.jpg -------------------------------------------------------------------------------- /static/assets/storybook/Home/imageD.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/imageD.jpg -------------------------------------------------------------------------------- /static/assets/storybook/SfHero/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/SfHero/hero.png -------------------------------------------------------------------------------- /constants/checkouttype.js: -------------------------------------------------------------------------------- 1 | export const CHECKOUT_TYPE = { 2 | CUSTOM: 'custom', 3 | REDIRECTED: 'redirected', 4 | EMBEDDED: 'embedded' 5 | }; 6 | -------------------------------------------------------------------------------- /static/assets/storybook/Home/bannerHM.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/bannerHM.jpg -------------------------------------------------------------------------------- /static/assets/storybook/Home/newsletter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/newsletter.jpg -------------------------------------------------------------------------------- /static/assets/storybook/Home/productA.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/productA.jpg -------------------------------------------------------------------------------- /static/assets/storybook/Home/productB.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/productB.jpg -------------------------------------------------------------------------------- /static/assets/storybook/Home/productC.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/productC.jpg -------------------------------------------------------------------------------- /static/assets/storybook/SfStore/storeA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/SfStore/storeA.png -------------------------------------------------------------------------------- /static/assets/storybook/SfStore/storeB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/SfStore/storeB.png -------------------------------------------------------------------------------- /static/assets/storybook/checkout/debit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/checkout/debit.png -------------------------------------------------------------------------------- /config/css.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | src: '~assets/sass/app.scss', 4 | lang: 'scss' 5 | }, 6 | '@storefront-ui/vue/styles.scss' 7 | ]; 8 | -------------------------------------------------------------------------------- /static/assets/storybook/Home/placeholderA.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Home/placeholderA.jpg -------------------------------------------------------------------------------- /static/assets/storybook/Product/productA.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Product/productA.jpg -------------------------------------------------------------------------------- /static/assets/storybook/Product/productB.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Product/productB.jpg -------------------------------------------------------------------------------- /static/assets/storybook/Product/productM.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/Product/productM.jpg -------------------------------------------------------------------------------- /static/assets/storybook/SfBanner/Banner1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/SfBanner/Banner1.jpg -------------------------------------------------------------------------------- /static/assets/storybook/SfBanner/Banner2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/SfBanner/Banner2.jpg -------------------------------------------------------------------------------- /static/assets/storybook/checkout/electron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/checkout/electron.png -------------------------------------------------------------------------------- /static/assets/storybook/checkout/product.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/checkout/product.png -------------------------------------------------------------------------------- /static/assets/storybook/SfImage/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/SfImage/placeholder.png -------------------------------------------------------------------------------- /static/assets/storybook/checkout/mastercard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/checkout/mastercard.png -------------------------------------------------------------------------------- /plugins/vueCountryRegionSelect.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import vueCountryRegionSelect from 'vue-country-region-select'; 3 | Vue.use(vueCountryRegionSelect); 4 | -------------------------------------------------------------------------------- /test/utils/dotenv-test.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import dotenv from 'dotenv'; 3 | 4 | dotenv.config({ path: path.resolve(__dirname, '../../.env.test') }); 5 | -------------------------------------------------------------------------------- /static/assets/storybook/SfImage/product-109x164.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/SfImage/product-109x164.jpg -------------------------------------------------------------------------------- /static/assets/storybook/SfImage/product-216x326.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/SfImage/product-216x326.jpg -------------------------------------------------------------------------------- /static/assets/storybook/SfMegaMenu/bannerBeachBag.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/SfMegaMenu/bannerBeachBag.jpg -------------------------------------------------------------------------------- /static/assets/storybook/SfMegaMenu/bannerSandals.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/SfMegaMenu/bannerSandals.jpg -------------------------------------------------------------------------------- /static/assets/storybook/SfProductCard/no-product.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/SfProductCard/no-product.jpg -------------------------------------------------------------------------------- /assets/sass/_mixins.scss: -------------------------------------------------------------------------------- 1 | @import '~@storefront-ui/vue/styles'; 2 | @mixin for-desktop { 3 | @media screen and (min-width: $desktop-min) { 4 | @content; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /static/assets/storybook/SfProductCard/product_thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/SfProductCard/product_thumb.jpg -------------------------------------------------------------------------------- /set-heroku-env.sh: -------------------------------------------------------------------------------- 1 | MAIN_VARS="HOST='0.0.0.0' NODE_ENV='production'" 2 | # ENV_VARS=$(grep -v '#.*' .env) 3 | CMD="heroku config:set" 4 | QUERY="$CMD $MAIN_VARS"; 5 | eval $QUERY; 6 | -------------------------------------------------------------------------------- /static/assets/storybook/SfGroupedProduct/product-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/SfGroupedProduct/product-black.png -------------------------------------------------------------------------------- /static/assets/storybook/SfGroupedProduct/product-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/SfGroupedProduct/product-green.png -------------------------------------------------------------------------------- /static/assets/storybook/SfGroupedProduct/product-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/SfGroupedProduct/product-white.png -------------------------------------------------------------------------------- /static/assets/storybook/SfMegaMenu/bannerBeachBag-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/SfMegaMenu/bannerBeachBag-full.png -------------------------------------------------------------------------------- /static/assets/storybook/SfMegaMenu/bannerSandals-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/bc-nuxt-vue-starter/HEAD/static/assets/storybook/SfMegaMenu/bannerSandals-full.png -------------------------------------------------------------------------------- /assets/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | $brand-secondary: #4d9c83; // #0a8080; 2 | 3 | // Borders 4 | $border-radius: 0.125rem; 5 | $border-radius-lg: 0.75rem; 6 | $border-radius-sm: 0.25rem; 7 | -------------------------------------------------------------------------------- /constants/index.js: -------------------------------------------------------------------------------- 1 | export * from './menu'; 2 | export * from './color'; 3 | export * from './breadcrumbs'; 4 | export * from './checkouttype'; 5 | export * from './characteristics'; 6 | -------------------------------------------------------------------------------- /config/constants.js: -------------------------------------------------------------------------------- 1 | export const API_URL = 2 | process.env.DEPLOY_PLATFORM === 'netlify' 3 | ? process.env.NETLIFY_API_URL || 'http://localhost:8888/.netlify/functions' 4 | : ''; 5 | -------------------------------------------------------------------------------- /config/plugins.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | '@/plugins/axios', 3 | '@/plugins/queries', 4 | '@/plugins/vuelidate', 5 | '@/plugins/vueCountryRegionSelect', 6 | '@/plugins/vueAppend' 7 | ]; 8 | -------------------------------------------------------------------------------- /utils/queries/customer-logout.js: -------------------------------------------------------------------------------- 1 | module.exports.customerLogOut = () => { 2 | return ` 3 | mutation Logout { 4 | logout { 5 | result 6 | } 7 | } 8 | `; 9 | }; 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /config/toast.js: -------------------------------------------------------------------------------- 1 | export default { 2 | position: 'bottom-left', 3 | theme: 'custom', 4 | duration: 2500, 5 | register: [ 6 | { 7 | // Register custom toasts 8 | } 9 | ] 10 | }; 11 | -------------------------------------------------------------------------------- /sfui.scss: -------------------------------------------------------------------------------- 1 | /* Here you can override global SCSS variables */ 2 | 3 | // global override excluding scoped components 4 | // $button-padding: 5px !default; 5 | // global override including scoped componentss 6 | // $button-padding: 50px; -------------------------------------------------------------------------------- /middleware/getCheckout.js: -------------------------------------------------------------------------------- 1 | import { getCartId } from '~/utils/storage'; 2 | 3 | export default function ({ store, redirect }) { 4 | if (getCartId()) store.dispatch('checkout/getCheckout'); 5 | else return redirect('/products'); 6 | } 7 | -------------------------------------------------------------------------------- /components/README.md: -------------------------------------------------------------------------------- 1 | # COMPONENTS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | The components directory contains your Vue.js Components. 6 | 7 | _Nuxt.js doesn't supercharge these components._ 8 | -------------------------------------------------------------------------------- /utils/queries/customer-login.js: -------------------------------------------------------------------------------- 1 | module.exports.customerLogin = () => { 2 | return ` 3 | mutation Login($email: String!, $password: String!) { 4 | login(email: $email, password: $password) { 5 | result 6 | } 7 | } 8 | `; 9 | }; 10 | -------------------------------------------------------------------------------- /config/modules.js: -------------------------------------------------------------------------------- 1 | const modules = [ 2 | '@nuxtjs/dotenv', 3 | '@nuxtjs/axios', 4 | '@nuxtjs/pwa', 5 | '@nuxtjs/toast', 6 | '@nuxtjs/proxy' 7 | ]; 8 | if (process.env.NODE_ENV === 'production') modules.push('@nuxtjs/sentry'); 9 | export default modules; 10 | -------------------------------------------------------------------------------- /constants/color.js: -------------------------------------------------------------------------------- 1 | export const color = { 2 | Silver: '#C0C0C0', 3 | Black: '#000000', 4 | Purple: '#800080', 5 | Blue: '#0000FF', 6 | Green: '#00FF00', 7 | Yellow: '#FFFF00', 8 | Orange: '#FFA500', 9 | Red: '#FF0000', 10 | Pink: '#FFC0CB' 11 | }; 12 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "~/*": ["./*"], 6 | "@/*": ["./*"], 7 | "~~/*": ["./*"], 8 | "@@/*": ["./*"] 9 | } 10 | }, 11 | "exclude": ["node_modules", ".nuxt", "dist"] 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /components/_profile/index.js: -------------------------------------------------------------------------------- 1 | import MyProfile from './MyProfile.vue'; 2 | import OrderHistory from './OrderHistory.vue'; 3 | import ShippingDetails from './ShippingDetails'; 4 | import Wishlist from './Wishlist'; 5 | 6 | export { MyProfile, OrderHistory, ShippingDetails, Wishlist }; 7 | -------------------------------------------------------------------------------- /utils/queries/category.js: -------------------------------------------------------------------------------- 1 | module.exports.category = () => { 2 | return ` 3 | query paginateProducts{ 4 | site { 5 | categoryTree { 6 | name 7 | path 8 | productCount 9 | } 10 | } 11 | } 12 | `; 13 | }; 14 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "targets": { 9 | "node": "current" 10 | } 11 | } 12 | ] 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "bracketSpacing": true, 8 | "jsxBracketSameLine": false, 9 | "arrowParens": "always", 10 | "endOfLine": "lf", 11 | "trailingComma": "none" 12 | } 13 | -------------------------------------------------------------------------------- /middleware/authenticated.js: -------------------------------------------------------------------------------- 1 | export default async function ({ store, redirect }) { 2 | const storefrontStatus = await store.dispatch( 3 | 'storefront/getStorefrontStatus' 4 | ); 5 | if (storefrontStatus) store.dispatch('customer/isLoggedIn'); 6 | else return redirect('/error'); 7 | } 8 | -------------------------------------------------------------------------------- /layouts/README.md: -------------------------------------------------------------------------------- 1 | # LAYOUTS 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 Layouts. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/views#layouts). 8 | -------------------------------------------------------------------------------- /constants/menu.js: -------------------------------------------------------------------------------- 1 | export const menu = [ 2 | { 3 | link: '/', 4 | name: 'Home' 5 | }, 6 | { 7 | link: '/products', 8 | name: 'Products' 9 | }, 10 | { 11 | link: '/cart', 12 | name: 'Cart' 13 | }, 14 | { 15 | link: '/login', 16 | name: 'Profile' 17 | } 18 | ]; 19 | -------------------------------------------------------------------------------- /config/proxy.js: -------------------------------------------------------------------------------- 1 | export default { 2 | '/graphql': { 3 | target: process.env.BASE_URL || 'http"//localhost:3000', 4 | pathRewrite: { '^/graphql': '/graphql' } 5 | }, 6 | '/api': { 7 | target: process.env.BC_API_URL || 'http"//localhost:3000', 8 | pathRewrite: { '^/api': '' } 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /pages/README.md: -------------------------------------------------------------------------------- 1 | # PAGES 2 | 3 | This directory contains your Application Views and Routes. 4 | The framework reads all the `*.vue` files inside this directory and creates the router of your application. 5 | 6 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing). 7 | -------------------------------------------------------------------------------- /test/components/Logo.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import Logo from '@/components/Logo.vue'; 3 | 4 | describe('Logo', () => { 5 | test('mounts logo component properly', () => { 6 | const wrapper = shallowMount(Logo, {}); 7 | expect(wrapper.vm).toBeTruthy(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /static/assets/storybook/SfFooter/facebook.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /utils/validation/index.js: -------------------------------------------------------------------------------- 1 | export const passwordRegexValidate = (pwd) => { 2 | const regex = 3 | '^(((?=.*[a-z])(?=.*[A-Z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{6,})'; 4 | return !!pwd.match(regex); 5 | }; 6 | 7 | export const checkFormValidation = (data) => { 8 | const { $dirty, $error } = data; 9 | return !$dirty ? true : !$error; 10 | }; 11 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | APP_NAME='BigCommerce Nuxt Vue Starter' 2 | PORT=3000 3 | JWT_SECRET='test secret' 4 | BC_API_URL='https://api.bigcommerce.com' 5 | STORE_HASH='test hash' 6 | API_TOKEN='test api token' 7 | API_CLIENT_ID='test api id' 8 | API_SECRET='test api secrent' 9 | CHANNEL_ID=123123 10 | BASE_URL='https://store-test-123123.mybigcommerce.com' 11 | STOREFRONT_API_TOKEN='test' 12 | -------------------------------------------------------------------------------- /config/sentry.js: -------------------------------------------------------------------------------- 1 | export default { 2 | dsn: process.env.SENTRY_DSN, // Enter your project's DSN here 3 | // Additional Module Options go here 4 | // https://sentry.nuxtjs.org/sentry/options 5 | config: { 6 | // Add native Sentry config here 7 | // https://docs.sentry.io/platforms/javascript/guides/vue/configuration/options/ 8 | debug: false 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [dev] 2 | command = "npm run dev" 3 | [build] 4 | command = "npm run generate" 5 | publish = "dist" 6 | functions = 'functions' # directs netlify to where your functions directory is located 7 | 8 | [[headers]] 9 | # Define which paths this specific [[headers]] block will cover. 10 | for = "/*" 11 | [headers.values] 12 | Access-Control-Allow-Origin = "*" 13 | -------------------------------------------------------------------------------- /test/home.spec.js: -------------------------------------------------------------------------------- 1 | import { OK } from 'http-status'; 2 | import request from 'supertest'; 3 | import app from '../api/index'; 4 | 5 | describe('Home API Test', () => { 6 | it('should successfully call API', async () => { 7 | const response = await request(app).get('/ping').send(); 8 | 9 | expect(response.status).toBe(OK); 10 | expect(response.body).toBe('PONG'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /utils/queries/homepage-content-widgets.js: -------------------------------------------------------------------------------- 1 | module.exports.homePageContentWidgets = () => { 2 | return ` 3 | query getHomePageContentWidgets { 4 | site { 5 | content { 6 | renderedRegionsByPageType(pageType: HOME) { 7 | regions { 8 | name 9 | html 10 | } 11 | } 12 | } 13 | } 14 | } 15 | `; 16 | }; 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /api/middleware/index.js: -------------------------------------------------------------------------------- 1 | import { checkApiAccessPermission } from '../../utils/permission'; 2 | 3 | export const permissionMiddleware = async (req, res, next) => { 4 | try { 5 | const path = req.route.path.split('/')[1]; 6 | const permissionString = `${req.method}_${path}`; 7 | checkApiAccessPermission(permissionString); 8 | next(); 9 | } catch (error) { 10 | res.status(403).json('API permission is denied'); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /components/checkout/index.js: -------------------------------------------------------------------------------- 1 | import PersonalDetails from './PersonalDetails.vue'; 2 | import Shipping from './Shipping.vue'; 3 | import Payment from './Payment.vue'; 4 | import ConfirmOrder from './ConfirmOrder.vue'; 5 | import OrderSummary from './OrderSummary.vue'; 6 | import OrderReview from './OrderReview.vue'; 7 | export { 8 | PersonalDetails, 9 | Shipping, 10 | Payment, 11 | ConfirmOrder, 12 | OrderSummary, 13 | OrderReview 14 | }; 15 | -------------------------------------------------------------------------------- /store/README.md: -------------------------------------------------------------------------------- 1 | # STORE 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 Vuex Store files. 6 | Vuex Store option is implemented in the Nuxt.js framework. 7 | 8 | Creating a file in this directory automatically activates the option in the framework. 9 | 10 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store). 11 | -------------------------------------------------------------------------------- /static/README.md: -------------------------------------------------------------------------------- 1 | # STATIC 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 static files. 6 | Each file inside this directory is mapped to `/`. 7 | Thus you'd want to delete this README.md before deploying to production. 8 | 9 | Example: `/static/robots.txt` is mapped as `/robots.txt`. 10 | 11 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#static). 12 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import routes from './routes'; 3 | 4 | const app = express(); 5 | 6 | app.use(express.urlencoded({ extended: true })); 7 | app.use(express.json()); 8 | app.use(routes); 9 | app.get('/ping', function (req, res) { 10 | res.json('PONG'); 11 | }); 12 | 13 | if (require.main === module) { 14 | const port = 3001; 15 | app.listen(port, () => { 16 | console.log(`API server listening on port ${port}`); 17 | }); 18 | } 19 | 20 | export default app; 21 | -------------------------------------------------------------------------------- /constants/characteristics.js: -------------------------------------------------------------------------------- 1 | export const characteristics = [ 2 | { 3 | title: 'Safety', 4 | description: 'It carefully packaged with a personal touch', 5 | icon: 'safety' 6 | }, 7 | { 8 | title: 'Easy shipping', 9 | description: 'You’ll receive dispatch confirmation and an arrival date', 10 | icon: 'shipping' 11 | }, 12 | { 13 | title: 'Changed your mind?', 14 | description: 'Rest assured, we offer free returns within 30 days', 15 | icon: 'return' 16 | } 17 | ]; 18 | -------------------------------------------------------------------------------- /utils/queries/get-customer.js: -------------------------------------------------------------------------------- 1 | module.exports.getCustomer = () => { 2 | return ` 3 | query { 4 | customer { 5 | firstName 6 | lastName 7 | email 8 | id: entityId 9 | groupId: customerGroupId 10 | company 11 | notes 12 | phone 13 | taxExemptCategory 14 | addressCount 15 | attributeCount 16 | storeCredit { 17 | currencyCode 18 | value 19 | } 20 | } 21 | } 22 | `; 23 | }; 24 | -------------------------------------------------------------------------------- /functions/getCategories.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const queries = require('../utils/queries'); 3 | const permission = require('./middleware/permission'); 4 | 5 | const getCategories = async (event, context) => { 6 | const { data, status } = await customAxios('graphql').post(`/graphql`, { 7 | query: queries.category() 8 | }); 9 | return { 10 | body: JSON.stringify(data), 11 | statusCode: status 12 | }; 13 | }; 14 | 15 | exports.handler = (event, context) => permission(getCategories)(event, context); 16 | -------------------------------------------------------------------------------- /functions/createCart.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const getCart = async ({ body }, context) => { 5 | const { cartData } = JSON.parse(body); 6 | const { data, status } = await customAxios('api').post( 7 | `/stores/${process.env.STORE_HASH}/v3/carts?include=redirect_urls`, 8 | cartData 9 | ); 10 | return { 11 | body: JSON.stringify(data), 12 | statusCode: status 13 | }; 14 | }; 15 | 16 | exports.handler = (event, context) => permission(getCart)(event, context); 17 | -------------------------------------------------------------------------------- /functions/getWishlist.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const getWishlist = async ({ queryStringParameters }, context) => { 5 | const { wishlistId } = queryStringParameters; 6 | const { data, status } = await customAxios('api').get( 7 | `/stores/${process.env.STORE_HASH}/v3/wishlists/${wishlistId}` 8 | ); 9 | return { 10 | body: JSON.stringify(data), 11 | statusCode: status 12 | }; 13 | }; 14 | 15 | exports.handler = (event, context) => permission(getWishlist)(event, context); 16 | -------------------------------------------------------------------------------- /functions/createOrder.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const createOrder = async ({ queryStringParameters }, context) => { 5 | const { checkoutId } = queryStringParameters; 6 | const { data, status } = await customAxios('api').post( 7 | `/stores/${process.env.STORE_HASH}/v3/checkouts/${checkoutId}/orders` 8 | ); 9 | return { 10 | body: JSON.stringify(data), 11 | statusCode: status 12 | }; 13 | }; 14 | 15 | exports.handler = (event, context) => permission(createOrder)(event, context); 16 | -------------------------------------------------------------------------------- /functions/createWishlist.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const createWishlist = async ({ body }, context) => { 5 | const { wishlistData } = JSON.parse(body); 6 | const { data, status } = await customAxios('api').post( 7 | `/stores/${process.env.STORE_HASH}/v3/wishlists`, 8 | wishlistData 9 | ); 10 | return { 11 | body: JSON.stringify(data), 12 | statusCode: status 13 | }; 14 | }; 15 | 16 | exports.handler = (event, context) => 17 | permission(createWishlist)(event, context); 18 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globalSetup: '/jest.setup.js', 3 | moduleNameMapper: { 4 | '^@/(.*)$': '/$1', 5 | '^~/(.*)$': '/$1', 6 | '^vue$': 'vue/dist/vue.common.js' 7 | }, 8 | moduleFileExtensions: ['js', 'vue', 'json'], 9 | transform: { 10 | '^.+\\.js$': 'babel-jest', 11 | '.*\\.(vue)$': 'vue-jest' 12 | }, 13 | collectCoverage: false, 14 | collectCoverageFrom: [ 15 | '/components/**/*.vue', 16 | '/pages/**/*.vue' 17 | ], 18 | testEnvironment: 'jsdom', 19 | forceExit: !!process.env.CI 20 | }; 21 | -------------------------------------------------------------------------------- /functions/customerRegister.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const customerRegister = async ({ body }, context) => { 5 | const { variables } = JSON.parse(body); 6 | 7 | const { data, status } = await customAxios('api').post( 8 | `/stores/${process.env.STORE_HASH}/v2/customers`, 9 | variables 10 | ); 11 | 12 | return { 13 | body: JSON.stringify(data), 14 | statusCode: status 15 | }; 16 | }; 17 | 18 | exports.handler = (event, context) => 19 | permission(customerRegister)(event, context); 20 | -------------------------------------------------------------------------------- /functions/getHomePageContentWidgets.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const queries = require('../utils/queries'); 3 | const permission = require('./middleware/permission'); 4 | 5 | const getHomePageContentWidgets = async (event, context) => { 6 | const { data, status } = await customAxios('graphql').post(`/graphql`, { 7 | query: queries.homePageContentWidgets() 8 | }); 9 | 10 | return { 11 | body: JSON.stringify(data), 12 | statusCode: status 13 | }; 14 | }; 15 | 16 | exports.handler = (event, context) => 17 | permission(getHomePageContentWidgets)(event, context); 18 | -------------------------------------------------------------------------------- /functions/getProductBySlug.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const queries = require('../utils/queries'); 3 | const permission = require('./middleware/permission'); 4 | 5 | const getProductBySlug = async ({ queryStringParameters }, context) => { 6 | const { data, status } = await customAxios('graphql').post(`/graphql`, { 7 | query: queries.productBySlug(queryStringParameters) 8 | }); 9 | return { 10 | body: JSON.stringify(data), 11 | statusCode: status 12 | }; 13 | }; 14 | 15 | exports.handler = (event, context) => 16 | permission(getProductBySlug)(event, context); 17 | -------------------------------------------------------------------------------- /functions/getAllAddresses.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const getAllAddresses = async ({ queryStringParameters }, context) => { 5 | const { customerId } = queryStringParameters; 6 | const { data, status } = await customAxios('api').get( 7 | `/stores/${process.env.STORE_HASH}/v2/customers/${customerId}/addresses` 8 | ); 9 | return { 10 | body: JSON.stringify(data), 11 | statusCode: status 12 | }; 13 | }; 14 | 15 | exports.handler = (event, context) => 16 | permission(getAllAddresses)(event, context); 17 | -------------------------------------------------------------------------------- /components/Loader.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /functions/getCart.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const getCart = async ({ queryStringParameters }, context) => { 5 | const { cartId } = queryStringParameters; 6 | 7 | const { data, status } = await customAxios('api').get( 8 | `/stores/${process.env.STORE_HASH}/v3/carts/${cartId}?include=redirect_urls,line_items.physical_items.options` 9 | ); 10 | return { 11 | body: JSON.stringify(data), 12 | statusCode: status 13 | }; 14 | }; 15 | 16 | exports.handler = (event, context) => permission(getCart)(event, context); 17 | -------------------------------------------------------------------------------- /functions/storefront.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const storefront = async ({ queryStringParameters }, context) => { 5 | const { field } = queryStringParameters; 6 | const { data, status } = await customAxios('api').get( 7 | `/stores/${process.env.STORE_HASH}/v3/settings/storefront/${field}?channel_id=${process.env.CHANNEL_ID}` 8 | ); 9 | return { 10 | body: JSON.stringify(data), 11 | statusCode: status 12 | }; 13 | }; 14 | 15 | exports.handler = (event, context) => permission(storefront)(event, context); 16 | -------------------------------------------------------------------------------- /functions/deleteAddress.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const deleteAddress = async ({ queryStringParameters }, context) => { 5 | const { customerId, addressId } = queryStringParameters; 6 | const { data, status } = await customAxios('api').delete( 7 | `/stores/${process.env.STORE_HASH}/v2/customers/${customerId}/addresses/${addressId}` 8 | ); 9 | return { 10 | body: JSON.stringify(data), 11 | statusCode: status 12 | }; 13 | }; 14 | 15 | exports.handler = (event, context) => permission(deleteAddress)(event, context); 16 | -------------------------------------------------------------------------------- /static/assets/storybook/SfFooter/youtube.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/queries/index.js: -------------------------------------------------------------------------------- 1 | const { category } = require('./category'); 2 | const { productsByCategory } = require('./products-by-category'); 3 | const { productBySlug } = require('./product-by-slug'); 4 | const { customerLogin } = require('./customer-login'); 5 | const { customerLogOut } = require('./customer-logout'); 6 | const { getCustomer } = require('./get-customer'); 7 | const { homePageContentWidgets } = require('./homepage-content-widgets'); 8 | module.exports = { 9 | productsByCategory, 10 | productBySlug, 11 | customerLogin, 12 | customerLogOut, 13 | getCustomer, 14 | category, 15 | homePageContentWidgets 16 | }; 17 | -------------------------------------------------------------------------------- /functions/deleteCartItem.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const deleteCartItem = async ({ queryStringParameters }, context) => { 5 | const { cartId, itemId } = queryStringParameters; 6 | const { data, status } = await customAxios('api').delete( 7 | `/stores/${process.env.STORE_HASH}/v3/carts/${cartId}/items/${itemId}?include=redirect_urls` 8 | ); 9 | return { 10 | body: JSON.stringify(data), 11 | statusCode: status 12 | }; 13 | }; 14 | 15 | exports.handler = (event, context) => 16 | permission(deleteCartItem)(event, context); 17 | -------------------------------------------------------------------------------- /functions/getAllOrders.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const getAllOrders = async ({ queryStringParameters }, context) => { 5 | const { customerId } = queryStringParameters; 6 | const { data, status } = await customAxios('api').get( 7 | `/stores/${process.env.STORE_HASH}/v2/orders?customer_id=${customerId}&channel_id=${process.env.CHANNEL_ID}` 8 | ); 9 | return { 10 | body: JSON.stringify(data), 11 | statusCode: status 12 | }; 13 | }; 14 | 15 | exports.handler = (event, context) => permission(getAllOrders)(event, context); 16 | -------------------------------------------------------------------------------- /functions/getCheckout.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const getCheckout = async ({ queryStringParameters }, context) => { 5 | const { checkoutId } = queryStringParameters; 6 | 7 | const { data, status } = await customAxios('api').get( 8 | `/stores/${process.env.STORE_HASH}/v3/checkouts/${checkoutId}?includes=consignments.available_shipping_options` 9 | ); 10 | return { 11 | body: JSON.stringify(data), 12 | statusCode: status 13 | }; 14 | }; 15 | 16 | exports.handler = (event, context) => permission(getCheckout)(event, context); 17 | -------------------------------------------------------------------------------- /assets/sass/app.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | @import './vendor/toasted'; 3 | @import './mixins'; 4 | 5 | .sf-loader { 6 | &__overlay { 7 | background: rgba($color: #ffffff, $alpha: 0.2) !important; 8 | } 9 | } 10 | 11 | .images-grid__col { 12 | img { 13 | width: 100% !important; 14 | } 15 | } 16 | 17 | .sf-header__navigation { 18 | display: flex; 19 | flex-direction: column; 20 | outline: none; 21 | white-space: nowrap; 22 | @include for-desktop { 23 | flex-direction: row; 24 | } 25 | } 26 | 27 | #product .product .sf-image { 28 | &:not(.sf-image-loaded) { 29 | position: relative !important; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /functions/deleteWishlistItem.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const deleteWishlistItem = async ({ queryStringParameters }, context) => { 5 | const { wishlistId, wishlistItemId } = queryStringParameters; 6 | const { status } = await customAxios('api').delete( 7 | `/stores/${process.env.STORE_HASH}/v3/wishlists/${wishlistId}/items/${wishlistItemId}` 8 | ); 9 | return { 10 | body: JSON.stringify(), 11 | statusCode: status 12 | }; 13 | }; 14 | 15 | exports.handler = (event, context) => 16 | permission(deleteWishlistItem)(event, context); 17 | -------------------------------------------------------------------------------- /functions/getPaymentMethodByOrder.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const getPaymentMethodByOrder = async ({ queryStringParameters }, context) => { 5 | const { orderId } = queryStringParameters; 6 | 7 | const { data, status } = await customAxios('api').get( 8 | `/stores/${process.env.STORE_HASH}/v3/payments/methods?order_id=${orderId}` 9 | ); 10 | return { 11 | body: JSON.stringify(data), 12 | statusCode: status 13 | }; 14 | }; 15 | 16 | exports.handler = (event, context) => 17 | permission(getPaymentMethodByOrder)(event, context); 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Run Jest Unit test", 8 | "cwd": "${workspaceFolder}/", 9 | "program": "${workspaceFolder}/node_modules/.bin/jest", 10 | "args": [ 11 | "${fileDirname}/${fileBasenameNoExtension}", 12 | "--watch", 13 | "--runInBand", 14 | "--forceExit" 15 | ], 16 | "console": "integratedTerminal", 17 | "internalConsoleOptions": "neverOpen", 18 | "disableOptimisticBPs": true, 19 | "envFile": "${workspaceFolder}/.env.test" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /functions/addAddress.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const addAddress = async ({ queryStringParameters, body }, context) => { 5 | const { customerId } = queryStringParameters; 6 | const { address } = JSON.parse(body); 7 | const { data, status } = await customAxios('api').post( 8 | `/stores/${process.env.STORE_HASH}/v2/customers/${customerId}/addresses`, 9 | address 10 | ); 11 | return { 12 | body: JSON.stringify(data), 13 | statusCode: status 14 | }; 15 | }; 16 | 17 | exports.handler = (event, context) => permission(addAddress)(event, context); 18 | -------------------------------------------------------------------------------- /functions/addCoupons.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const processPayment = async ({ body }, context) => { 5 | const { checkoutId, couponCode } = JSON.parse(body); 6 | 7 | const { data, status } = await customAxios('api').get( 8 | `/stores/${process.env.STORE_HASH}/v3/checkouts/${checkoutId}/coupons`, 9 | { 10 | coupon_code: couponCode 11 | } 12 | ); 13 | return { 14 | body: JSON.stringify(data), 15 | statusCode: status 16 | }; 17 | }; 18 | 19 | exports.handler = (event, context) => 20 | permission(processPayment)(event, context); 21 | -------------------------------------------------------------------------------- /functions/searchProductByKey.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const searchProductByKey = async ({ queryStringParameters }, context) => { 5 | const { key } = queryStringParameters; 6 | const { data, status } = await customAxios('api').get( 7 | `/stores/${process.env.STORE_HASH}/v3/catalog/products?keyword=${key}&keyword_context=${key}&include=primary_image` 8 | ); 9 | return { 10 | body: JSON.stringify(data), 11 | statusCode: status 12 | }; 13 | }; 14 | 15 | exports.handler = (event, context) => 16 | permission(searchProductByKey)(event, context); 17 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | export default { 2 | appName: process.env.APP_NAME || 'BC Nuxt Vue Starter', 3 | baseUrl: process.env.BASE_URL || '', 4 | storeFrontApiToken: process.env.STOREFRONT_API_TOKEN || '', 5 | apiToken: process.env.API_TOKEN || '', 6 | apiClientId: process.env.API_CLIENT_ID || '', 7 | apiSecret: process.env.API_SECRET || '', 8 | bcApiUrl: process.env.BC_API_URL || '', 9 | storeHash: process.env.STORE_HASH || '', 10 | jwtSecret: process.env.JWT_SECRET || '', 11 | channelId: process.env.CHANNEL_ID || '', 12 | checkoutType: process.env.CHECKOUT_TYPE || 'redirected', 13 | paymentUrl: process.env.PAYMENT_URL || 'http://localhost:3000' 14 | }; 15 | -------------------------------------------------------------------------------- /functions/customerLogOut.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const queries = require('../utils/queries'); 3 | const permission = require('./middleware/permission'); 4 | 5 | const customerLogOut = async ({ body }, context) => { 6 | const { cookie } = JSON.parse(body); 7 | 8 | const { data, status } = await customAxios('graphql', cookie).post( 9 | `/graphql`, 10 | { 11 | query: queries.customerLogOut() 12 | } 13 | ); 14 | 15 | return { 16 | body: JSON.stringify(data), 17 | statusCode: status 18 | }; 19 | }; 20 | 21 | exports.handler = (event, context) => 22 | permission(customerLogOut)(event, context); 23 | -------------------------------------------------------------------------------- /functions/getProductOption.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const getProductOption = async ({ queryStringParameters }, context) => { 5 | const { productId } = queryStringParameters; 6 | const { data, status } = await customAxios('api').get( 7 | `/stores/${process.env.STORE_HASH}/v3/catalog/products/${productId}?include=options,variants,modifiers&include_fields=id` 8 | ); 9 | return { 10 | body: JSON.stringify(data), 11 | statusCode: status 12 | }; 13 | }; 14 | 15 | exports.handler = (event, context) => 16 | permission(getProductOption)(event, context); 17 | -------------------------------------------------------------------------------- /functions/addCartItem.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const addCartItem = async ({ queryStringParameters, body }, context) => { 5 | const { cartData } = JSON.parse(body); 6 | const { cartId } = queryStringParameters; 7 | const { data, status } = await customAxios('api').post( 8 | `/stores/${process.env.STORE_HASH}/v3/carts/${cartId}/items?include=redirect_urls`, 9 | cartData 10 | ); 11 | return { 12 | body: JSON.stringify(data), 13 | statusCode: status 14 | }; 15 | }; 16 | 17 | exports.handler = (event, context) => permission(addCartItem)(event, context); 18 | -------------------------------------------------------------------------------- /functions/getProductsByCategory.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const queries = require('../utils/queries'); 3 | const permission = require('./middleware/permission'); 4 | 5 | const getProductsByCategory = async ({ queryStringParameters }, context) => { 6 | const { path, pageParam } = queryStringParameters; 7 | const { data, status } = await customAxios('graphql').post(`/graphql`, { 8 | query: queries.productsByCategory(path, pageParam) 9 | }); 10 | return { 11 | body: JSON.stringify(data), 12 | statusCode: status 13 | }; 14 | }; 15 | 16 | exports.handler = (event, context) => 17 | permission(getProductsByCategory)(event, context); 18 | -------------------------------------------------------------------------------- /functions/getProductsByIds.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const getProductsByIds = async ({ queryStringParameters }, context) => { 5 | const { productIds } = queryStringParameters; 6 | const { data, status } = await customAxios('api').get( 7 | `/stores/${process.env.STORE_HASH}/v3/catalog/products?include_fields=name,description,price&include=variants,primary_image&id:in=${productIds}` 8 | ); 9 | return { 10 | body: JSON.stringify(data), 11 | statusCode: status 12 | }; 13 | }; 14 | 15 | exports.handler = (event, context) => 16 | permission(getProductsByIds)(event, context); 17 | -------------------------------------------------------------------------------- /functions/updateCartWithCustomerId.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const updateCartWithCustomerId = async ({ queryStringParameters }, context) => { 5 | const { cartId, customerId } = queryStringParameters; 6 | const { data, status } = await customAxios('api').put( 7 | `/stores/${process.env.STORE_HASH}/v3/carts/${cartId}`, 8 | { 9 | customer_id: customerId 10 | } 11 | ); 12 | return { 13 | body: JSON.stringify(data), 14 | statusCode: status 15 | }; 16 | }; 17 | 18 | exports.handler = (event, context) => 19 | permission(updateCartWithCustomerId)(event, context); 20 | -------------------------------------------------------------------------------- /functions/addToWishlistItem.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const addToWishlistItem = async ({ queryStringParameters, body }, context) => { 5 | const { wishlistData } = JSON.parse(body); 6 | const { wishlistId } = queryStringParameters; 7 | const { data, status } = await customAxios('api').post( 8 | `/stores/${process.env.STORE_HASH}/v3/wishlists/${wishlistId}/items`, 9 | wishlistData 10 | ); 11 | return { 12 | body: JSON.stringify(data), 13 | statusCode: status 14 | }; 15 | }; 16 | 17 | exports.handler = (event, context) => 18 | permission(addToWishlistItem)(event, context); 19 | -------------------------------------------------------------------------------- /functions/updateAddress.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const updateAddress = async ({ queryStringParameters, body }, context) => { 5 | const { customerId, addressId } = queryStringParameters; 6 | const { address } = JSON.parse(body); 7 | const { data, status } = await customAxios('api').put( 8 | `/stores/${process.env.STORE_HASH}/v2/customers/${customerId}/addresses/${addressId}`, 9 | address 10 | ); 11 | return { 12 | body: JSON.stringify(data), 13 | statusCode: status 14 | }; 15 | }; 16 | 17 | exports.handler = (event, context) => permission(updateAddress)(event, context); 18 | -------------------------------------------------------------------------------- /functions/updateCartItem.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const updateCartItem = async ({ queryStringParameters, body }, context) => { 5 | const { cartData } = JSON.parse(body); 6 | const { cartId, itemId } = queryStringParameters; 7 | const { data, status } = await customAxios('api').put( 8 | `/stores/${process.env.STORE_HASH}/v3/carts/${cartId}/items/${itemId}?include=redirect_urls`, 9 | cartData 10 | ); 11 | return { 12 | body: JSON.stringify(data), 13 | statusCode: status 14 | }; 15 | }; 16 | 17 | exports.handler = (event, context) => 18 | permission(updateCartItem)(event, context); 19 | -------------------------------------------------------------------------------- /test/store/customer.spec.js: -------------------------------------------------------------------------------- 1 | import Vuex from 'vuex'; 2 | import { createLocalVue } from '@vue/test-utils'; 3 | 4 | describe('store/customer', () => { 5 | const localVue = createLocalVue(); 6 | localVue.use(Vuex); 7 | let NuxtStore; 8 | let store; 9 | 10 | beforeAll(async () => { 11 | const storePath = `${process.env.buildDir}/store.js`; 12 | NuxtStore = await import(storePath); 13 | }); 14 | 15 | beforeEach(async () => { 16 | store = await NuxtStore.createStore(); 17 | }); 18 | 19 | describe('customer', () => { 20 | test('getter is a function', () => { 21 | const customer = store.getters['customer/customer']; 22 | expect(customer).toBe(null); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /constants/breadcrumbs.js: -------------------------------------------------------------------------------- 1 | export const profileBreadcrumbs = [ 2 | { 3 | text: 'Home', 4 | link: '/' 5 | }, 6 | { 7 | text: 'My Account', 8 | link: '#' 9 | } 10 | ]; 11 | 12 | export const productsBreadcrumbs = [ 13 | { 14 | text: 'Home', 15 | link: '/' 16 | }, 17 | { 18 | text: 'Products', 19 | link: '#' 20 | } 21 | ]; 22 | 23 | export const productBreadcrumbs = [ 24 | { 25 | text: 'Products', 26 | link: '/products' 27 | }, 28 | { 29 | text: 'Product Page', 30 | link: '#' 31 | } 32 | ]; 33 | 34 | export const cartBreadcrumbs = [ 35 | { 36 | text: 'Home', 37 | link: '/' 38 | }, 39 | { 40 | text: 'Cart', 41 | link: '#' 42 | } 43 | ]; 44 | -------------------------------------------------------------------------------- /utils/language/sample_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "es": [ 3 | { 4 | "[Sample] 1 L Le Parfait Jar": "[Muestra] Tarro Le Parfait de 1 litro", 5 | "Color": "Color", 6 | "Orange": "Naranja", 7 | "Size": "Tamaño", 8 | "Medium": "Medio", 9 | "Weight": "Peso", 10 | "5Kg": "5Kg", 11 | "Modifier": "Modificador", 12 | "test2": "test2" 13 | } 14 | ], 15 | "en": [ 16 | { 17 | "[Sample] 1 L Le Parfait Jar": "[Sample] 1 L Le Parfait Jar", 18 | "Color": "Color", 19 | "Orange": "Orange", 20 | "Size": "Size", 21 | "Medium": "Medium", 22 | "Weight": "Weight", 23 | "5Kg": "5Kg", 24 | "Modifier": "Modifier", 25 | "test2": "test2" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /functions/setBillingAddressToCheckout.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const setBillingAddressToCheckout = async ( 5 | { queryStringParameters, body }, 6 | context 7 | ) => { 8 | const { checkoutId } = queryStringParameters; 9 | const { data } = JSON.parse(body); 10 | 11 | const result = await customAxios('api').post( 12 | `/stores/${process.env.STORE_HASH}/v3/checkouts/${checkoutId}/billing-address`, 13 | data 14 | ); 15 | return { 16 | body: JSON.stringify(result.data), 17 | statusCode: result.status 18 | }; 19 | }; 20 | 21 | exports.handler = (event, context) => 22 | permission(setBillingAddressToCheckout)(event, context); 23 | -------------------------------------------------------------------------------- /functions/setConsignmentToCheckout.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const setConsignmentToCheckout = async ( 5 | { queryStringParameters, body }, 6 | context 7 | ) => { 8 | const { checkoutId } = queryStringParameters; 9 | const { consignment } = JSON.parse(body); 10 | 11 | const { data, status } = await customAxios('api').post( 12 | `/stores/${process.env.STORE_HASH}/v3/checkouts/${checkoutId}/consignments`, 13 | consignment 14 | ); 15 | return { 16 | body: JSON.stringify(data), 17 | statusCode: status 18 | }; 19 | }; 20 | 21 | exports.handler = (event, context) => 22 | permission(setConsignmentToCheckout)(event, context); 23 | -------------------------------------------------------------------------------- /functions/updateShippingOption.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const updateShippingOption = async ({ queryStringParameters }, context) => { 5 | const { checkoutId, consignmentId, shippingOptionId } = queryStringParameters; 6 | 7 | const { data, status } = await customAxios( 8 | 'api' 9 | ).put( 10 | `/stores/${process.env.STORE_HASH}/v3/checkouts/${checkoutId}/consignments/${consignmentId}`, 11 | { shipping_option_id: shippingOptionId } 12 | ); 13 | return { 14 | body: JSON.stringify(data), 15 | statusCode: status 16 | }; 17 | }; 18 | 19 | exports.handler = (event, context) => 20 | permission(updateShippingOption)(event, context); 21 | -------------------------------------------------------------------------------- /assets/sass/components/customerprofile.scss: -------------------------------------------------------------------------------- 1 | @import '~@storefront-ui/vue/styles'; 2 | 3 | #my-account { 4 | box-sizing: border-box; 5 | @include for-desktop { 6 | max-width: 1272px; 7 | padding: 0 var(--spacer-sm); 8 | margin: 0 auto; 9 | } 10 | } 11 | .my-account { 12 | @include for-mobile { 13 | --content-pages-sidebar-category-title-font-weight: var( 14 | --font-weight--normal 15 | ); 16 | --content-pages-sidebar-category-title-margin: var(--spacer-sm) 17 | var(--spacer-sm) var(--spacer-sm) var(--spacer-base); 18 | } 19 | @include for-desktop { 20 | --content-pages-sidebar-category-title-margin: var(--spacer-xl) 0 0 0; 21 | } 22 | } 23 | .breadcrumbs { 24 | padding: var(--spacer-base) 0 var(--spacer-lg); 25 | } 26 | -------------------------------------------------------------------------------- /assets/sass/components/orders.scss: -------------------------------------------------------------------------------- 1 | @import '~@storefront-ui/vue/styles'; 2 | .no-orders { 3 | &__title { 4 | margin: 0 0 var(--spacer-base) 0; 5 | } 6 | &__button { 7 | --button-width: 100%; 8 | margin: var(--spacer-2xl) 0 0 0; 9 | @include for-desktop { 10 | --button-width: 17.375rem; 11 | } 12 | } 13 | } 14 | .orders { 15 | @include for-desktop { 16 | &__element { 17 | &--right { 18 | text-align: right; 19 | } 20 | } 21 | } 22 | } 23 | .message { 24 | margin: 0 0 var(--spacer-xl) 0; 25 | color: var(--c-dark-variant); 26 | } 27 | a { 28 | color: var(--c-primary); 29 | font-weight: var(--font-weight--medium); 30 | text-decoration: none; 31 | &:hover { 32 | color: var(--c-text); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /functions/updateConsignmentToCheckout.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const updateConsignmentToCheckout = async ( 5 | { queryStringParameters, body }, 6 | context 7 | ) => { 8 | const { checkoutId, consignmentId } = queryStringParameters; 9 | const { consignment } = JSON.parse(body); 10 | 11 | const { data, status } = await customAxios('api').put( 12 | `/stores/${process.env.STORE_HASH}/v3/checkouts/${checkoutId}/consignments/${consignmentId}`, 13 | consignment 14 | ); 15 | return { 16 | body: JSON.stringify(data), 17 | statusCode: status 18 | }; 19 | }; 20 | 21 | exports.handler = (event, context) => 22 | permission(updateConsignmentToCheckout)(event, context); 23 | -------------------------------------------------------------------------------- /utils/language/index.js: -------------------------------------------------------------------------------- 1 | export const getBrowserLocales = (options = { languageCodeOnly: true }) => { 2 | const defaultOptions = { 3 | languageCodeOnly: false 4 | }; 5 | 6 | const opt = { 7 | ...defaultOptions, 8 | ...options 9 | }; 10 | 11 | if (typeof window === 'undefined') { 12 | return undefined; 13 | } 14 | 15 | const browserLocales = 16 | navigator.languages === undefined 17 | ? [navigator.language] 18 | : navigator.languages; 19 | 20 | if (!browserLocales) { 21 | return undefined; 22 | } 23 | 24 | return browserLocales.map((locale) => { 25 | const trimmedLocale = locale.trim(); 26 | 27 | return opt.languageCodeOnly ? trimmedLocale.split(/-|_/)[0] : trimmedLocale; 28 | }); 29 | }; 30 | export default require('./sample_data.json'); 31 | -------------------------------------------------------------------------------- /api/controller/order.js: -------------------------------------------------------------------------------- 1 | import { customAxios } from '../utils/axios'; 2 | 3 | export const getAllOrders = async (req, res, next) => { 4 | try { 5 | const customerId = req.query.customerId; 6 | const { data } = await customAxios('api').get( 7 | `/stores/${process.env.STORE_HASH}/v2/orders?customer_id=${customerId}&channel_id=${process.env.CHANNEL_ID}` 8 | ); 9 | res.json(data); 10 | } catch (error) { 11 | next(error); 12 | } 13 | }; 14 | 15 | export const createOrder = async (req, res, next) => { 16 | try { 17 | const checkoutId = req.query.checkoutId; 18 | const { data } = await customAxios('api').post( 19 | `/stores/${process.env.STORE_HASH}/v3/checkouts/${checkoutId}/orders` 20 | ); 21 | res.json(data); 22 | } catch (error) { 23 | next(error); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /static/assets/storybook/SfOptions/profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /api/controller/storefront.js: -------------------------------------------------------------------------------- 1 | import { customAxios } from '../utils/axios'; 2 | const queries = require('../../utils/queries'); 3 | 4 | export const storefront = async (req, res, next) => { 5 | const field = req.query.field; 6 | try { 7 | const { data } = await customAxios('api').get( 8 | `/stores/${process.env.STORE_HASH}/v3/settings/storefront/${field}?channel_id=${process.env.CHANNEL_ID}` 9 | ); 10 | res.json(data); 11 | } catch (error) { 12 | next(error); 13 | } 14 | }; 15 | 16 | export const getHomePageContentWidgets = async (req, res, next) => { 17 | try { 18 | const { data } = await customAxios('graphql').post(`/graphql`, { 19 | query: queries.homePageContentWidgets() 20 | }); 21 | res.json(data); 22 | } catch (error) { 23 | console.log(error); 24 | next(error); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /functions/middleware/permission.js: -------------------------------------------------------------------------------- 1 | const { checkApiAccessPermission } = require('../../utils/permission'); 2 | 3 | const permission = (func) => (a, b) => { 4 | const handler = { 5 | apply: (target, thisArg, args) => { 6 | const { path, httpMethod } = args[0]; 7 | const routePath = path.replace('/.netlify/functions/', ''); 8 | const permString = `${httpMethod}_${routePath}`; 9 | try { 10 | checkApiAccessPermission(permString); 11 | } catch (error) { 12 | return { 13 | statusCode: 403, 14 | body: JSON.stringify('API permission is denied') 15 | }; 16 | } 17 | 18 | return target(args[0], args[1]); 19 | } 20 | }; 21 | 22 | const proxy = new Proxy(func, handler); 23 | 24 | return proxy.apply(this, [a, b]); 25 | }; 26 | 27 | module.exports = permission; 28 | -------------------------------------------------------------------------------- /static/assets/storybook/SfFooter/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/sass/vendor/toasted.scss: -------------------------------------------------------------------------------- 1 | @import '../variables'; 2 | 3 | .toasted { 4 | &.custom { 5 | border-radius: $border-radius-lg; 6 | min-height: 5rem; 7 | line-height: 1.2rem; 8 | background-color: #353535; 9 | padding: 1rem 2rem; 10 | font-size: 1rem; 11 | font-weight: 400; 12 | color: #fff; 13 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 14 | 15 | &.success { 16 | background: #4caf50; 17 | } 18 | 19 | &.error { 20 | background: #f44336; 21 | } 22 | 23 | &.info { 24 | background: $brand-secondary; 25 | } 26 | 27 | .action { 28 | color: #a1c2fa; 29 | } 30 | } 31 | } 32 | 33 | /* Medium devices (tablets, 768px and up) The navbar toggle appears at this breakpoint */ 34 | @media (min-width: 768px) { 35 | .toasted { 36 | &.custom { 37 | min-height: 3rem; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /api/utils/axios.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | module.exports.customAxios = (action, cookie = null, auth = null) => { 4 | let baseURL = ''; 5 | const headers = { 6 | 'Content-Type': 'application/json', 7 | Accept: 'application/json' 8 | }; 9 | if (action === 'api') { 10 | baseURL = process.env.BC_API_URL; 11 | headers['X-Auth-Token'] = `${process.env.API_TOKEN}`; 12 | } else if (action === 'graphql') { 13 | baseURL = process.env.BASE_URL; 14 | headers.Authorization = `Bearer ${process.env.STOREFRONT_API_TOKEN}`; 15 | } else if (action === 'payment') { 16 | baseURL = process.env.PAYMENT_URL; 17 | headers.Accept = 'application/vnd.bc.v1+json'; 18 | headers.Authorization = `PAT ${auth}`; 19 | } 20 | if (cookie) { 21 | headers.Cookie = cookie; 22 | } 23 | return axios.create({ 24 | baseURL, 25 | headers, 26 | withCredentials: true 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /functions/customerLogin.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const queries = require('../utils/queries'); 3 | const permission = require('./middleware/permission'); 4 | 5 | const customerLogin = async ({ body }, context) => { 6 | const { variables } = JSON.parse(body); 7 | 8 | const loginRes = await customAxios('graphql').post(`/graphql`, { 9 | query: queries.customerLogin(), 10 | variables 11 | }); 12 | 13 | const cookies = loginRes.headers['set-cookie'].filter((item) => 14 | item.includes('SHOP_TOKEN') 15 | ); 16 | 17 | const { data, status } = await customAxios('graphql', cookies[0]).post( 18 | `/graphql`, 19 | { 20 | query: queries.getCustomer() 21 | } 22 | ); 23 | 24 | return { 25 | body: JSON.stringify(data), 26 | statusCode: status 27 | }; 28 | }; 29 | 30 | exports.handler = (event, context) => permission(customerLogin)(event, context); 31 | -------------------------------------------------------------------------------- /pages/error.vue: -------------------------------------------------------------------------------- 1 | 21 | 29 | 30 | -------------------------------------------------------------------------------- /static/assets/storybook/SfOptions/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /utils/script/create-storefront-site.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | const axios = require('axios'); 3 | dotenv.config(); 4 | 5 | module.exports.createSite = async function (channelId, siteUrl) { 6 | try { 7 | const data = { 8 | channel_id: channelId, 9 | url: siteUrl 10 | }; 11 | 12 | const response = await axios.post( 13 | `${process.env.BC_API_URL}/stores/${process.env.STORE_HASH}/v3/channels/${channelId}/site`, 14 | data, 15 | { 16 | headers: { 17 | 'Content-Type': 'application/json', 18 | 'X-Auth-Token': process.env.API_TOKEN 19 | } 20 | } 21 | ); 22 | console.log(response.data); 23 | return '===========SUCCESS==========='; 24 | } catch (err) { 25 | const error = err?.response?.data ?? err; 26 | console.log(error); 27 | return '===========FAILED==========='; 28 | } 29 | }; 30 | require('make-runnable/custom')({ 31 | printOutputFrame: false 32 | }); 33 | -------------------------------------------------------------------------------- /assets/sass/pages/register.scss: -------------------------------------------------------------------------------- 1 | @import '~@storefront-ui/vue/styles'; 2 | @import '@/assets/sass/_variables.scss'; 3 | @import '@/assets/sass/_mixins.scss'; 4 | 5 | #register-page { 6 | font-family: var(--font-family--primary); 7 | box-sizing: border-box; 8 | display: flex; 9 | justify-content: center; 10 | @include for-desktop { 11 | max-width: 1240px; 12 | margin: auto; 13 | } 14 | .register-form { 15 | padding: 1rem; 16 | min-width: 400px; 17 | display: flex; 18 | justify-content: center; 19 | flex-direction: column; 20 | 21 | @include for-mobile { 22 | min-width: 300px; 23 | } 24 | 25 | .register-header { 26 | text-align: center; 27 | } 28 | .register-actions { 29 | display: flex; 30 | flex-direction: column; 31 | align-items: center; 32 | margin-top: 1.5rem; 33 | .sf-button { 34 | margin: 1rem; 35 | } 36 | } 37 | .sf-input { 38 | margin-top: 2rem !important; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /assets/sass/components/myprofile.scss: -------------------------------------------------------------------------------- 1 | @import '~@storefront-ui/vue/styles'; 2 | .form { 3 | @include for-desktop { 4 | display: flex; 5 | flex-wrap: wrap; 6 | align-items: center; 7 | } 8 | &__element { 9 | margin: 0 0 var(--spacer-lg) 0; 10 | @include for-desktop { 11 | flex: 0 0 100%; 12 | } 13 | &--half { 14 | @include for-desktop { 15 | flex: 1 1 50%; 16 | } 17 | &-even { 18 | @include for-desktop { 19 | padding: 0 0 0 var(--spacer-lg); 20 | } 21 | } 22 | } 23 | } 24 | &__button { 25 | --button-width: 100%; 26 | @include for-desktop { 27 | --button-width: 17.5rem; 28 | } 29 | } 30 | } 31 | .message { 32 | margin: 0 0 var(--spacer-xl) 0; 33 | color: var(--c-dark-variant); 34 | } 35 | .notice { 36 | margin: var(--spacer-base) 0 0 0; 37 | font-size: var(--font-size--sm); 38 | } 39 | a { 40 | color: var(--c-primary); 41 | text-decoration: none; 42 | &:hover { 43 | color: var(--c-text); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /static/assets/storybook/SfFooter/pinterest.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /utils/script/create-storefront-token.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const dotenv = require('dotenv'); 3 | const axios = require('axios'); 4 | dotenv.config(); 5 | 6 | module.exports.createStoreFrontToken = async function (channelId, hostUrl) { 7 | try { 8 | const data = { 9 | channel_id: channelId, 10 | allowed_cors_origins: [hostUrl], 11 | expires_at: moment('2025').unix() 12 | }; 13 | 14 | const response = await axios.post( 15 | `${process.env.BC_API_URL}/stores/${process.env.STORE_HASH}/v3/storefront/api-token`, 16 | data, 17 | { 18 | headers: { 19 | 'Content-Type': 'application/json', 20 | 'X-Auth-Token': process.env.API_TOKEN 21 | } 22 | } 23 | ); 24 | console.log(response.data); 25 | return '===========SUCCESS==========='; 26 | } catch (err) { 27 | const error = err?.response?.data ?? err; 28 | console.log(error); 29 | return '===========FAILED==========='; 30 | } 31 | }; 32 | require('make-runnable/custom')({ 33 | printOutputFrame: false 34 | }); 35 | -------------------------------------------------------------------------------- /config/head.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: process.env.npm_package_name || '', 3 | meta: [ 4 | { charset: 'utf-8' }, 5 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 6 | { 7 | hid: 'description', 8 | name: 'description', 9 | content: process.env.npm_package_description || '' 10 | }, 11 | { 12 | rel: 'preconnect', 13 | href: 'https://fonts.gstatic.com/', 14 | crossorigin: 'anonymous' 15 | } 16 | ], 17 | link: [ 18 | { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }, 19 | { 20 | rel: 'preload', 21 | as: 'style', 22 | href: 23 | 'https://fonts.googleapis.com/css?family=Raleway:300,400,400i,500,600,700|Roboto:300,300i,400,400i,500,700&display=swap', 24 | crossorigin: 'anonymous' 25 | }, 26 | { 27 | rel: 'stylesheet', 28 | href: 29 | 'https://fonts.googleapis.com/css?family=Raleway:300,400,400i,500,600,700|Roboto:300,300i,400,400i,500,700&display=swap', 30 | media: 'print', 31 | onload: "this.media='all'" 32 | } 33 | ] 34 | }; 35 | -------------------------------------------------------------------------------- /components/Logo.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 42 | -------------------------------------------------------------------------------- /functions/processPayment.js: -------------------------------------------------------------------------------- 1 | const { customAxios } = require('../api/utils/axios'); 2 | const permission = require('./middleware/permission'); 3 | 4 | const processPayment = async ({ queryStringParameters, body }, context) => { 5 | const { orderId } = queryStringParameters; 6 | const { paymentData } = JSON.parse(body); 7 | 8 | paymentData.payment.instrument.expiry_month = parseInt( 9 | paymentData.payment.instrument.expiry_month 10 | ); 11 | paymentData.payment.instrument.expiry_year = parseInt( 12 | paymentData.payment.instrument.expiry_year 13 | ); 14 | 15 | const tokenResult = await customAxios('api').post( 16 | `/stores/${process.env.STORE_HASH}/v3/payments/access_tokens`, 17 | { 18 | order: { 19 | id: parseInt(orderId) 20 | } 21 | } 22 | ); 23 | 24 | const { data, status } = await customAxios( 25 | 'payment', 26 | null, 27 | tokenResult.data?.data?.id 28 | ).post(`/stores/${process.env.STORE_HASH}/payments`, paymentData); 29 | 30 | return { 31 | body: JSON.stringify(data), 32 | statusCode: status 33 | }; 34 | }; 35 | 36 | exports.handler = (event, context) => 37 | permission(processPayment)(event, context); 38 | -------------------------------------------------------------------------------- /assets/sass/components/wishlist.scss: -------------------------------------------------------------------------------- 1 | @import '~@storefront-ui/vue/styles'; 2 | @import '@/assets/sass/_variables.scss'; 3 | @import '@/assets/sass/_mixins.scss'; 4 | .no-wishlist { 5 | &__title { 6 | margin: 0 0 var(--spacer-base) 0; 7 | } 8 | &__button { 9 | --button-width: 100%; 10 | margin: var(--spacer-2xl) 0 0 0; 11 | @include for-desktop { 12 | --button-width: 17.375rem; 13 | } 14 | } 15 | } 16 | #wishlist { 17 | @include for-desktop { 18 | &__element { 19 | &--right { 20 | text-align: right; 21 | } 22 | } 23 | } 24 | .sf-collected-product { 25 | --collected-product-padding: var(--spacer-sm) 0; 26 | --collected-product-actions-display: flex; 27 | border: 1px solid var(--c-light); 28 | border-width: 1px 0 0 0; 29 | &:first-of-type { 30 | border-top: none; 31 | } 32 | @include for-mobile { 33 | --collected-product-remove-bottom: var(--spacer-sm); 34 | } 35 | @include for-desktop { 36 | --collected-product-padding: var(--spacer-lg) 0; 37 | &:hover { 38 | --collected-product-configuration-display: flex !important; 39 | } 40 | } 41 | } 42 | .sf-collected-product__actions { 43 | display: none; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /assets/sass/components/checkout/ordersummary.scss: -------------------------------------------------------------------------------- 1 | @import '~@storefront-ui/vue/styles'; 2 | .title { 3 | --heading-title-margin: 0 0 var(--spacer-xl) 0; 4 | --heading-title-font-weight: var(--font-weight--bold); 5 | --heading-padding: 0; 6 | --heading-title-margin: 0 0 var(--spacer-xl) 0; 7 | @include for-desktop { 8 | --heading-title-font-weight: var(--font-weight--semibold); 9 | } 10 | } 11 | .property { 12 | margin: var(--spacer-base) 0; 13 | --property-name-font-weight: var(--font-weight--medium); 14 | --property-value-font-weight: var(--font-weight--bold); 15 | &:last-of-type { 16 | margin: var(--spacer-base) 0 var(--spacer-xl); 17 | --property-name-color: var(--c-text); 18 | } 19 | } 20 | .divider { 21 | --divider-border-color: var(--c-white); 22 | --divider-margin: var(--spacer-xl) 0 0 0; 23 | } 24 | .promo-code { 25 | display: flex; 26 | justify-content: center; 27 | align-items: flex-start; 28 | &__input { 29 | --input-background: var(--c-white); 30 | --input-label-font-size: var(--font-size--base); 31 | flex: 1; 32 | } 33 | &__button { 34 | --button-height: 1.875rem; 35 | } 36 | } 37 | .characteristics { 38 | &__item { 39 | margin: var(--spacer-base) 0; 40 | &:last-of-type { 41 | margin: 0; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /store/order.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { API_URL } from '~/config/constants'; 3 | import { getSecuredData, getUser } from '~/utils/auth'; 4 | 5 | export const state = () => ({ 6 | orders: [] 7 | }); 8 | 9 | export const getters = { 10 | orders(state) { 11 | return state.orders; 12 | } 13 | }; 14 | 15 | export const mutations = { 16 | SET_ORDERS(state, orders) { 17 | state.orders = orders; 18 | } 19 | }; 20 | 21 | export const actions = { 22 | async getAllOrders({ commit }) { 23 | try { 24 | const user = getUser(); 25 | const customer = getSecuredData(user.secureData); 26 | 27 | const { data } = await axios.get( 28 | `${API_URL}/getAllOrders?customerId=${customer.id}` 29 | ); 30 | 31 | let orders = []; 32 | if (data) { 33 | orders = data.map((item) => { 34 | return [ 35 | `#${item.id}`, 36 | item.date_modified, 37 | item.payment_method, 38 | Number.parseFloat(item.total_inc_tax).toFixed(2), 39 | item.status 40 | ]; 41 | }); 42 | } 43 | commit('SET_ORDERS', orders); 44 | } catch (error) { 45 | console.log(error); 46 | this.$toast.error('Something went wrong in getting orders'); 47 | } 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /test/api/address/getAllAddresses.spec.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { NOT_FOUND, OK } from 'http-status'; 3 | import request from 'supertest'; 4 | import app from '../../../api/index'; 5 | 6 | jest.setTimeout(700000); 7 | jest.mock('axios', () => { 8 | return { 9 | create: jest.fn().mockReturnThis(), 10 | get: jest.fn().mockImplementation(async () => ({ data: '' })), 11 | post: jest.fn().mockImplementation(async () => ({ data: '' })), 12 | patch: jest.fn().mockImplementation(async () => ({ data: '' })), 13 | delete: jest.fn().mockImplementation(async () => ({ data: '' })) 14 | }; 15 | }); 16 | const API_PATH = '/getAllAddresses'; 17 | 18 | describe('Address API Test', () => { 19 | it('route not found', async () => { 20 | const response = await request(app) 21 | .get('/getAllAddre') 22 | .query({ customerId: 'test' }); 23 | 24 | expect(response.status).toBe(NOT_FOUND); 25 | }); 26 | it('route not found', async () => { 27 | const customerId = 'test'; 28 | const response = await request(app) 29 | .get(`${API_PATH}`) 30 | .query({ customerId }); 31 | 32 | expect(response.status).toBe(OK); 33 | expect(axios.get).toBeCalledWith( 34 | `/stores/${process.env.STORE_HASH}/v2/customers/${customerId}/addresses` 35 | ); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /static/assets/storybook/SfOptions/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /utils/permission/index.js: -------------------------------------------------------------------------------- 1 | const ACCESSIBLE_API_ENDPOINTS = [ 2 | 'GET_getCategories', 3 | 'GET_getProductsByCategory', 4 | 'GET_getProductBySlug', 5 | 'GET_getProductOption', 6 | 'GET_getCart', 7 | 'POST_createCart', 8 | 'POST_addCartItem', 9 | 'PUT_updateCartItem', 10 | 'PUT_updateCartWithCustomerId', 11 | 'DELETE_deleteCartItem', 12 | 'GET_getAllOrders', 13 | 'GET_getAllAddresses', 14 | 'PUT_updateAddress', 15 | 'POST_addAddress', 16 | 'DELETE_deleteAddress', 17 | 'GET_searchProductByKey', 18 | 'POST_customerLogin', 19 | 'POST_customerRegister', 20 | 'POST_customerLogOut', 21 | 'GET_getCheckout', 22 | 'POST_setConsignmentToCheckout', 23 | 'PUT_updateConsignmentToCheckout', 24 | 'PUT_updateShippingOption', 25 | 'POST_setBillingAddressToCheckout', 26 | 'POST_createOrder', 27 | 'GET_getPaymentMethodByOrder', 28 | 'POST_processPayment', 29 | 'POST_addCoupons', 30 | 'GET_getProductOption', 31 | 'GET_storefront', 32 | 'GET_getHomePageContentWidgets', 33 | 'POST_createWishlist', 34 | 'POST_addToWishlistItem', 35 | 'GET_getWishlist', 36 | 'GET_getProductsByIds', 37 | 'DELETE_deleteWishlistItem' 38 | ]; 39 | 40 | module.exports.checkApiAccessPermission = (permission) => { 41 | if (!ACCESSIBLE_API_ENDPOINTS.includes(permission)) { 42 | throw new Error('permission error'); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /utils/script/create-storefront-channel.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | const axios = require('axios'); 3 | dotenv.config(); 4 | 5 | module.exports.createChannel = async function (name, appId, sections) { 6 | try { 7 | const data = { 8 | is_listable_from_ui: false, 9 | is_visible: true, 10 | name, 11 | status: 'active', 12 | type: 'storefront', 13 | platform: 'custom' 14 | }; 15 | const configMeta = { app: {} }; 16 | const secs = sections ? JSON.parse(sections) : null; 17 | 18 | if (appId || secs) { 19 | configMeta.app.id = appId || null; 20 | configMeta.app.sections = secs || null; 21 | data.config_meta = configMeta; 22 | } 23 | 24 | const response = await axios.post( 25 | `${process.env.BC_API_URL}/stores/${process.env.STORE_HASH}/v3/channels`, 26 | data, 27 | { 28 | headers: { 29 | 'Content-Type': 'application/json', 30 | 'X-Auth-Token': process.env.API_TOKEN 31 | } 32 | } 33 | ); 34 | console.log(response.data); 35 | return '===========SUCCESS==========='; 36 | } catch (err) { 37 | const error = err?.response?.data ?? err; 38 | console.log(error); 39 | return '===========FAILED==========='; 40 | } 41 | }; 42 | require('make-runnable/custom')({ 43 | printOutputFrame: false 44 | }); 45 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | import build from './config/build'; 2 | import buildModules from './config/buildModules'; 3 | import head from './config/head'; 4 | import css from './config/css'; 5 | import plugins from './config/plugins'; 6 | import modules from './config/modules'; 7 | import axios from './config/axios'; 8 | import toast from './config/toast'; 9 | import env from './config/env'; 10 | import proxy from './config/proxy'; 11 | import sentry from './config/sentry'; 12 | 13 | export default { 14 | ssr: false, 15 | loading: false, 16 | components: false, 17 | parallel: true, 18 | cache: true, 19 | hardSource: true, 20 | target: 'static', 21 | /** 22 | ** Global Env Varialbes 23 | */ 24 | env, 25 | /* 26 | ** Headers of the page 27 | */ 28 | head, 29 | /* 30 | ** Global CSS 31 | */ 32 | css, 33 | /* 34 | ** Plugins to load before mounting the App 35 | */ 36 | plugins, 37 | /* 38 | ** Nuxt.js dev-modules 39 | */ 40 | // Doc: https://github.com/nuxt-community/eslint-module 41 | buildModules, 42 | /* 43 | ** Nuxt.js modules 44 | */ 45 | // Doc: https://axios.nuxtjs.org/usage 46 | modules, 47 | // Sentry configuration 48 | sentry, 49 | /* 50 | ** Axios module configuration 51 | ** See https://axios.nuxtjs.org/options 52 | */ 53 | axios, 54 | /* 55 | ** Build configuration 56 | */ 57 | build, 58 | toast, 59 | proxy, 60 | serverMiddleware: [{ path: '/', handler: '~/api' }] 61 | }; 62 | -------------------------------------------------------------------------------- /assets/sass/pages/error.scss: -------------------------------------------------------------------------------- 1 | @import '~@storefront-ui/vue/styles'; 2 | #error { 3 | box-sizing: border-box; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | width: 100%; 9 | padding: 0 var(--spacer-sm); 10 | @include for-desktop { 11 | margin: 0 auto; 12 | max-width: 1272px; 13 | } 14 | } 15 | .image { 16 | --image-width: 14.375rem; 17 | padding: var(--spacer-xl) 0 var(--spacer-sm); 18 | @include for-desktop { 19 | --image-width: 25.75rem; 20 | } 21 | } 22 | .heading { 23 | --heading-title-margin: 0 0 var(--spacer-sm); 24 | --heading-title-color: var(--c-primary); 25 | --heading-title-font-weight: var(--font-weight--semibold); 26 | --heading-description-color: var(--c-text-muted); 27 | --heading-description-font-size: var(--font-size--base); 28 | --heading-description-margin: 0 var(--spacer-base); 29 | --heading-description-font-family: var(--font-family--primary); 30 | @include for-desktop { 31 | --heading-description-margin: 0; 32 | } 33 | } 34 | .actions { 35 | display: flex; 36 | align-items: center; 37 | justify-content: flex-end; 38 | flex-direction: column; 39 | width: 100%; 40 | margin: var(--spacer-xl) 0 0 0; 41 | &__button { 42 | &:last-child { 43 | margin: var(--spacer-sm) 0; 44 | } 45 | } 46 | @include for-desktop { 47 | margin: var(--spacer-lg) 0 0 0; 48 | &__button { 49 | --button-width: 25rem; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /api/controller/customer.js: -------------------------------------------------------------------------------- 1 | import { customAxios } from '../utils/axios'; 2 | const queries = require('../../utils/queries'); 3 | 4 | export const customerLogin = async (req, res, next) => { 5 | try { 6 | const variables = req.body.variables; 7 | const loginRes = await customAxios('graphql').post(`/graphql`, { 8 | query: queries.customerLogin(), 9 | variables 10 | }); 11 | 12 | const cookies = loginRes.headers['set-cookie'].filter((item) => 13 | item.includes('SHOP_TOKEN') 14 | ); 15 | 16 | const { data } = await customAxios('graphql', cookies[0]).post(`/graphql`, { 17 | query: queries.getCustomer() 18 | }); 19 | 20 | res.json({ 21 | ...data, 22 | cookie: loginRes.headers['set-cookie'][0] 23 | }); 24 | } catch (error) { 25 | next(error); 26 | } 27 | }; 28 | 29 | export const customerRegister = async (req, res, next) => { 30 | try { 31 | const variables = req.body.variables; 32 | const { data } = await customAxios('api').post( 33 | `/stores/${process.env.STORE_HASH}/v2/customers`, 34 | variables 35 | ); 36 | res.json(data); 37 | } catch (error) { 38 | next(error); 39 | } 40 | }; 41 | 42 | export const customerLogOut = async (req, res, next) => { 43 | try { 44 | const cookie = req.body.cookie; 45 | const { data } = await customAxios('graphql', cookie).post(`/graphql`, { 46 | query: queries.customerLogOut() 47 | }); 48 | 49 | res.json(data); 50 | } catch (error) { 51 | next(error); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import { Nuxt, Builder } from 'nuxt'; 2 | import nuxtConfig from './nuxt.config'; 3 | import './test/utils/dotenv-test'; 4 | 5 | // these boolean switches turn off the build for all but the store 6 | const resetConfig = { 7 | loading: false, 8 | loadingIndicator: false, 9 | fetch: { 10 | client: false, 11 | server: false 12 | }, 13 | features: { 14 | store: true, 15 | layouts: false, 16 | meta: false, 17 | middleware: false, 18 | transitions: false, 19 | deprecations: false, 20 | validate: false, 21 | asyncData: false, 22 | fetch: false, 23 | clientOnline: false, 24 | clientPrefetch: false, 25 | clientUseUrl: false, 26 | componentAliases: false, 27 | componentClientOnly: false 28 | }, 29 | build: { 30 | indicator: false, 31 | terser: false 32 | } 33 | }; 34 | 35 | // we take our nuxt config, lay the resets on top of it, 36 | // and lastly we apply the non-boolean overrides 37 | const config = Object.assign({}, nuxtConfig, resetConfig, { 38 | srcDir: nuxtConfig.srcDir, 39 | ignore: ['**/components/**/*', '**/layouts/**/*', '**/pages/**/*'] 40 | }); 41 | 42 | const buildNuxt = async () => { 43 | const nuxt = new Nuxt(config); 44 | await new Builder(nuxt).build(); 45 | return nuxt; 46 | }; 47 | 48 | module.exports = async () => { 49 | const nuxt = await buildNuxt(); 50 | 51 | // we surface this path as an env var now 52 | // so we can import the store dynamically later on 53 | process.env.buildDir = nuxt.options.buildDir; 54 | }; 55 | -------------------------------------------------------------------------------- /plugins/axios.js: -------------------------------------------------------------------------------- 1 | export default function (context) { 2 | const { 3 | $axios, 4 | app: { $toast } 5 | } = context; 6 | $axios.onRequest((config) => { 7 | console.log('Making request to ' + `: ${config.baseURL}${config.url}`); 8 | if ( 9 | !config.url.startsWith('http://') && 10 | !config.url.startsWith('https://') && 11 | config.headers.common && 12 | !config.url.includes('api') 13 | ) { 14 | config.headers.Authorization = `Bearer ${process.env.storeFrontApiToken}`; 15 | config.headers['Content-Type'] = 'application/json'; 16 | } else { 17 | config.headers['Content-Type'] = 'application/json'; 18 | config.headers.Accept = 'application/json'; 19 | config.headers['X-Auth-Token'] = `${process.env.apiToken}`; 20 | } 21 | }); 22 | 23 | $axios.onError(async (error) => { 24 | const code = parseInt(error.response && error.response.status); 25 | if (code === 401) { 26 | $toast.error('Token is expired or incorrect.'); 27 | } else if (code === 403) { 28 | $toast.error('You do not have permission to do that.'); 29 | } else { 30 | const errorMessage = 31 | error?.response?.data?.error ?? 32 | error?.response?.message ?? 33 | error?.response?.error?.message ?? 34 | error?.response?.data?.message ?? 35 | error?.response?.data?.errors?.variant; 36 | if (errorMessage) { 37 | $toast.error(errorMessage); 38 | } 39 | console.log(error); 40 | } 41 | throw error; 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /assets/sass/components/checkout/checkout.scss: -------------------------------------------------------------------------------- 1 | @import '~@storefront-ui/vue/styles'; 2 | #checkout { 3 | padding: 50px; 4 | box-sizing: border-box; 5 | @include for-desktop { 6 | padding: 0 var(--spacer-sm); 7 | max-width: 1272px; 8 | margin: 0 auto; 9 | } 10 | } 11 | .checkout { 12 | --steps-content-padding: 0 var(--spacer-sm); 13 | @include for-desktop { 14 | --steps-content-padding: 0; 15 | display: flex; 16 | } 17 | &__main { 18 | ::v-deep .sf-steps__step.is-done { 19 | --steps-step-color: var(--c-primary); 20 | } 21 | @include for-desktop { 22 | flex: 1; 23 | padding: var(--spacer-xl) 0 0 0; 24 | } 25 | } 26 | &__aside { 27 | @include for-desktop { 28 | flex: 0 0 26.8125rem; 29 | margin: 0 0 0 var(--spacer-base); 30 | } 31 | &-order { 32 | box-sizing: border-box; 33 | width: 100%; 34 | background: var(--c-light); 35 | padding: var(--spacer-base) var(--spacer-sm) var(--spacer-xl); 36 | @include for-desktop { 37 | padding: var(--spacer-xl); 38 | } 39 | } 40 | } 41 | } 42 | .actions { 43 | background: var(--c-white); 44 | padding: var(--spacer-sm); 45 | box-shadow: 0px -2px 10px rgba(154, 154, 154, 0.15); 46 | text-align: center; 47 | position: sticky; 48 | bottom: 0; 49 | &__button { 50 | margin-bottom: var(--spacer-sm); 51 | } 52 | @include for-desktop { 53 | position: relative; 54 | box-shadow: none; 55 | padding: var(--spacer-lg); 56 | width: 25rem; 57 | &__button { 58 | margin: 0; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /utils/queries/products-by-category.js: -------------------------------------------------------------------------------- 1 | module.exports.productsByCategory = (path, pageParam) => { 2 | return ` 3 | query CategoryByUrl { 4 | site { 5 | route(path: "${path}") { 6 | node { 7 | id 8 | ... on Category { 9 | name 10 | entityId 11 | products(${pageParam}) { 12 | pageInfo { 13 | hasNextPage 14 | hasPreviousPage 15 | startCursor 16 | endCursor 17 | } 18 | edges { 19 | node { 20 | variants { 21 | edges { 22 | node { 23 | options { 24 | edges { 25 | cursor 26 | } 27 | } 28 | } 29 | } 30 | } 31 | entityId 32 | name 33 | path 34 | description 35 | defaultImage { 36 | url(width: 216, height: 326) 37 | } 38 | prices { 39 | price { 40 | value 41 | currencyCode 42 | } 43 | } 44 | reviewSummary { 45 | numberOfReviews 46 | summationOfRatings 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | `; 57 | }; 58 | -------------------------------------------------------------------------------- /.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 | 83 | # Service worker 84 | sw.* 85 | 86 | # macOS 87 | .DS_Store 88 | 89 | # Vim swap files 90 | *.swp 91 | 92 | .vercel 93 | 94 | # Local Netlify folder 95 | .netlify 96 | -------------------------------------------------------------------------------- /api/controller/address.js: -------------------------------------------------------------------------------- 1 | import { customAxios } from '../utils/axios'; 2 | 3 | export const getAllAddresses = async (req, res, next) => { 4 | try { 5 | const customerId = req.query.customerId; 6 | const { data } = await customAxios('api').get( 7 | `/stores/${process.env.STORE_HASH}/v2/customers/${customerId}/addresses` 8 | ); 9 | res.json(data); 10 | } catch (error) { 11 | next(error); 12 | } 13 | }; 14 | 15 | export const updateAddress = async (req, res, next) => { 16 | try { 17 | const customerId = req.query.customerId; 18 | const addressId = req.query.addressId; 19 | const address = req.body.address; 20 | const { data } = await customAxios('api').put( 21 | `/stores/${process.env.STORE_HASH}/v2/customers/${customerId}/addresses/${addressId}`, 22 | address 23 | ); 24 | res.json(data); 25 | } catch (error) { 26 | next(error); 27 | } 28 | }; 29 | 30 | export const addAddress = async (req, res, next) => { 31 | try { 32 | const customerId = req.query.customerId; 33 | const address = req.body.address; 34 | const { data } = await customAxios('api').post( 35 | `/stores/${process.env.STORE_HASH}/v2/customers/${customerId}/addresses`, 36 | address 37 | ); 38 | res.json(data); 39 | } catch (error) { 40 | next(error); 41 | } 42 | }; 43 | 44 | export const deleteAddress = async (req, res, next) => { 45 | try { 46 | const customerId = req.query.customerId; 47 | const addressId = req.query.addressId; 48 | const { data } = await customAxios('api').delete( 49 | `/stores/${process.env.STORE_HASH}/v2/customers/${customerId}/addresses/${addressId}` 50 | ); 51 | res.json(data); 52 | } catch (error) { 53 | next(error); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /store/storefront.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { API_URL } from '~/config/constants'; 3 | import { getSeo, setSeo } from '~/utils/storage'; 4 | 5 | export const state = () => ({ 6 | seo: {}, 7 | renderedRegions: [] 8 | }); 9 | 10 | export const getters = { 11 | seo(state) { 12 | return state.seo; 13 | }, 14 | renderedRegions(state) { 15 | return state.renderedRegions; 16 | } 17 | }; 18 | 19 | export const mutations = { 20 | SET_SEO(state, seo) { 21 | state.seo = seo; 22 | }, 23 | SET_RENDERED_REGIONS(state, renderedRegions) { 24 | state.renderedRegions = renderedRegions; 25 | } 26 | }; 27 | 28 | export const actions = { 29 | async getStorefrontSeo({ commit }) { 30 | try { 31 | const seo = getSeo(); 32 | if (seo) { 33 | commit('SET_SEO', seo); 34 | } else { 35 | const { 36 | data: { data } 37 | } = await axios.get(`${API_URL}/storefront?field=seo`); 38 | commit('SET_SEO', data); 39 | setSeo(data); 40 | } 41 | } catch (error) { 42 | console.log(error); 43 | } 44 | }, 45 | async getStorefrontStatus() { 46 | try { 47 | const { data } = await axios.get(`${API_URL}/storefront?field=status`); 48 | return data; 49 | } catch (error) { 50 | return null; 51 | } 52 | }, 53 | async getHomePageContentWidgets({ commit }) { 54 | try { 55 | const { data } = await axios.get(`${API_URL}/getHomePageContentWidgets`); 56 | commit( 57 | 'SET_RENDERED_REGIONS', 58 | data?.data?.site?.content?.renderedRegionsByPageType?.regions 59 | ); 60 | } catch (error) { 61 | console.log(error); 62 | this.$toast.error('Something went wrong in getting widgets'); 63 | } 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /assets/sass/pages/login.scss: -------------------------------------------------------------------------------- 1 | @import '~@storefront-ui/vue/styles'; 2 | @import '@/assets/sass/_variables.scss'; 3 | @import '@/assets/sass/_mixins.scss'; 4 | 5 | #login-page { 6 | font-family: var(--font-family--primary); 7 | margin: 3rem auto 6rem; 8 | max-width: 65rem; 9 | box-sizing: border-box; 10 | 11 | .page-header { 12 | text-align: center; 13 | } 14 | 15 | .login-row { 16 | margin: 0 auto; 17 | max-width: 85rem; 18 | width: 100%; 19 | .customer-form { 20 | margin-top: 1rem; 21 | display: flex; 22 | flex-direction: row; 23 | 24 | @media (max-width: 801px) { 25 | flex-direction: column; 26 | } 27 | 28 | .login-form { 29 | padding-left: 0.75rem; 30 | padding-right: 0.75rem; 31 | width: 100%; 32 | 33 | @media (max-width: 801px) { 34 | margin-bottom: 1rem; 35 | } 36 | } 37 | 38 | .new-customer-form { 39 | padding-left: 0.75rem; 40 | padding-right: 0.75rem; 41 | width: 100%; 42 | 43 | .panel { 44 | position: relative; 45 | .panel-header { 46 | background-color: #e5e5e5; 47 | margin: 0; 48 | padding: 1rem 1rem 0; 49 | .panel-title { 50 | line-height: 1.5; 51 | margin: 0; 52 | font-size: 20px; 53 | } 54 | } 55 | .panel-body { 56 | margin-bottom: 1.5rem; 57 | padding: 1rem 2rem 1.5rem; 58 | background: #e5e5e5; 59 | .new-customer-fact-list { 60 | margin-left: 1rem; 61 | margin-bottom: 1rem; 62 | font-size: 15px; 63 | color: gray; 64 | > li { 65 | line-height: 20px; 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /utils/storage/index.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | 3 | // Cart 4 | export const setCartId = (cartId) => 5 | window.localStorage.setItem('cartId', cartId); 6 | export const getCartId = () => window.localStorage.getItem('cartId'); 7 | export const removeCartId = () => window.localStorage.removeItem('cartId'); 8 | 9 | // Order 10 | export const setOrderId = (orderId) => 11 | window.localStorage.setItem('orderId', orderId); 12 | export const getOrderId = () => window.localStorage.getItem('orderId'); 13 | 14 | // Seo 15 | export const setSeo = (seo) => 16 | window.localStorage.setItem( 17 | 'big-nuxt-storefront-seo', 18 | jwt.sign(seo, process.env.jwtSecret) 19 | ); 20 | export const getSeo = () => { 21 | const seo = window.localStorage.getItem('big-nuxt-storefront-seo'); 22 | if (typeof window !== 'undefined' && seo && seo !== 'null') { 23 | const data = jwt.verify(seo, process.env.jwtSecret); 24 | return data; 25 | } 26 | return null; 27 | }; 28 | 29 | // Customer 30 | export const setCustomer = (user) => 31 | window.localStorage.setItem('bigcommerceCustomer', JSON.stringify(user)); 32 | export const getCustomer = () => 33 | window.localStorage.getItem('bigcommerceCustomer'); 34 | 35 | // Cookie 36 | export const setCookie = (cookie) => 37 | window.localStorage.setItem('cookie', cookie); 38 | export const getCookie = () => window.localStorage.getItem('cookie'); 39 | 40 | // Remove Customer and Cookie 41 | export const removeUserAndCookie = () => { 42 | window.localStorage.removeItem('bigcommerceCustomer'); 43 | window.localStorage.removeItem('cookie'); 44 | }; 45 | 46 | // Wishlist 47 | export const setWishlistId = (wishlistId) => 48 | window.localStorage.setItem('wishlistId', wishlistId); 49 | export const getWishlistId = () => window.localStorage.getItem('wishlistId'); 50 | export const removeWishlistId = () => 51 | window.localStorage.removeItem('wishlistId'); 52 | -------------------------------------------------------------------------------- /assets/sass/components/checkout/orderreview.scss: -------------------------------------------------------------------------------- 1 | @import '~@storefront-ui/vue/styles'; 2 | .title { 3 | --heading-title-margin: 0 0 var(--spacer-xl) 0; 4 | --heading-title-font-weight: var(--font-weight--bold); 5 | border-bottom: 1px solid var(--c-white); 6 | } 7 | .review { 8 | box-sizing: border-box; 9 | width: 100%; 10 | &__header { 11 | display: flex; 12 | align-items: center; 13 | justify-content: space-between; 14 | } 15 | &__title { 16 | font-family: var(--font-family--secondary); 17 | font-weight: var(--font-weight--medium); 18 | font-size: var(--font-size--base); 19 | } 20 | } 21 | .promo-code { 22 | display: flex; 23 | justify-content: center; 24 | align-items: flex-start; 25 | padding-top: var(--spacer-lg); 26 | margin-top: var(--spacer-lg); 27 | border-top: var(--c-white) solid 1px; 28 | &__input { 29 | --input-background: var(--c-white); 30 | --input-label-font-size: var(--font-size--base); 31 | flex: 1; 32 | } 33 | &__button { 34 | --button-height: 1.875rem; 35 | } 36 | } 37 | .promo-code { 38 | display: flex; 39 | justify-content: center; 40 | align-items: flex-start; 41 | padding: var(--spacer-lg) 0 var(--spacer-base) 0; 42 | margin-top: var(--spacer-lg); 43 | border-top: var(--c-white) solid 1px; 44 | &__input { 45 | --input-background: var(--c-white); 46 | flex: 1; 47 | } 48 | &__button { 49 | --button-height: 30px; 50 | } 51 | } 52 | .characteristics { 53 | &__item { 54 | margin: var(--spacer-base) 0; 55 | &:last-of-type { 56 | margin-bottom: 0; 57 | } 58 | } 59 | } 60 | .content { 61 | font-family: var(--font-family--primary); 62 | font-size: var(--font-size--sm); 63 | line-height: 1.6; 64 | font-weight: var(--font-weight--normal); 65 | margin: 0; 66 | color: var(--c-dark-variant); 67 | &__label { 68 | color: var(--c-text); 69 | } 70 | &__shipping { 71 | font-weight: var(--font-weight--bold); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | parserOptions: { 8 | parser: 'babel-eslint' 9 | }, 10 | extends: [ 11 | 'eslint:recommended', 12 | 'plugin:vue/strongly-recommended', 13 | '@nuxtjs', 14 | 'prettier', 15 | 'prettier/vue', 16 | 'plugin:prettier/recommended', 17 | 'plugin:nuxt/recommended' 18 | ], 19 | plugins: ['prettier'], 20 | rules: { 21 | semi: ['error', 'always'], 22 | 'require-await': 'off', 23 | 'no-console': 'off', 24 | 'no-debugger': 'off', 25 | 'vue/order-in-components': [ 26 | 'error', 27 | { 28 | order: [ 29 | 'el', 30 | 'name', 31 | 'key', 32 | 'parent', 33 | 'functional', 34 | ['delimiters', 'comments'], 35 | ['components', 'directives', 'filters'], 36 | 'extends', 37 | 'mixins', 38 | ['provide', 'inject'], 39 | 'ROUTER_GUARDS', 40 | 'layout', 41 | 'middleware', 42 | 'validate', 43 | 'scrollToTop', 44 | 'transition', 45 | 'loading', 46 | 'inheritAttrs', 47 | 'model', 48 | ['props', 'propsData'], 49 | 'emits', 50 | 'setup', 51 | 'asyncData', 52 | 'data', 53 | 'fetch', 54 | 'head', 55 | 'computed', 56 | 'watch', 57 | 'watchQuery', 58 | 'LIFECYCLE_HOOKS', 59 | 'methods', 60 | ['template', 'render'], 61 | 'renderError' 62 | ] 63 | } 64 | ], 65 | 'prettier/prettier': [ 66 | 'error', 67 | { 68 | printWidth: 80, 69 | tabWidth: 2, 70 | useTabs: false, 71 | semi: true, 72 | singleQuote: true, 73 | bracketSpacing: true, 74 | jsxBracketSameLine: false, 75 | arrowParens: 'always', 76 | endOfLine: 'lf' 77 | } 78 | ] 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /utils/script/create-storefront-route.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | const axios = require('axios'); 3 | dotenv.config(); 4 | 5 | const defaultRoutes = [ 6 | { 7 | type: 'cart', 8 | matching: '*', 9 | route: '/cart' 10 | }, 11 | { 12 | type: 'product', 13 | matching: '*', 14 | route: '/products/{slug}' 15 | }, 16 | { 17 | type: 'category', 18 | matching: '*', 19 | route: '/{slug}' 20 | }, 21 | { 22 | type: 'home', 23 | matching: '*', 24 | route: '/' 25 | }, 26 | { 27 | type: 'account_order_status', 28 | matching: '*', 29 | route: '/login?action=view_order_status' 30 | }, 31 | { 32 | type: 'create_account', 33 | matching: '*', 34 | route: '/register' 35 | }, 36 | { 37 | type: 'login', 38 | matching: '*', 39 | route: '/login' 40 | } 41 | ]; 42 | 43 | const createRouteCall = async (siteId, route) => { 44 | return await axios.post( 45 | `${process.env.BC_API_URL}/stores/${process.env.STORE_HASH}/v3/sites/${siteId}/routes`, 46 | { 47 | ...route 48 | }, 49 | { 50 | headers: { 51 | 'Content-Type': 'application/json', 52 | 'X-Auth-Token': process.env.API_TOKEN 53 | } 54 | } 55 | ); 56 | }; 57 | 58 | module.exports.createRoute = async function (siteId, type, route) { 59 | try { 60 | if (type && route) { 61 | const response = await createRouteCall(siteId, { 62 | type, 63 | matching: '*', 64 | route 65 | }); 66 | console.log(response.data); 67 | } else { 68 | for (const rot of defaultRoutes) { 69 | const response = await createRouteCall(siteId, rot); 70 | console.log(response.data); 71 | } 72 | } 73 | return '===========SUCCESS==========='; 74 | } catch (err) { 75 | const error = err?.response?.data ?? err; 76 | console.log(error); 77 | return '===========FAILED==========='; 78 | } 79 | }; 80 | require('make-runnable/custom')({ 81 | printOutputFrame: false 82 | }); 83 | -------------------------------------------------------------------------------- /components/_profile/OrderHistory.vue: -------------------------------------------------------------------------------- 1 | 41 | 69 | 70 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 84 | 85 | -------------------------------------------------------------------------------- /assets/sass/components/shippingaddress.scss: -------------------------------------------------------------------------------- 1 | @import '~@storefront-ui/vue/styles'; 2 | .shipping-list { 3 | margin: 0 0 var(--spacer-base) 0; 4 | } 5 | .shipping { 6 | display: flex; 7 | padding: var(--spacer-base) 0; 8 | border: 1px solid var(--c-light); 9 | border-width: 1px 0 0 0; 10 | &:last-child { 11 | border-width: 1px 0 1px 0; 12 | } 13 | &__content { 14 | flex: 1; 15 | color: var(--c-text); 16 | } 17 | &__actions { 18 | display: flex; 19 | flex-direction: column; 20 | justify-content: space-between; 21 | align-items: flex-end; 22 | @include for-desktop { 23 | flex-direction: row; 24 | justify-content: flex-end; 25 | align-items: center; 26 | } 27 | } 28 | &__button-delete { 29 | --button-background: var(--c-light); 30 | --button-color: var(--c-dark-variant); 31 | &:hover { 32 | --button-background: var(--_c-light-primary); 33 | } 34 | @include for-desktop { 35 | margin: 0 0 0 var(--spacer-base); 36 | } 37 | } 38 | &__address { 39 | margin: 0 0 var(--spacer-base) 0; 40 | &:last-child { 41 | margin: 0; 42 | } 43 | } 44 | } 45 | .tab-orphan { 46 | .country_select, 47 | .region_select { 48 | width: 100%; 49 | font-size: 18px; 50 | border: none; 51 | border-bottom: 1px solid; 52 | margin-bottom: 25px; 53 | padding-bottom: 2px; 54 | } 55 | @include for-mobile { 56 | --tabs-content-border-width: 0; 57 | --tabs-title-display: none; 58 | --tabs-content-padding: 0; 59 | } 60 | } 61 | .form { 62 | @include for-desktop { 63 | display: flex; 64 | flex-wrap: wrap; 65 | align-items: center; 66 | } 67 | &__element { 68 | margin: 0 0 var(--spacer-base) 0; 69 | @include for-desktop { 70 | flex: 0 0 100%; 71 | } 72 | &--half { 73 | @include for-desktop { 74 | flex: 1 1 50%; 75 | } 76 | &-even { 77 | @include for-desktop { 78 | padding: 0 0 0 var(--spacer-lg); 79 | } 80 | } 81 | } 82 | } 83 | &__select { 84 | padding-bottom: calc(var(--font-xs) * 1.2); 85 | } 86 | } 87 | .message { 88 | margin: 0 0 var(--spacer-base) 0; 89 | } 90 | .action-button { 91 | --button-width: 100%; 92 | @include for-desktop { 93 | --button-width: auto; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /components/_profile/Wishlist.vue: -------------------------------------------------------------------------------- 1 | 42 | 74 | 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bc-nuxt-vue-starter", 3 | "version": "1.0.0", 4 | "description": "BC + Nuxt + Storefront UI Starter", 5 | "author": "Nate Stewart", 6 | "private": true, 7 | "scripts": { 8 | "dev": "nuxt", 9 | "test": "jest --runInBand --detectOpenHandles --forceExit", 10 | "dev-debug": "node --inspect node_modules/.bin/nuxt", 11 | "build": "nuxt build", 12 | "start": "nuxt start", 13 | "generate": "nuxt generate", 14 | "lint": "eslint --ext .js,.vue --ignore-path .gitignore . --fix", 15 | "set-heroku-env": "sh ./set-heroku-env.sh", 16 | "create-storefront-token": "node ./utils/script/create-storefront-token createStoreFrontToken", 17 | "create-storefront-channel": "node ./utils/script/create-storefront-channel createChannel", 18 | "create-storefront-site": "node ./utils/script/create-storefront-site createSite", 19 | "create-storefront-route": "node ./utils/script/create-storefront-route createRoute", 20 | "deploy": "npm run generate && vercel" 21 | }, 22 | "dependencies": { 23 | "@bigcommerce/checkout-sdk": "^1.160.0", 24 | "@nuxtjs/axios": "^5.12.4", 25 | "@nuxtjs/dotenv": "^1.4.1", 26 | "@nuxtjs/proxy": "^2.1.0", 27 | "@nuxtjs/pwa": "^3.3.3", 28 | "@nuxtjs/sentry": "^5.1.3", 29 | "@nuxtjs/toast": "^3.3.1", 30 | "@storefront-ui/vue": "^0.11.0", 31 | "core-js": "^3.8.1", 32 | "express": "^4.17.1", 33 | "http-status": "^1.5.0", 34 | "jsonwebtoken": "^8.5.1", 35 | "moment": "^2.29.1", 36 | "nuxt": "^2.14.12", 37 | "set-cookie-parser": "^2.4.8", 38 | "uuid": "^8.3.2", 39 | "vue-append": "^2.0.1", 40 | "vue-country-region-select": "^2.0.14", 41 | "vuelidate": "^0.7.6" 42 | }, 43 | "devDependencies": { 44 | "@nuxtjs/eslint-config": "^5.0.0", 45 | "@nuxtjs/eslint-module": "^3.0.2", 46 | "@vue/test-utils": "^1.2.2", 47 | "babel-core": "^7.0.0-bridge.0", 48 | "babel-eslint": "^10.1.0", 49 | "babel-jest": "^27.2.0", 50 | "eslint": "^7.16.0", 51 | "eslint-config-prettier": "^7.1.0", 52 | "eslint-plugin-nuxt": "^2.0.0", 53 | "eslint-plugin-prettier": "^3.3.0", 54 | "jest": "^27.2.0", 55 | "lodash": "^4.17.21", 56 | "make-runnable": "^1.3.10", 57 | "prettier": "^2.2.1", 58 | "supertest": "^6.1.6", 59 | "vue-jest": "^3.0.7" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /utils/auth/index.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import { getCustomer, setCustomer } from '../storage'; 3 | const { v4: uuidv4 } = require('uuid'); 4 | 5 | export const setUser = (user) => { 6 | if (user) { 7 | const { 8 | id, 9 | groupId, 10 | email, 11 | firstName, 12 | lastName, 13 | company, 14 | notes, 15 | phone, 16 | taxExemptCategory, 17 | addressCount, 18 | attributeCount, 19 | storeCredit 20 | } = user; 21 | const secureData = jwt.sign( 22 | { 23 | id, 24 | groupId 25 | }, 26 | process.env.jwtSecret 27 | ); 28 | user = { 29 | email, 30 | firstName, 31 | lastName, 32 | company, 33 | notes, 34 | phone, 35 | taxExemptCategory, 36 | addressCount: addressCount !== 0 ? addressCount : '', 37 | attributeCount: attributeCount !== 0 ? attributeCount : '', 38 | storeCredit: storeCredit[0], 39 | secureData 40 | }; 41 | } 42 | setCustomer(user); 43 | return user; 44 | }; 45 | 46 | export const getUser = () => { 47 | const user = getCustomer(); 48 | if (typeof window !== 'undefined' && user && user !== 'null') { 49 | return JSON.parse(user); 50 | } 51 | return null; 52 | }; 53 | 54 | export const getSecuredData = (secureData) => { 55 | const data = jwt.verify(secureData, process.env.jwtSecret); 56 | return data; 57 | }; 58 | 59 | export const getCartCheckoutRedirectUrl = (url) => { 60 | const user = getUser(); 61 | if (!user || typeof user?.secureData === 'undefined') { 62 | return url; 63 | } else { 64 | const loggedInCustomerData = getSecuredData(user.secureData); 65 | const dateCreated = Date.parse(new Date().toGMTString()) / 1000; 66 | 67 | const payload = { 68 | iss: process.env.apiClientId, 69 | iat: dateCreated, 70 | jti: uuidv4(), 71 | operation: 'customer_login', 72 | store_hash: process.env.storeHash, 73 | customer_id: loggedInCustomerData.id, 74 | redirect_to: url 75 | }; 76 | 77 | // The JWT token must be signed by the BC API Secret 78 | const token = jwt.sign(payload, process.env.apiSecret, { 79 | algorithm: 'HS256' 80 | }); 81 | const loginUrl = `${process.env.baseUrl}/login/token/${token}`; 82 | return loginUrl; 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /assets/sass/components/checkout/payment.scss: -------------------------------------------------------------------------------- 1 | @import '~@storefront-ui/vue/styles'; 2 | .title { 3 | --heading-padding: var(--spacer-xl) 0 var(--spacer-lg); 4 | --heading-title-font-weight: var(--font-weight--bold); 5 | &:not(:first-of-type) { 6 | --heading-padding: var(--spacer-base) 0; 7 | } 8 | @include for-desktop { 9 | --heading-title-font-size: var(--h3-font-size); 10 | --heading-title-font-weight: var(--font-weight--semibold); 11 | --heading-padding: var(--spacer-xl) 0; 12 | } 13 | } 14 | .form { 15 | .country_select, 16 | .region_select { 17 | width: 100%; 18 | font-size: 18px; 19 | border: none; 20 | border-bottom: 1px solid; 21 | margin-bottom: 30px; 22 | padding-bottom: 5px; 23 | } 24 | &__element { 25 | margin: 0 0 var(--spacer-base) 0; 26 | &:last-of-type { 27 | margin: 0; 28 | } 29 | } 30 | &__checkbox { 31 | margin: 0 0 var(--spacer-xl) 0; 32 | } 33 | &__group { 34 | display: flex; 35 | align-items: center; 36 | } 37 | &__select { 38 | display: flex; 39 | align-items: center; 40 | --select-option-font-size: var(--font-size--base); 41 | --select-dropdown-color: blue; 42 | ::v-deep .sf-select__dropdown { 43 | margin: 0 0 2px 0; 44 | font-size: var(--font-size--base); 45 | font-family: var(--font-family--secondary); 46 | color: var(--c-link); 47 | } 48 | } 49 | &__radio { 50 | margin: var(--spacer-xs) 0; 51 | &:last-of-type { 52 | margin: var(--spacer-xs) 0 var(--spacer-xl); 53 | } 54 | ::v-deep .sf-radio__container { 55 | --radio-container-padding: var(--spacer-xs); 56 | @include for-desktop { 57 | --radio-container-padding: var(--spacer-xs) var(--spacer-xs) 58 | var(--spacer-xs) var(--spacer-sm); 59 | } 60 | } 61 | } 62 | @include for-desktop { 63 | display: flex; 64 | flex-wrap: wrap; 65 | align-items: center; 66 | &:last-of-type { 67 | margin: 0 calc(var(--spacer-2xl) - var(--spacer-sm)) 0 0; 68 | } 69 | &__element { 70 | margin: 0 0 var(--spacer-sm) 0; 71 | flex: 0 0 100%; 72 | &--half { 73 | flex: 1 1 50%; 74 | &-even { 75 | padding: 0 0 0 var(--spacer-base); 76 | } 77 | } 78 | } 79 | &__radio-group { 80 | flex: 0 0 calc(100% + var(--spacer-sm)); 81 | margin: 0 calc(var(--spacer-sm) * -1); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /assets/sass/layouts/default.scss: -------------------------------------------------------------------------------- 1 | @import '~@storefront-ui/vue/styles'; 2 | 3 | html { 4 | font-size: 16px; 5 | word-spacing: 1px; 6 | -ms-text-size-adjust: 100%; 7 | -webkit-text-size-adjust: 100%; 8 | -moz-osx-font-smoothing: grayscale; 9 | -webkit-font-smoothing: antialiased; 10 | box-sizing: border-box; 11 | } 12 | 13 | *, 14 | *:before, 15 | *:after { 16 | box-sizing: border-box; 17 | margin: 0; 18 | } 19 | 20 | .button--green { 21 | display: inline-block; 22 | border-radius: 4px; 23 | border: 1px solid #3b8070; 24 | color: #3b8070; 25 | text-decoration: none; 26 | padding: 10px 30px; 27 | } 28 | 29 | .button--green:hover { 30 | color: #fff; 31 | background-color: #3b8070; 32 | } 33 | 34 | .button--grey { 35 | display: inline-block; 36 | border-radius: 4px; 37 | border: 1px solid #35495e; 38 | color: #35495e; 39 | text-decoration: none; 40 | padding: 10px 30px; 41 | margin-left: 15px; 42 | } 43 | 44 | .button--grey:hover { 45 | color: #fff; 46 | background-color: #35495e; 47 | } 48 | .sf-modal__container { 49 | z-index: 10000; 50 | transform: none !important; 51 | top: 100px !important; 52 | box-shadow: 0 4px 10px rgba(168, 172, 176, 0.19); 53 | 54 | .sf-bar.sf-modal__bar { 55 | display: none; 56 | } 57 | 58 | .sf-modal__content { 59 | padding: 0; 60 | h2 { 61 | text-align: center; 62 | padding: 20px; 63 | } 64 | .product_list { 65 | position: relative; 66 | background: white; 67 | list-style-type: none; 68 | overflow: scroll; 69 | overflow-x: hidden; 70 | height: 20em; 71 | line-height: 2em; 72 | padding: 0; 73 | margin: 0; 74 | width: 100%; 75 | right: 0px; 76 | top: 0px; 77 | 78 | @include for-desktop { 79 | border: 1px solid #eee; 80 | border-radius: 3px; 81 | } 82 | } 83 | 84 | .list_item { 85 | padding: 10px; 86 | display: flex; 87 | align-items: center; 88 | border-top: 1px solid #eee; 89 | cursor: pointer; 90 | 91 | .sf-image--wrapper { 92 | margin-right: 0.5rem; 93 | } 94 | 95 | &:first-of-type { 96 | border-top: none; 97 | } 98 | 99 | &:hover { 100 | background-color: #eee; 101 | } 102 | } 103 | } 104 | } 105 | .sf-footer { 106 | margin-top: 2rem; 107 | } 108 | -------------------------------------------------------------------------------- /store/customer.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { API_URL } from '~/config/constants'; 3 | import { setUser, getUser } from '~/utils/auth'; 4 | import { getCookie, removeUserAndCookie, setCookie } from '~/utils/storage'; 5 | 6 | export const state = () => ({ 7 | customer: null, 8 | loggedIn: false, 9 | loginCred: null 10 | }); 11 | 12 | export const getters = { 13 | customer(state) { 14 | return state.customer; 15 | }, 16 | loggedIn(state) { 17 | return state.loggedIn; 18 | }, 19 | loginCred(state) { 20 | return state.loginCred; 21 | } 22 | }; 23 | 24 | export const mutations = { 25 | SET_CUSTOMER(state, customer) { 26 | state.customer = customer; 27 | }, 28 | SET_LOGGEDIN(state, loggedIn) { 29 | state.loggedIn = loggedIn; 30 | }, 31 | SET_LOGINCRED(state, loginCred) { 32 | state.loginCred = loginCred; 33 | } 34 | }; 35 | 36 | export const actions = { 37 | async login({ dispatch, commit }, variables) { 38 | try { 39 | const { data } = await axios.post(`${API_URL}/customerLogin`, { 40 | variables 41 | }); 42 | 43 | const user = setUser(data.data.customer); 44 | setCookie(data.cookie); 45 | commit('SET_CUSTOMER', user); 46 | dispatch('isLoggedIn'); 47 | } catch (error) { 48 | console.log(error); 49 | this.$toast.error('Something went wrong in login'); 50 | } 51 | }, 52 | async createCustomer({ commit }, variables) { 53 | try { 54 | await axios.post(`${API_URL}/customerRegister`, { 55 | variables 56 | }); 57 | this.$toast.success('Successfully registered!'); 58 | commit('SET_LOGINCRED', { 59 | email: variables.email, 60 | password: variables._authentication.password 61 | }); 62 | this.$router.push('/login'); 63 | } catch (error) { 64 | console.log(error); 65 | this.$toast.error('Something went wrong in register'); 66 | } 67 | }, 68 | async logOut({ commit }) { 69 | try { 70 | const cookie = getCookie(); 71 | await axios.post(`${API_URL}/customerLogOut`, { 72 | cookie 73 | }); 74 | 75 | commit('SET_LOGGEDIN', false); 76 | removeUserAndCookie(); 77 | } catch (error) { 78 | console.log(error); 79 | this.$toast.error('Something went wrong in logout'); 80 | } 81 | }, 82 | isLoggedIn({ commit }) { 83 | const user = getUser(); 84 | commit('SET_LOGGEDIN', !!user); 85 | commit('SET_CUSTOMER', user); 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Env Variables 2 | ## General 3 | APP_NAME='BigCommerce Nuxt Vue Starter' 4 | PORT=3000 5 | JWT_SECRET='***' 6 | ## Your API Config with Channels, Sites, Information & Settings, Customers, Orders, Customer Login and Storefront Token scopes 7 | ## This is used by the app for REST API calls and the CLI commands to automatically configure the rest of the env variables 8 | BC_API_URL='https://api.bigcommerce.com' 9 | STORE_HASH='***' 10 | API_TOKEN='***' 11 | API_CLIENT_ID='***' 12 | API_SECRET='***' 13 | ## BigCommerce Storefront Channel and Site Config 14 | ## This is used by the app to know which GraphQL hostname to use and utilize the right Storefront Channel in BigCommerce so 15 | ## areas like the checkout and customer emails route back to the headless site instead of the default BigCommerce storefront 16 | ## 17 | ## *These can be automatically set by the `create-storefront-channel -name {channel-name} -url {base-url}` CLI command* 18 | ## The command: 19 | ## - Creates a Channel: https://developer.bigcommerce.com/api-reference/store-management/channels/channels/createchannel 20 | ## - Creates a Channel Site: https://developer.bigcommerce.com/api-reference/store-management/channels/channel-site/postchannelsite 21 | ## - Creates Routes for the Site that point home, cart, login, register, and product links back to the headless site: 22 | ## https://developer.bigcommerce.com/api-reference/store-management/sites/site-routes/post-site-route 23 | ## 24 | ## Your Storefront Channel ID 25 | ## Used in all of the Cart API requests to make sure checkout and beyond respect any specific Storefront Channel settings 26 | CHANNEL_ID=1234567890 27 | ## Your Storefront Channel Base URL 28 | ## BASE_URL should be created like below 29 | BASE_URL='https://store-${STORE_HASH}-${CHANNEL_ID}.mybigcommerce.com' 30 | ## Your Storefront API Token, You can create here https://developer.bigcommerce.com/api-reference/store-management/tokens/api-token/createtoken 31 | ## This is used to authenticate with the GraphQL Storefront API 32 | ## 33 | ## *Can be automatically set by the create-storefront-channel or create-storefront-token CLI commands* 34 | ## 35 | STOREFRONT_API_TOKEN='***' 36 | ## Check type should be one of `redirected`, `custom`, `embedded` 37 | CHECKOUT_TYPE='redirected' 38 | PAYMENT_URL="https://payments.bigcommerce.com" 39 | ## This should be one of 'heroku', 'netlify', 'vercel' 40 | DEPLOY_PLATFORM='vercel' 41 | ## Netlify serverless function URL 42 | NETLIFY_API_URL="http://localhost:8888/.netlify/functions" 43 | ## Sentry ENV Variables 44 | SENTRY_DSN="https://****.ingest.sentry.io/***" 45 | -------------------------------------------------------------------------------- /api/controller/cart.js: -------------------------------------------------------------------------------- 1 | import { customAxios } from '../utils/axios'; 2 | 3 | export const getCart = async (req, res, next) => { 4 | try { 5 | const cartId = req.query.cartId; 6 | const { data } = await customAxios('api').get( 7 | `/stores/${process.env.STORE_HASH}/v3/carts/${cartId}?include=redirect_urls,line_items.physical_items.options` 8 | ); 9 | res.json(data); 10 | } catch (error) { 11 | next(error); 12 | } 13 | }; 14 | 15 | export const createCart = async (req, res, next) => { 16 | try { 17 | const cartData = req.body.cartData; 18 | const { data } = await customAxios('api').post( 19 | `/stores/${process.env.STORE_HASH}/v3/carts?include=redirect_urls`, 20 | cartData 21 | ); 22 | res.json(data); 23 | } catch (error) { 24 | next(error); 25 | } 26 | }; 27 | 28 | export const addCartItem = async (req, res, next) => { 29 | try { 30 | const cartId = req.query.cartId; 31 | const cartData = req.body.cartData; 32 | const { data } = await customAxios('api').post( 33 | `/stores/${process.env.STORE_HASH}/v3/carts/${cartId}/items?include=redirect_urls`, 34 | cartData 35 | ); 36 | res.json(data); 37 | } catch (error) { 38 | next(error); 39 | } 40 | }; 41 | 42 | export const updateCartItem = async (req, res, next) => { 43 | try { 44 | const cartId = req.query.cartId; 45 | const itemId = req.query.itemId; 46 | const cartData = req.body.cartData; 47 | const { data } = await customAxios('api').put( 48 | `/stores/${process.env.STORE_HASH}/v3/carts/${cartId}/items/${itemId}?include=redirect_urls`, 49 | cartData 50 | ); 51 | res.json(data); 52 | } catch (error) { 53 | next(error); 54 | } 55 | }; 56 | 57 | export const deleteCartItem = async (req, res, next) => { 58 | try { 59 | const cartId = req.query.cartId; 60 | const itemId = req.query.itemId; 61 | const { data } = await customAxios('api').delete( 62 | `/stores/${process.env.STORE_HASH}/v3/carts/${cartId}/items/${itemId}?include=redirect_urls` 63 | ); 64 | res.json(data); 65 | } catch (error) { 66 | next(error); 67 | } 68 | }; 69 | 70 | export const updateCartWithCustomerId = async (req, res, next) => { 71 | try { 72 | const cartId = req.query.cartId; 73 | const customerId = req.query.customerId; 74 | const { data } = await customAxios('api').put( 75 | `/stores/${process.env.STORE_HASH}/v3/carts/${cartId}`, 76 | { 77 | customer_id: customerId 78 | } 79 | ); 80 | res.json(data); 81 | } catch (error) { 82 | next(error); 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /components/checkout/basic/SpDropdown.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /assets/sass/components/cartsidebar.scss: -------------------------------------------------------------------------------- 1 | @import '~@storefront-ui/vue/styles'; 2 | #cart { 3 | @include for-desktop { 4 | & > * { 5 | --sidebar-bottom-padding: var(--spacer-base); 6 | --sidebar-content-padding: var(--spacer-base); 7 | } 8 | } 9 | } 10 | .cart-summary { 11 | margin-top: var(--spacer-xl); 12 | } 13 | 14 | .my-cart { 15 | flex: 1; 16 | display: flex; 17 | flex-direction: column; 18 | &__total-items { 19 | margin: 0; 20 | } 21 | &__total-price { 22 | --price-font-size: var(--font-size--xl); 23 | --price-font-weight: var(--font-weight--medium); 24 | margin: 0 0 var(--spacer-base) 0; 25 | } 26 | } 27 | .empty-cart { 28 | --heading-description-margin: 0 0 var(--spacer-xl) 0; 29 | --heading-title-margin: 0 0 var(--spacer-xl) 0; 30 | --heading-title-color: var(--c-primary); 31 | --heading-title-font-weight: var(--font-weight--semibold); 32 | display: flex; 33 | flex: 1; 34 | align-items: center; 35 | flex-direction: column; 36 | &__banner { 37 | display: flex; 38 | justify-content: center; 39 | flex-direction: column; 40 | align-items: center; 41 | flex: 1; 42 | } 43 | &__heading { 44 | padding: 0 var(--spacer-base); 45 | } 46 | &__image { 47 | --image-width: 13.1875rem; 48 | margin: 0 0 var(--spacer-xl) 0; 49 | @include for-desktop { 50 | --image-width: 23.3125rem; 51 | margin: 0 0 var(--spacer-2xl) 0; 52 | } 53 | } 54 | @include for-desktop { 55 | --heading-title-font-size: var(--font-size--xl); 56 | --heading-title-margin: 0 0 var(--spacer-sm) 0; 57 | } 58 | } 59 | .collected-product-list { 60 | flex: 1; 61 | } 62 | .collected-product { 63 | margin: 0 0 var(--spacer-sm) 0; 64 | &__properties { 65 | margin: var(--spacer-xs) 0 0 0; 66 | display: flex; 67 | flex-direction: column; 68 | justify-content: flex-end; 69 | align-items: flex-start; 70 | flex: 2; 71 | &:first-child { 72 | margin-bottom: 8px; 73 | } 74 | } 75 | &__actions { 76 | transition: opacity 150ms ease-in-out; 77 | } 78 | &__save, 79 | &__compare { 80 | --button-padding: 0; 81 | &:focus { 82 | --cp-save-opacity: 1; 83 | --cp-compare-opacity: 1; 84 | } 85 | } 86 | &__save { 87 | opacity: var(--cp-save-opacity, 0); 88 | } 89 | &__compare { 90 | opacity: var(--cp-compare-opacity, 0); 91 | } 92 | &:hover { 93 | --cp-save-opacity: 1; 94 | --cp-compare-opacity: 1; 95 | @include for-desktop { 96 | .collected-product__properties { 97 | display: none; 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /assets/sass/pages/home.scss: -------------------------------------------------------------------------------- 1 | @import '~@storefront-ui/vue/styles'; 2 | @import '@/assets/sass/_variables.scss'; 3 | @import '@/assets/sass/_mixins.scss'; 4 | 5 | #home { 6 | box-sizing: border-box; 7 | @include for-desktop { 8 | max-width: 1240px; 9 | margin: auto; 10 | } 11 | } 12 | .call-to-action-newsletter { 13 | margin: var(--spacer-lg) 0; 14 | box-sizing: border-box; 15 | @include for-desktop { 16 | margin: calc(var(--spacer-xl) * 2) 0; 17 | } 18 | } 19 | .banner-central { 20 | @include for-desktop { 21 | padding-right: 30%; 22 | } 23 | } 24 | .banner-application { 25 | min-height: 420px; 26 | max-width: 1040px; 27 | margin: auto; 28 | padding-right: calc(25% + 5rem); 29 | padding-left: 2.5rem; 30 | line-height: 1.6; 31 | &__title { 32 | margin: var(--spacer-lg) 0 0 0; 33 | font-size: var(--h1-font-size-mobile); 34 | font-weight: var(--h1-font-weight-mobile); 35 | @include for-desktop { 36 | font-size: var(--h1-font-size-desktop); 37 | font-weight: var(--h1-font-weight-desktop); 38 | } 39 | } 40 | &__subtitle { 41 | color: #a3a5ad; 42 | font-family: var(--body-font-family-primary); 43 | font-size: var(--font-size-extra-big-mobile); 44 | font-weight: var(--body-font-weight-primary); 45 | @include for-desktop { 46 | font-size: var(--font-size-extra-big-desktop); 47 | } 48 | } 49 | &__download { 50 | max-height: 47px; 51 | margin-top: var(--spacer-xl); 52 | & + & { 53 | margin-left: var(--spacer-lg); 54 | } 55 | } 56 | } 57 | .banners { 58 | margin: var(--spacer-lg) 0; 59 | @include for-desktop { 60 | margin: var(--spacer-xl) 0; 61 | } 62 | } 63 | 64 | .images-grid { 65 | display: flex; 66 | flex-wrap: wrap; 67 | justify-content: center; 68 | align-items: center; 69 | 70 | &__col { 71 | flex-basis: 45%; 72 | padding: 1rem; 73 | 74 | @include for-mobile { 75 | padding-top: 1rem; 76 | flex-basis: 100%; 77 | } 78 | } 79 | } 80 | .product-card { 81 | max-width: unset; 82 | &:hover { 83 | @include for-desktop { 84 | box-shadow: 0 4px 20px rgba(168, 172, 176, 0.19); 85 | } 86 | } 87 | } 88 | .product-carousel { 89 | margin: -20px -20px -20px 0; 90 | @include for-desktop { 91 | margin: -20px 0; 92 | } 93 | ::v-deep .sf-carousel__wrapper { 94 | padding: 20px 0; 95 | @include for-desktop { 96 | padding: 20px; 97 | max-width: calc(100% - 216px); 98 | } 99 | } 100 | } 101 | .section { 102 | padding-left: var(--spacer-lg); 103 | padding-right: var(--spacer-lg); 104 | @include for-desktop { 105 | padding-left: 0; 106 | padding-right: 0; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /components/_profile/MyProfile.vue: -------------------------------------------------------------------------------- 1 | 79 | 95 | 96 | -------------------------------------------------------------------------------- /assets/sass/components/checkout/personal.scss: -------------------------------------------------------------------------------- 1 | @import '~@storefront-ui/vue/styles'; 2 | .title { 3 | --heading-padding: var(--spacer-xl) 0 var(--spacer-base); 4 | --heading-title-font-weight: var(--font-weight--bold); 5 | @include for-desktop { 6 | --heading-title-font-size: var(--h3-font-size); 7 | --heading-title-font-weight: var(--font-weight--semibold); 8 | --heading-padding: var(--spacer-xl) 0; 9 | } 10 | } 11 | .log-in { 12 | &__info { 13 | margin: 0; 14 | color: var(--c-dark-variant); 15 | font: var(--font-weight--medium) var(--font-size--base) / 1.6 16 | var(--font-family--secondary); 17 | @include for-desktop { 18 | font-weight: var(--font-weight--normal); 19 | } 20 | } 21 | &__button { 22 | margin: var(--spacer-xl) 0 var(--spacer-base) 0; 23 | @include for-desktop { 24 | margin: var(--spacer-xl) 0; 25 | --button-width: 25rem; 26 | } 27 | } 28 | } 29 | .info { 30 | &__heading { 31 | font-family: var(--font-family--secondary); 32 | font-weight: var(--font-weight--medium); 33 | color: var(--c-link); 34 | margin-bottom: var(--spacer-base); 35 | } 36 | &__characteristic { 37 | --characteristic-description-font-size: var(--font-size--base); 38 | margin: 0 0 var(--spacer-base) var(--spacer-2xs); 39 | } 40 | @include for-desktop { 41 | width: 37.5rem; 42 | display: flex; 43 | flex-wrap: wrap; 44 | margin: 0; 45 | &__heading { 46 | flex: 100%; 47 | margin: 0 0 var(--spacer-lg) 0; 48 | } 49 | &__characteristic { 50 | margin: 0 0 var(--spacer-2xs) 0; 51 | flex: 0 50%; 52 | box-sizing: border-box; 53 | padding-right: var(--spacer-3xl); 54 | &:nth-of-type(2), 55 | &:nth-of-type(3) { 56 | padding-right: var(--spacer-2xl); 57 | } 58 | } 59 | } 60 | } 61 | .form { 62 | &__element { 63 | --input-padding: var(--spacer-sm) 0 var(--spacer-2xs) 0; 64 | margin: 0 0 var(--spacer-base) 0; 65 | } 66 | &__checkbox { 67 | margin: var(--spacer-base) 0 var(--spacer-xl); 68 | --checkbox-font-family: var(--font-family--primary); 69 | --checkbox-font-size: var(--font-size--base); 70 | } 71 | &__action-button { 72 | &:first-child { 73 | margin: var(--spacer-sm) 0 0 0; 74 | } 75 | &--secondary { 76 | margin: var(--spacer-base) 0; 77 | } 78 | @include for-desktop { 79 | --button-width: 25rem; 80 | } 81 | } 82 | @include for-desktop { 83 | display: flex; 84 | flex-wrap: wrap; 85 | align-items: center; 86 | &__element { 87 | margin: 0 0 var(--spacer-base) 0; 88 | flex: 0 0 100%; 89 | &--half { 90 | flex: 1 1 50%; 91 | &-even { 92 | padding: 0 0 0 var(--spacer-base); 93 | } 94 | } 95 | } 96 | &__checkbox { 97 | margin: var(--spacer-lg) 0 var(--spacer-xl); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/getProductsByCategory", 5 | "destination": "/api/routes" 6 | }, 7 | { "source": "/getProductBySlug", "destination": "/api/routes" }, 8 | { "source": "/getCategories", "destination": "/api/routes" }, 9 | { "source": "/searchProductByKey", "destination": "/api/routes" }, 10 | { "source": "/getProductOption", "destination": "/api/routes" }, 11 | { "source": "/getAllAddresses", "destination": "/api/routes" }, 12 | { "source": "/updateAddress", "destination": "/api/routes" }, 13 | { "source": "/addAddress", "destination": "/api/routes" }, 14 | { "source": "/deleteAddress", "destination": "/api/routes" }, 15 | { "source": "/getAllOrders", "destination": "/api/routes" }, 16 | { "source": "/createOrder", "destination": "/api/routes" }, 17 | { "source": "/getCart", "destination": "/api/routes" }, 18 | { "source": "/createCart", "destination": "/api/routes" }, 19 | { "source": "/addCartItem", "destination": "/api/routes" }, 20 | { "source": "/updateCartItem", "destination": "/api/routes" }, 21 | { "source": "/deleteCartItem", "destination": "/api/routes" }, 22 | { 23 | "source": "/updateCartWithCustomerId", 24 | "destination": "/api/routes" 25 | }, 26 | { "source": "/customerLogin", "destination": "/api/routes" }, 27 | { "source": "/customerRegister", "destination": "/api/routes" }, 28 | { "source": "/customerLogOut", "destination": "/api/routes" }, 29 | { "source": "/getCheckout", "destination": "/api/routes" }, 30 | { 31 | "source": "/setConsignmentToCheckout", 32 | "destination": "/api/routes" 33 | }, 34 | { 35 | "source": "/updateConsignmentToCheckout", 36 | "destination": "/api/routes" 37 | }, 38 | { 39 | "source": "/updateShippingOption", 40 | "destination": "/api/routes" 41 | }, 42 | { 43 | "source": "/setBillingAddressToCheckout", 44 | "destination": "/api/routes" 45 | }, 46 | { 47 | "source": "/getPaymentMethodByOrder", 48 | "destination": "/api/routes" 49 | }, 50 | { "source": "/processPayment", "destination": "/api/routes" }, 51 | { "source": "/addCoupons", "destination": "/api/routes" }, 52 | { "source": "/storefront", "destination": "/api/routes" }, 53 | { "source": "/getHomePageContentWidgets", "destination": "/api/routes" } 54 | ], 55 | "env": { 56 | "APP_NAME": "BigCommerce Nuxt Vue Starter", 57 | "PORT": "3000", 58 | "JWT_SECRET": "***", 59 | "BC_API_URL": "https://api.bigcommerce.com", 60 | "STORE_HASH": "***", 61 | "API_TOKEN": "***", 62 | "API_CLIENT_ID": "***", 63 | "API_SECRET": "***", 64 | "CHANNEL_ID": "***", 65 | "BASE_URL": "https://store-storehash-channelid.mybigcommerce.com", 66 | "STOREFRONT_API_TOKEN": "***", 67 | "CHECKOUT_TYPE": "embedded", 68 | "PAYMENT_URL": "https://payments.bigcommerce.com", 69 | "DEPLOY_PLATFORM": "vercel", 70 | "NETLIFY_API_URL": "" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /store/address.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { API_URL } from '~/config/constants'; 3 | import { getSecuredData, getUser } from '~/utils/auth'; 4 | 5 | export const state = () => ({ 6 | addresses: [] 7 | }); 8 | 9 | export const getters = { 10 | addresses(state) { 11 | return state.addresses; 12 | } 13 | }; 14 | 15 | export const mutations = { 16 | SET_ADDRESSES(state, addresses) { 17 | state.addresses = addresses; 18 | } 19 | }; 20 | 21 | export const actions = { 22 | async getAllAddresses({ commit }) { 23 | try { 24 | const user = getUser(); 25 | const customer = getSecuredData(user.secureData); 26 | const { data } = await axios.get( 27 | `${API_URL}/getAllAddresses?customerId=${customer.id}` 28 | ); 29 | const addresses = data.map((item) => ({ 30 | address_type: item.address_type, 31 | city: item.city, 32 | company: item.company, 33 | country: item.country, 34 | customer_id: item.customer_id, 35 | first_name: item.first_name, 36 | id: item.id, 37 | last_name: item.last_name, 38 | phone: item.phone, 39 | state: item.state, 40 | street_1: item.street_1, 41 | street_2: item.street_2, 42 | zip: item.zip 43 | })); 44 | commit('SET_ADDRESSES', addresses); 45 | } catch (error) { 46 | console.log(error); 47 | this.$toast.error('Something went wrong in getting addresses'); 48 | } 49 | }, 50 | async updateAddress({ dispatch }, address) { 51 | try { 52 | const customerId = address.customer_id; 53 | const id = address.id; 54 | delete address.id; 55 | await axios.put( 56 | `${API_URL}/updateAddress?customerId=${customerId}&addressId=${id}`, 57 | { 58 | address 59 | } 60 | ); 61 | dispatch('getAllAddresses'); 62 | this.$toast.success('Successfully updated!'); 63 | } catch (error) { 64 | console.log(error); 65 | this.$toast.error('Something went wrong in updating address'); 66 | } 67 | }, 68 | async addAddress({ dispatch }, address) { 69 | try { 70 | const user = getUser(); 71 | const customer = getSecuredData(user.secureData); 72 | delete address.id; 73 | delete address.customer_id; 74 | await axios.post(`${API_URL}/addAddress?customerId=${customer.id}`, { 75 | address 76 | }); 77 | dispatch('getAllAddresses'); 78 | this.$toast.success('Successfully created!'); 79 | } catch (error) { 80 | console.log(error); 81 | this.$toast.error('Something went wrong in adding address'); 82 | } 83 | }, 84 | async deleteAddress({ dispatch }, { customerId, addressId }) { 85 | try { 86 | await axios.delete( 87 | `/deleteAddress?customerId=${customerId}&addressId=${addressId}` 88 | ); 89 | dispatch('getAllAddresses'); 90 | this.$toast.success('Successfully deleted!'); 91 | } catch (error) { 92 | console.log(error); 93 | this.$toast.error('Something went wrong in deleting address'); 94 | } 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /utils/queries/product-by-slug.js: -------------------------------------------------------------------------------- 1 | module.exports.productBySlug = (params) => { 2 | return `query LookUpUrl { 3 | site { 4 | route(path: "/${params.slug}/") { 5 | node { 6 | __typename 7 | ... on Product { 8 | id 9 | entityId 10 | productOptions { 11 | edges { 12 | node { 13 | entityId 14 | displayName 15 | isRequired 16 | } 17 | } 18 | } 19 | variants { 20 | edges { 21 | node { 22 | id 23 | entityId 24 | options { 25 | edges { 26 | node { 27 | displayName 28 | values { 29 | edges { 30 | node { 31 | entityId 32 | label 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | name 43 | description 44 | addToCartUrl 45 | defaultImage { 46 | url640wide: url(width: 640) 47 | } 48 | images { 49 | edges { 50 | node { 51 | altText 52 | mobile: url(width: 400, height: 400) 53 | desktop: url(width: 600, height: 600) 54 | big: url(width: 1200, height: 1200) 55 | } 56 | } 57 | } 58 | brand { 59 | name 60 | seo { 61 | pageTitle 62 | metaDescription 63 | metaKeywords 64 | } 65 | } 66 | path 67 | prices { 68 | price { 69 | value 70 | currencyCode 71 | } 72 | salePrice { 73 | value 74 | currencyCode 75 | } 76 | } 77 | reviewSummary { 78 | numberOfReviews 79 | summationOfRatings 80 | } 81 | options { 82 | edges { 83 | node { 84 | entityId 85 | isRequired 86 | displayName 87 | values { 88 | edges { 89 | node { 90 | entityId 91 | label 92 | } 93 | } 94 | } 95 | } 96 | } 97 | } 98 | customFields { 99 | edges { 100 | node { 101 | name 102 | value 103 | } 104 | } 105 | } 106 | } 107 | } 108 | } 109 | } 110 | }`; 111 | }; 112 | -------------------------------------------------------------------------------- /assets/sass/pages/cart.scss: -------------------------------------------------------------------------------- 1 | @import '~@storefront-ui/vue/styles'; 2 | @import '@/assets/sass/_variables.scss'; 3 | @import '@/assets/sass/_mixins.scss'; 4 | 5 | #detailed-cart { 6 | box-sizing: border-box; 7 | @include for-desktop { 8 | max-width: 1272px; 9 | margin: 0 auto; 10 | padding: 0 var(--spacer-sm); 11 | } 12 | } 13 | .breadcrumbs { 14 | padding: var(--spacer-base) 0; 15 | } 16 | .detailed-cart { 17 | &__main { 18 | padding: 0 var(--spacer-sm); 19 | @include for-desktop { 20 | padding: 0; 21 | } 22 | } 23 | &__aside { 24 | box-sizing: border-box; 25 | width: 100%; 26 | background: var(--c-light); 27 | padding: var(--spacer-base) var(--spacer-sm); 28 | } 29 | @include for-desktop { 30 | display: flex; 31 | &__main { 32 | flex: 1; 33 | } 34 | &__aside { 35 | flex: 0 0 26.8125rem; 36 | order: 1; 37 | margin: 0 0 0 var(--spacer-xl); 38 | padding: var(--spacer-xl); 39 | } 40 | } 41 | } 42 | .checkout-action { 43 | box-sizing: border-box; 44 | width: 100%; 45 | padding: var(--spacer-base) var(--spacer-sm); 46 | @include for-desktop { 47 | padding: 0; 48 | } 49 | } 50 | .collected-product { 51 | --collected-product-padding: var(--spacer-sm) 0; 52 | --collected-product-actions-display: flex; 53 | border: 1px solid var(--c-light); 54 | border-width: 1px 0 0 0; 55 | &:first-of-type { 56 | border-top: none; 57 | } 58 | &__properties { 59 | --property-value-font-weight: var(--font-weight--normal); 60 | margin: var(--spacer-sm) 0 0 0; 61 | display: flex; 62 | flex-direction: column; 63 | justify-content: flex-end; 64 | align-items: flex-start; 65 | flex: 2; 66 | } 67 | @include for-mobile { 68 | --collected-product-remove-bottom: var(--spacer-sm); 69 | } 70 | @include for-desktop { 71 | --collected-product-padding: var(--spacer-lg) 0; 72 | &:hover { 73 | --collected-product-configuration-display: flex !important; 74 | } 75 | } 76 | } 77 | .actions { 78 | &__button { 79 | display: block; 80 | margin: 0 0 var(--spacer-xs) 0; 81 | color: var(--c-text); 82 | &:hover { 83 | color: var(--c-text-muted); 84 | } 85 | } 86 | &__description { 87 | font-family: var(--font-family--primary); 88 | font-size: var(--font-size--sm); 89 | font-weight: var(--font-weight--light); 90 | color: var(--c-text-muted); 91 | position: absolute; 92 | bottom: 0; 93 | padding-bottom: var(--spacer-lg); 94 | } 95 | } 96 | .empty-cart { 97 | --heading-title-color: var(--c-primary); 98 | --heading-title-margin: 0 0 var(--spacer-base) 0; 99 | --heading-description-margin: 0 0 var(--spacer-xl) 0; 100 | --heading-title-font-weight: var(--font-weight--semibold); 101 | display: flex; 102 | flex: 1; 103 | align-items: center; 104 | flex-direction: column; 105 | &__image { 106 | --image-width: 13.1875rem; 107 | margin: var(--spacer-2xl) 0; 108 | } 109 | @include for-desktop { 110 | &__image { 111 | --image-width: 22rem; 112 | } 113 | &__button { 114 | --button-width: 20.9375rem; 115 | } 116 | } 117 | } 118 | .cart-checkout { 119 | margin-top: 2rem; 120 | } 121 | -------------------------------------------------------------------------------- /components/CustomerProfile.vue: -------------------------------------------------------------------------------- 1 | 40 | 105 | 110 | -------------------------------------------------------------------------------- /components/LayoutDefaultHeader.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 123 | -------------------------------------------------------------------------------- /assets/sass/components/checkout/shipping.scss: -------------------------------------------------------------------------------- 1 | @import '~@storefront-ui/vue/styles'; 2 | .title { 3 | --heading-padding: var(--spacer-xl) 0 var(--spacer-lg); 4 | --heading-title-font-weight: var(--font-weight--bold); 5 | &:not(:first-of-type) { 6 | --heading-padding: var(--spacer-base) 0; 7 | } 8 | @include for-desktop { 9 | --heading-title-font-size: var(--h3-font-size); 10 | --heading-title-font-weight: var(--font-weight--semibold); 11 | --heading-padding: var(--spacer-xl) 0; 12 | } 13 | } 14 | .form { 15 | .country_select, 16 | .region_select { 17 | width: 100%; 18 | font-size: 18px; 19 | border: none; 20 | border-bottom: 1px solid; 21 | margin-bottom: 30px; 22 | padding-bottom: 5px; 23 | } 24 | &__element { 25 | margin: 0 0 var(--spacer-base) 0; 26 | &:last-of-type { 27 | margin: 0; 28 | } 29 | } 30 | &__group { 31 | display: flex; 32 | align-items: center; 33 | } 34 | &__select { 35 | display: flex; 36 | align-items: center; 37 | --select-option-font-size: var(--font-size--base); 38 | --select-dropdown-color: blue; 39 | ::v-deep .sf-select__dropdown { 40 | margin: 0 0 2px 0; 41 | font-size: var(--font-size--base); 42 | font-family: var(--font-family--secondary); 43 | color: var(--c-link); 44 | } 45 | } 46 | &__radio { 47 | margin: var(--spacer-xs) 0; 48 | &:last-of-type { 49 | margin: var(--spacer-xs) 0 var(--spacer-xl); 50 | } 51 | ::v-deep .sf-radio__container { 52 | --radio-container-padding: var(--spacer-xs); 53 | @include for-desktop { 54 | --radio-container-padding: var(--spacer-xs) var(--spacer-xs) 55 | var(--spacer-xs) var(--spacer-sm); 56 | } 57 | } 58 | } 59 | @include for-desktop { 60 | display: flex; 61 | flex-wrap: wrap; 62 | align-items: center; 63 | &:last-of-type { 64 | margin: 0 calc(var(--spacer-2xl) - var(--spacer-sm)) 0 0; 65 | } 66 | &__element { 67 | margin: 0 0 var(--spacer-sm) 0; 68 | flex: 0 0 100%; 69 | &--half { 70 | flex: 1 1 50%; 71 | &-even { 72 | padding: 0 0 0 var(--spacer-base); 73 | } 74 | } 75 | } 76 | &__radio-group { 77 | flex: 0 0 calc(100% + var(--spacer-sm)); 78 | margin: 0 calc(var(--spacer-sm) * -1); 79 | } 80 | } 81 | } 82 | .shipping { 83 | --radio-container-padding: var(--spacer-sm); 84 | &__label { 85 | display: flex; 86 | justify-content: space-between; 87 | align-items: flex-end; 88 | &-price { 89 | font-size: var(--font-size--lg); 90 | text-transform: uppercase; 91 | } 92 | } 93 | &__description { 94 | --radio-description-margin: 0; 95 | } 96 | &__delivery { 97 | color: var(--c-text-muted); 98 | font-weight: var(--font-weight--normal); 99 | display: flex; 100 | width: 10.625rem; 101 | @include for-desktop { 102 | font-weight: var(--font-weight--light); 103 | } 104 | } 105 | &__action { 106 | margin: 0 0 0 var(--spacer-xs); 107 | &::before { 108 | content: '+'; 109 | } 110 | &--is-active { 111 | --button-color: var(--c-primary); 112 | --button-transition: color 150ms linear; 113 | &::before { 114 | content: '-'; 115 | } 116 | } 117 | } 118 | @include for-desktop { 119 | &__label { 120 | justify-content: space-between; 121 | } 122 | &__delivery { 123 | width: 100%; 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /components/checkout/OrderReview.vue: -------------------------------------------------------------------------------- 1 | 71 | 114 | 119 | -------------------------------------------------------------------------------- /pages/login/index.vue: -------------------------------------------------------------------------------- 1 | 60 | 112 | 113 | -------------------------------------------------------------------------------- /api/controller/product.js: -------------------------------------------------------------------------------- 1 | import { customAxios } from '../utils/axios'; 2 | const queries = require('../../utils/queries'); 3 | 4 | export const searchProductByKey = async (req, res, next) => { 5 | try { 6 | const searchKey = req.query.key; 7 | const { data } = await customAxios('api').get( 8 | `/stores/${process.env.STORE_HASH}/v3/catalog/products?keyword=${searchKey}&keyword_context=${searchKey}&include=primary_image` 9 | ); 10 | res.json(data); 11 | } catch (error) { 12 | next(error); 13 | } 14 | }; 15 | 16 | export const getCategories = async (req, res, next) => { 17 | try { 18 | const { data } = await customAxios('graphql').post(`/graphql`, { 19 | query: queries.category() 20 | }); 21 | res.json(data); 22 | } catch (error) { 23 | next(error); 24 | } 25 | }; 26 | 27 | export const getProductsByCategory = async (req, res, next) => { 28 | try { 29 | const path = req.query.path; 30 | const pageParam = req.query.pageParam; 31 | const { data } = await customAxios('graphql').post(`/graphql`, { 32 | query: queries.productsByCategory(path, pageParam) 33 | }); 34 | res.json(data); 35 | } catch (error) { 36 | next(error); 37 | } 38 | }; 39 | 40 | export const getProductBySlug = async (req, res, next) => { 41 | try { 42 | const { data } = await customAxios('graphql').post(`/graphql`, { 43 | query: queries.productBySlug(req.query) 44 | }); 45 | res.json(data); 46 | } catch (error) { 47 | next(error); 48 | } 49 | }; 50 | 51 | export const getProductOption = async (req, res, next) => { 52 | try { 53 | const productId = req.query.productId; 54 | const { data } = await customAxios('api').get( 55 | `/stores/${process.env.STORE_HASH}/v3/catalog/products/${productId}?include=options,variants,modifiers&include_fields=id` 56 | ); 57 | res.json(data); 58 | } catch (error) { 59 | next(error); 60 | } 61 | }; 62 | 63 | export const createWishlist = async (req, res, next) => { 64 | try { 65 | const wishlistData = req.body.wishlistData; 66 | const { data } = await customAxios('api').post( 67 | `/stores/${process.env.STORE_HASH}/v3/wishlists`, 68 | wishlistData 69 | ); 70 | res.json(data); 71 | } catch (error) { 72 | next(error); 73 | } 74 | }; 75 | 76 | export const addToWishlistItem = async (req, res, next) => { 77 | try { 78 | const wishlistId = req.query.wishlistId; 79 | const wishlistData = req.body.wishlistData; 80 | const { data } = await customAxios('api').post( 81 | `/stores/${process.env.STORE_HASH}/v3/wishlists/${wishlistId}/items`, 82 | wishlistData 83 | ); 84 | res.json(data); 85 | } catch (error) { 86 | next(error); 87 | } 88 | }; 89 | 90 | export const getWishlist = async (req, res, next) => { 91 | try { 92 | const wishlistId = req.query.wishlistId; 93 | const { data } = await customAxios('api').get( 94 | `/stores/${process.env.STORE_HASH}/v3/wishlists/${wishlistId}` 95 | ); 96 | res.json(data); 97 | } catch (error) { 98 | next(error); 99 | } 100 | }; 101 | 102 | export const getProductsByIds = async (req, res, next) => { 103 | try { 104 | const productIds = req.query.productIds; 105 | const { data } = await customAxios('api').get( 106 | `/stores/${process.env.STORE_HASH}/v3/catalog/products?include_fields=name,description,price&include=variants,primary_image&id:in=${productIds}` 107 | ); 108 | res.json(data); 109 | } catch (error) { 110 | next(error); 111 | } 112 | }; 113 | 114 | export const deleteWishlistItem = async (req, res, next) => { 115 | try { 116 | const wishlistId = req.query.wishlistId; 117 | const wishlistItemId = req.query.wishlistItemId; 118 | await customAxios('api').delete( 119 | `/stores/${process.env.STORE_HASH}/v3/wishlists/${wishlistId}/items/${wishlistItemId}` 120 | ); 121 | res.json(); 122 | } catch (error) { 123 | next(error); 124 | } 125 | }; 126 | -------------------------------------------------------------------------------- /components/checkout/OrderSummary.vue: -------------------------------------------------------------------------------- 1 | 57 | 132 | 137 | -------------------------------------------------------------------------------- /pages/checkout/index.vue: -------------------------------------------------------------------------------- 1 | 56 | 131 | 136 | -------------------------------------------------------------------------------- /api/controller/checkout.js: -------------------------------------------------------------------------------- 1 | import { customAxios } from '../utils/axios'; 2 | 3 | export const getCheckout = async (req, res, next) => { 4 | try { 5 | const checkoutId = req.query.checkoutId; 6 | const { data } = await customAxios('api').get( 7 | `/stores/${process.env.STORE_HASH}/v3/checkouts/${checkoutId}?includes=consignments.available_shipping_options` 8 | ); 9 | res.json(data); 10 | } catch (error) { 11 | next(error); 12 | } 13 | }; 14 | 15 | export const setConsignmentToCheckout = async (req, res, next) => { 16 | try { 17 | const checkoutId = req.query.checkoutId; 18 | const consignment = req.body.consignment; 19 | const { data } = await customAxios('api').post( 20 | `/stores/${process.env.STORE_HASH}/v3/checkouts/${checkoutId}/consignments`, 21 | consignment 22 | ); 23 | res.json(data); 24 | } catch (error) { 25 | next(error); 26 | } 27 | }; 28 | 29 | export const updateConsignmentToCheckout = async (req, res, next) => { 30 | try { 31 | const checkoutId = req.query.checkoutId; 32 | const consignmentId = req.query.consignmentId; 33 | const consignment = req.body.consignment; 34 | const { data } = await customAxios('api').put( 35 | `/stores/${process.env.STORE_HASH}/v3/checkouts/${checkoutId}/consignments/${consignmentId}`, 36 | consignment 37 | ); 38 | res.json(data); 39 | } catch (error) { 40 | next(error); 41 | } 42 | }; 43 | 44 | export const updateShippingOption = async (req, res, next) => { 45 | try { 46 | const checkoutId = req.query.checkoutId; 47 | const consignmentId = req.query.consignmentId; 48 | const shippingOptionId = req.query.shippingOptionId; 49 | const { data } = await customAxios( 50 | 'api' 51 | ).put( 52 | `/stores/${process.env.STORE_HASH}/v3/checkouts/${checkoutId}/consignments/${consignmentId}`, 53 | { shipping_option_id: shippingOptionId } 54 | ); 55 | res.json(data); 56 | } catch (error) { 57 | next(error); 58 | } 59 | }; 60 | 61 | export const setBillingAddressToCheckout = async (req, res, next) => { 62 | try { 63 | const checkoutId = req.query.checkoutId; 64 | const billData = req.body.data; 65 | const { data } = await customAxios('api').post( 66 | `/stores/${process.env.STORE_HASH}/v3/checkouts/${checkoutId}/billing-address`, 67 | billData 68 | ); 69 | res.json(data); 70 | } catch (error) { 71 | next(error); 72 | } 73 | }; 74 | 75 | export const getPaymentMethodByOrder = async (req, res, next) => { 76 | try { 77 | const orderId = req.query.orderId; 78 | const { data } = await customAxios('api').get( 79 | `/stores/${process.env.STORE_HASH}/v3/payments/methods?order_id=${orderId}` 80 | ); 81 | res.json(data); 82 | } catch (error) { 83 | next(error); 84 | } 85 | }; 86 | 87 | export const processPayment = async (req, res, next) => { 88 | try { 89 | const orderId = req.query.orderId; 90 | const paymentData = req.body.payment; 91 | 92 | paymentData.payment.instrument.expiry_month = parseInt( 93 | paymentData.payment.instrument.expiry_month 94 | ); 95 | paymentData.payment.instrument.expiry_year = parseInt( 96 | paymentData.payment.instrument.expiry_year 97 | ); 98 | 99 | const tokenResult = await customAxios('api').post( 100 | `/stores/${process.env.STORE_HASH}/v3/payments/access_tokens`, 101 | { 102 | order: { 103 | id: parseInt(orderId) 104 | } 105 | } 106 | ); 107 | 108 | const { data } = await customAxios( 109 | 'payment', 110 | null, 111 | tokenResult.data?.data?.id 112 | ).post(`/stores/${process.env.STORE_HASH}/payments`, paymentData); 113 | 114 | res.json(data); 115 | } catch (error) { 116 | next(error); 117 | } 118 | }; 119 | 120 | export const addCoupons = async (req, res, next) => { 121 | try { 122 | const { checkoutId, couponCode } = req.body; 123 | 124 | const { data } = await customAxios('api').get( 125 | `/stores/${process.env.STORE_HASH}/v3/checkouts/${checkoutId}/coupons`, 126 | { 127 | coupon_code: couponCode 128 | } 129 | ); 130 | res.json(data); 131 | } catch (error) { 132 | next(error); 133 | } 134 | }; 135 | -------------------------------------------------------------------------------- /api/routes/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | getAllAddresses, 4 | updateAddress, 5 | addAddress, 6 | deleteAddress 7 | } from '../controller/address'; 8 | import { 9 | getCart, 10 | createCart, 11 | addCartItem, 12 | updateCartItem, 13 | deleteCartItem, 14 | updateCartWithCustomerId 15 | } from '../controller/cart'; 16 | import { 17 | getCheckout, 18 | setConsignmentToCheckout, 19 | updateConsignmentToCheckout, 20 | updateShippingOption, 21 | setBillingAddressToCheckout, 22 | getPaymentMethodByOrder, 23 | processPayment, 24 | addCoupons 25 | } from '../controller/checkout'; 26 | import { 27 | customerLogin, 28 | customerLogOut, 29 | customerRegister 30 | } from '../controller/customer'; 31 | import { getAllOrders, createOrder } from '../controller/order'; 32 | import { 33 | getProductsByCategory, 34 | getProductBySlug, 35 | getCategories, 36 | searchProductByKey, 37 | getProductOption, 38 | createWishlist, 39 | addToWishlistItem, 40 | getWishlist, 41 | getProductsByIds, 42 | deleteWishlistItem 43 | } from '../controller/product'; 44 | import { 45 | storefront, 46 | getHomePageContentWidgets 47 | } from '../controller/storefront'; 48 | import { permissionMiddleware } from '../middleware'; 49 | 50 | const router = Router(); 51 | // address 52 | router.get('/getAllAddresses', permissionMiddleware, getAllAddresses); 53 | router.put('/updateAddress', permissionMiddleware, updateAddress); 54 | router.post('/addAddress', permissionMiddleware, addAddress); 55 | router.delete('/deleteAddress', permissionMiddleware, deleteAddress); 56 | 57 | router.get('/getCart', permissionMiddleware, getCart); 58 | router.post('/createCart', permissionMiddleware, createCart); 59 | router.post('/addCartItem', permissionMiddleware, addCartItem); 60 | router.put('/updateCartItem', permissionMiddleware, updateCartItem); 61 | router.delete('/deleteCartItem', permissionMiddleware, deleteCartItem); 62 | router.put( 63 | '/updateCartWithCustomerId', 64 | permissionMiddleware, 65 | updateCartWithCustomerId 66 | ); 67 | // checkout 68 | router.get('/getCheckout', permissionMiddleware, getCheckout); 69 | router.post( 70 | '/setConsignmentToCheckout', 71 | permissionMiddleware, 72 | setConsignmentToCheckout 73 | ); 74 | router.put( 75 | '/updateConsignmentToCheckout', 76 | permissionMiddleware, 77 | updateConsignmentToCheckout 78 | ); 79 | router.put('/updateShippingOption', permissionMiddleware, updateShippingOption); 80 | router.post( 81 | '/setBillingAddressToCheckout', 82 | permissionMiddleware, 83 | setBillingAddressToCheckout 84 | ); 85 | router.get( 86 | '/getPaymentMethodByOrder', 87 | permissionMiddleware, 88 | getPaymentMethodByOrder 89 | ); 90 | router.post('/processPayment', permissionMiddleware, processPayment); 91 | router.post('/addCoupons', permissionMiddleware, addCoupons); 92 | // customer 93 | router.post('/customerLogin', customerLogin); 94 | router.post('/customerRegister', customerRegister); 95 | router.post('/customerLogOut', customerLogOut); 96 | // order 97 | router.get('/getAllOrders', permissionMiddleware, getAllOrders); 98 | router.post('/createOrder', permissionMiddleware, createOrder); 99 | // product 100 | router.get( 101 | '/getProductsByCategory', 102 | permissionMiddleware, 103 | getProductsByCategory 104 | ); 105 | router.get('/getProductBySlug', permissionMiddleware, getProductBySlug); 106 | router.get('/getCategories', permissionMiddleware, getCategories); 107 | router.get('/searchProductByKey', permissionMiddleware, searchProductByKey); 108 | router.get('/getProductOption', permissionMiddleware, getProductOption); 109 | // storefront 110 | router.get('/storefront', permissionMiddleware, storefront); 111 | router.get( 112 | '/getHomePageContentWidgets', 113 | permissionMiddleware, 114 | getHomePageContentWidgets 115 | ); 116 | // wishlist 117 | router.post('/createWishlist', permissionMiddleware, createWishlist); 118 | router.post('/addToWishlistItem', permissionMiddleware, addToWishlistItem); 119 | router.get('/getWishlist', permissionMiddleware, getWishlist); 120 | router.get('/getProductsByIds', permissionMiddleware, getProductsByIds); 121 | router.delete('/deleteWishlistItem', permissionMiddleware, deleteWishlistItem); 122 | 123 | export default router; 124 | -------------------------------------------------------------------------------- /components/checkout/PersonalDetails.vue: -------------------------------------------------------------------------------- 1 | 64 | 139 | 144 | --------------------------------------------------------------------------------