Nuxt.js 2 рд╡реЙрдХрдереНрд░реВ рднрд╛рдЧ 3 рдкрд░ рдПрдХ рдСрдирд▓рд╛рдЗрди рд╕реНрдЯреЛрд░ рдмрдирд╛рдирд╛


рдЬреИрд╕рд╛ рдХрд┐ рд╡рд╛рджрд╛ рдХрд┐рдпрд╛ рдЧрдпрд╛ рдерд╛, рд╣рдо рдЬрд╛рд░реА рд░рдЦрддреЗ рд╣реИрдВред


рдЗрд╕ рд╣рд┐рд╕реНрд╕реЗ рдореЗрдВ:


  • рдорд╛рд▓ рдХреЗ рдмреНрд▓реЙрдХ рдмрдирд╛рдПрдБ "рд╡реЗ рднреА рдЗрд╕ рдЙрддреНрдкрд╛рдж рдХреЗ рд╕рд╛рде рдЦрд░реАрджрддреЗ рд╣реИрдВ" рдФрд░ "рджрд┐рд▓рдЪрд╕реНрдк рд╕рд╛рдорд╛рди"
  • рдЙрддреНрдкрд╛рджреЛрдВ рдХреА рд╕рдВрдЦреНрдпрд╛ рдХреЗ рд╕рд╛рде рдПрдХ рдЯреЛрдХрд░реА рдЖрдЗрдХрди рдмрдирд╛рдПрдВ
  • рдЯреЛрдХрд░реА рдореЗрдВ рдорд╛рд▓ рдХреЗ рд╕рд╛рде рдПрдХ рдореЛрдбрд▓ рд╡рд┐рдВрдбреЛ рдХрдиреЗрдХреНрдЯ рдХрд░реЗрдВ
  • рд╕рднреА рд╕реНрдЯреЛрд░ рддрд░реНрдХ рдХреЛ рдлрд┐рд░ рд╕реЗ рд▓рд┐рдЦрдирд╛

рдЪрд▓реЛ рдкреНрд░рдХрд╛рд░ рдХреЗ рд╕рд╛рдорд╛рдиреЛрдВ рдХреЗ рдмреНрд▓реЙрдХ рдмрдирд╛рддреЗ рд╣реИрдВ: "рд╡реЗ рднреА рдЗрд╕ рдЙрддреНрдкрд╛рдж рдХреЗ рд╕рд╛рде рдЦрд░реАрджрддреЗ рд╣реИрдВ"


Nuxt. . - , ?


AlsoBuyList asyncData, . , html , asyncData mounted ( ).


True SSR, asyncData . , , . , , , SSR.


.


Vuex :


index.js
// function for Mock API
const sleep = m => new Promise(r => setTimeout(r, m))
const sampleSize = require('lodash.samplesize')
const categories = [
  {
    id: 'cats',
    cTitle: '',
    cName: '',
    cSlug: 'cats',
    cMetaDescription: ' ',
    cDesc: '',
    cImage: 'https://source.unsplash.com/300x300/?cat,cats',
    products: []
  },
  {
    id: 'dogs',
    cTitle: '',
    cName: '',
    cSlug: 'dogs',
    cMetaDescription: ' ',
    cDesc: '',
    cImage: 'https://source.unsplash.com/300x300/?dog,dogs',
    products: []
  },
  {
    id: 'wolfs',
    cTitle: '',
    cName: '',
    cSlug: 'wolfs',
    cMetaDescription: ' ',
    cDesc: '',
    cImage: 'https://source.unsplash.com/300x300/?wolf',
    products: []
  },
  {
    id: 'bulls',
    cTitle: '',
    cName: '',
    cSlug: 'bulls',
    cMetaDescription: ' ',
    cDesc: '',
    cImage: 'https://source.unsplash.com/300x300/?bull',
    products: []
  }
]
function getProductsByIds (products, productsImages, ids) {
  const innerProducts = products.filter(p => p.id === ids.find(id => p.id === id))
  if (!innerProducts) return null
  return innerProducts.map(pr => {
    return {
      ...pr,
      images: productsImages.find(img => img.id === pr.id).urls,
      category: categories.find(cat => cat.id === pr.category_id)
    }
  })
}
function getProduct (products, productsImages, productSlug) {
  const innerProduct = products.find(p => p.pSlug === productSlug)
  if (!innerProduct) return null
  return {
    ...innerProduct,
    images: productsImages.find(img => img.id === innerProduct.id).urls,
    category: categories.find(cat => cat.id === innerProduct.category_id)
  }
}
function addProductsToCategory (products, productsImages, category) {
  const categoryInner = { ...category, products: [] }
  products.map(p => {
    if (p.category_id === category.id) {
      categoryInner.products.push({
        id: p.id,
        pName: p.pName,
        pSlug: p.pSlug,
        pPrice: p.pPrice,
        image: productsImages.find(img => img.id === p.id).urls
      })
    }
  })
  return categoryInner
}
function getBreadcrumbs (pageType, route, data) {
  const crumbs = []
  crumbs.push({
    title: '',
    url: '/'
  })
  switch (pageType) {
    case 'category':
      crumbs.push({
        title: data.cName,
        url: `/category/${data.cSlug}`
      })
      break
    case 'product':
      crumbs.push({
        title: data.category.cName,
        url: `/category/${data.category.cSlug}`
      })
      crumbs.push({
        title: data.pName,
        url: `/product/${data.pSlug}`
      })

      break

    default:
      break
  }
  return crumbs
}
export const state = () => ({
  categoriesList: [],
  currentCategory: {},
  currentProduct: {
    alsoBuyProducts: [],
    interestingProducts: []
  },
  bredcrumbs: []
})
export const mutations = {
  SET_CATEGORIES_LIST (state, categories) {
    state.categoriesList = categories
  },
  SET_CURRENT_CATEGORY (state, category) {
    state.currentCategory = category
  },
  SET_CURRENT_PRODUCT (state, product) {
    state.currentProduct = product
  },
  SET_BREADCRUMBS (state, crumbs) {
    state.bredcrumbs = crumbs
  },
  RESET_BREADCRUMBS (state) {
    state.bredcrumbs = []
  },
  GET_PRODUCTS_BY_IDS () {}
}
export const actions = {
  async getProductsListByIds ({ commit }) {
    // simulate api work
    const [products, productsImages] = await Promise.all(
      [
        this.$axios.$get('/mock/products.json'),
        this.$axios.$get('/mock/products-images.json')
      ]

    )
    commit('GET_PRODUCTS_BY_IDS')
    const idsArray = (sampleSize(products, 5)).map(p => p.id)
    return getProductsByIds(products, productsImages, idsArray)
  },
  async setBreadcrumbs ({ commit }, data) {
    await commit('SET_BREADCRUMBS', data)
  },
  async getCategoriesList ({ commit }) {
    try {
      await sleep(50)
      await commit('SET_CATEGORIES_LIST', categories)
    } catch (err) {
      console.log(err)
      throw new Error('  ,  ')
    }
  },
  async getCurrentCategory ({ commit, dispatch }, { route }) {
    await sleep(50)
    const category = categories.find((cat) => cat.cSlug === route.params.CategorySlug)
    // simulate api work
    const [products, productsImages] = await Promise.all(
      [
        this.$axios.$get('/mock/products.json'),
        this.$axios.$get('/mock/products-images.json')
      ]
    )
    const crubms = getBreadcrumbs('category', route, category)
    await dispatch('setBreadcrumbs', crubms)

    await commit('SET_CURRENT_CATEGORY', addProductsToCategory(products, productsImages, category))
  },
  async getCurrentProduct ({ commit, dispatch }, { route }) {
    await sleep(50)
    const productSlug = route.params.ProductSlug
    // simulate api work
    const [products, productsImages, alsoBuyProducts, interestingProducts] = await Promise.all(
      [
        this.$axios.$get('/mock/products.json'),
        this.$axios.$get('/mock/products-images.json'),
        dispatch('getProductsListByIds'),
        dispatch('getProductsListByIds')
      ]

    )
    const product = getProduct(products, productsImages, productSlug)
    const crubms = getBreadcrumbs('product', route, product)
    await dispatch('setBreadcrumbs', crubms)
    await commit('SET_CURRENT_PRODUCT', { ...product, alsoBuyProducts, interestingProducts })
  }

}

getProductsByIds api
getProductsListByIds action . 5


    await sleep(50)
    const productSlug = route.params.ProductSlug
    // simulate api work
    const [products, productsImages, alsoBuyProducts, interestingProducts] = await Promise.all(
      [
        this.$axios.$get('/mock/products.json'),
        this.$axios.$get('/mock/products-images.json'),
        dispatch('getProductsListByIds'),
        dispatch('getProductsListByIds')
      ]

    )
    const product = getProduct(products, productsImages, productSlug)
    const crubms = getBreadcrumbs('product', route, product)
    dispatch('setBreadcrumbs', crubms)
    commit('SET_CURRENT_PRODUCT', { ...product, alsoBuyProducts, interestingProducts })

. alsoBuyProducts interestingProducts .


actions, . :


        const products = await this.$axios.$get('/mock/products.json')
        const productsImages = await this.$axios.$get('/mock/products-images.json')
        const alsoBuyProducts = await dispatch('getProductsListByIds')
        const interestingProducts = await dispatch('getProductsListByIds')

.


:



,


    <h2>    </h2>
    <ProductsList :products="product.alsoBuyProducts" />
    <h2>   </h2>
    <ProductsList :products="product.interestingProducts" />

ProductsList


ProductsList.vue
<template>
  <div :class="$style.wrapper">
    <div v-for="product in products" :key="product.id">
      <nuxt-link :to="`/product/${product.pSlug}`">
        <p>{{ product.pName }}</p>
        <img
          v-lazy="product.images.imgL"
          :class="$style.image"
        />
      </nuxt-link>
      <p> {{ product.pPrice }}</p>
      <BuyButton :product="product" />
    </div>
  </div>
</template>

<script>
import BuyButton from '~~/components/common/BuyButton'
export default {
  components: {
    BuyButton
  },
  props: {
    products: {
      type: Array,
      default: () => []
    }
  }
}
</script>

<style lang="scss" module>
.wrapper {
  display: flex;
  flex-wrap: wrap;
  > div {
    margin: 1em;

  }
  p {
    max-width: 270px;
    height: 35px;
  }
}
.image {
  width: 300px;
  height: 300px;
  object-fit: cover;
}
</style>

. :



?


, client-side only, ( ). ( ) , .


24 !


:


  1. id , .
  2. action, id - (, , .).
  3. getter, -.
  4. vuex store localstorage , api .
  5. 100500 .
  6. , <client-only>, DOM ( ).


2 :


  • products { qty, productId, order }
  • metaProducts api (, , .)

, 1 . getter. computed , .



  getProductsInCart: state => {
    const products = []
    state.products.map(p => {
      const metaProduct = state.metaProducts.find(mp => mp.id === p.productId)
      if (metaProduct) {
        products.push({ ...p, meta: metaProduct })
      }
    })
    return products.sort((a, b) => a.order - b.order)
  }

. -,


{ ...p, meta: metaProduct }

p { qty, productId, order }, const products


products, order .


, merge. , api , products. , , , . , -.


, , - api, merge . .


mutations


export const mutations = {
  ADD_PRODUCT (state, productId) {
    // if cart doesn't have product add it
    if (!state.products.find(p => productId === p.productId)) {
      state.products = [...state.products, { productId: productId, qty: 1, order: findMax(state.products, 'order') + 1 }]
    }
  },
  REMOVE_PRODUCT (state, productId) {
    state.products = Array.from(state.products.filter(prod => prod.productId !== productId))
  },
  SET_PRODUCT_QTY (state, { productId, qty }) {
    state.products = [
      ...state.products.filter(prod => prod.productId !== productId),
      { ...state.products.find(prod => prod.productId === productId), qty }
    ]
  },
  SET_PRODUCTS_BY_IDS (state, products) {
    state.metaProducts = products
  }

}

ADD_PRODUCT тАФ , , qty = 1 ( 1 .) + 1.



const findMax = (array, field) => {
  if (!array || array.lenght === 0) return 1
  return Math.max(...array.map(o => o[field]), 0)
}

, .
REMOVE_PRODUCT тАФ
SET_PRODUCT_QTY тАФ
SET_PRODUCTS_BY_IDS тАФ -


actions


export const actions = {
  async setProductsListByIds ({ commit, state }) {
    // simulate api work
    await sleep(50)
    const [products, productsImages] = await Promise.all(
      [
        this.$axios.$get('/mock/products.json'),
        this.$axios.$get('/mock/products-images.json')
      ]

    )
    const productsIds = state.products.map(p => p.productId)
    await commit('SET_PRODUCTS_BY_IDS', mock.getProductsByIds(products, productsImages, productsIds))
  },
  async addProduct ({ commit, dispatch }, productId) {
    // simulate api work
    await sleep(50)
    await commit('ADD_PRODUCT', productId)
    await dispatch('setProductsListByIds')
  },
  async removeProduct ({ commit, dispatch }, productId) {
    // simulate api work
    await sleep(50)
    await commit('REMOVE_PRODUCT', productId)
    await dispatch('setProductsListByIds')
  },
  async setProductQuantity ({ commit, dispatch }, { productId, qty }) {
    // simulate api work
    await sleep(50)
    await commit('SET_PRODUCT_QTY', { productId, qty })
    await dispatch('setProductsListByIds')
  }

}

await dispatch('setProductsListByIds') - api.


, api , , , . , , .



vue-modal.js


import Vue from 'vue'
import VModal from 'vue-js-modal'

export default async (context, inject) => {
  Vue.use(VModal)
}

nuxt.config.js


{ src: '~~/plugins/vue-modal.js', mode: 'client' },


mode: 'client' ,


components/modals CastomerCartModal.vue


CastomerCartModal.vue
<template>
  <div>
    <client-only>
      <modal
        name="customer-cart"
        transition="pop-out"
        height="95%"
        width="95%"
        :max-width="960"
        :adaptive="true"
        :scrollable="true"
        :pivot-y="0.5"
        :reset="true"
        classes="v--modal-customer-cart"
        @before-open="beforeOpen"
      >
        <div class="modal-wrapper content-padding">
          <div class=" header-block">
            <p class="h1-header">
              Cart
            </p>
            <div class="close" @click="$modal.hide('customer-cart')">
              <CloseOrDeleteButton />
            </div>
          </div>
          <div v-if="getProductsInCart.length === 0" class="">
            <p>
                ,      :)
            </p>
          </div>
          <template v-else>
            <div class="wrapper">
              <template v-if="getAddedProduct">
                <p class="added-product ">
                  You've added
                </p>
                <ProductsList class="" :products-from-cart="getAddedProduct" />
                <p v-if="getProducts.length > 0" class="added-product ">
                  Previously added products
                </p>
              </template>
              <ProductsList class="products" :products-from-cart="getProducts" />
            </div>
            <div>Total: {{ getAmount | round }}</div>
            <div class="bottom">
              <a class="button color-grey close-button" @click.prevent="$modal.hide('customer-cart')">
                Close
              </a>
              <div class="amount-block">
                <nuxt-link
                  to="/checkout"
                  class="button color-primary checkout-button"
                >
                  To checkout
                </nuxt-link>
              </div>
            </div>
          </template>
        </div>
      </modal>
    </client-only>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
import ProductsList from '~~/components/cart/ProductsList.vue'
import CloseOrDeleteButton from '~~/components/common/input/CloseOrDeleteButton.vue'
import round from '~~/mixins/round.js'
export default {
  components: {
    ProductsList,
    CloseOrDeleteButton
  },
  mixins: [round],
  data () {
    return {
      addedProduct: null,
      defaults: {
        addedProduct: null
      }
    }
  },

  computed: {
    ...mapGetters({
      getProductsInCart: 'cart/getProductsInCart'
    }),
    getAddedProduct () {
      const product = this.getProductsInCart.find(
        prod => prod.productId === this.addedProduct
      )
      if (product) {
        return [product]
      } else {
        return null
      }
    },
    getAmount () {
      let amount = 0
      if (this.getProductsInCart && this.getProductsInCart.length > 0) {
        this.getProductsInCart.forEach(product => {
          amount +=
            parseFloat(product.meta.pPrice) *
            parseInt(product.qty)
        })
        return amount
      } else {
        return 0
      }
    },
    getProducts () {
      if (this.addedProduct) {
        return this.getProductsInCart.filter(
          prod => prod.productId !== this.addedProduct
        )
      } else {
        return this.getProductsInCart
      }
    }

  },

  watch: {
    $route: function () {
      this.$modal.hide('customer-cart')
    },
    getProductsInCart: function (newVal, oldVal) {
      if (oldVal.length > 0) {
        if (this.getProductsInCart.length === 0) {
          this.$modal.hide('customer-cart')
        }
      }
    }
  },
  methods: {
    beforeOpen (event) {
      if (!event.params) {
        event.params = {}
      }
      if (event.params.addedProduct) {
        this.addedProduct = event.params.addedProduct
      } else {
        this.addedProduct = this.defaults.addedProduct
      }
    }
  }
}
</script>
<style lang="scss">
</style>
<style lang="scss" scoped>
.submit-error {
  font-weight: 500;
  color: #de0d0d;
  // font-weight: 300;
  font-size: 0.7em;
}
.modal-wrapper {
  // border: 3px solid $accent-border-color;
  background: #fff;
  overflow-y: scroll;
  // margin-top: 20px;
  height: 100%;

  display: flex;
  flex-direction: column;
  align-items: stretch;
}
.bottom {
  flex-shrink: 0;
  margin-bottom: 30px;
  display: flex;
  justify-content: flex-start;
  // align-items: flex-start;
  flex-direction: column;
  margin-top: 16px;
  @media screen and (min-width: 1024px) {
    justify-content: space-between;
    flex-direction: row;
    align-items: flex-end;
    padding-bottom: 50px;
  }
  .close-button {
    display: none;
  @media screen and (min-width: 1024px) {
      display: block;
    }
  }
  .amount-block {
    .checkout-button {
      margin-top: 5px;
      width: 100%;
      display: flex;
        @media screen and (min-width: 640px) {
        width: auto;
      }
    }
  }

  button.bttn-material-flat {
    font-size: 1rem;
  @media screen and (min-width: 1024px) {
      font-size: 1.4rem;
    }
  }
}
p.added-product {
  font-size: 1.6rem;
  margin-bottom: 1rem;
}

.wrapper {
  // height: 100%;
  flex-grow: 1;
  position: relative;
}
.header-block {
  flex-shrink: 0;
  // padding: 10px 20px;
  margin-top: 20px;
  // background: #f8fafb;
  // font-size: 1.6rem;
  position: relative;
  margin-bottom: 1rem;
  .close {
    position: absolute;
    right: 12px;
    top: 0;
  }
}

.pop-out-enter-active,
.pop-out-leave-active {
  transition: all 0.5s;
}
.pop-out-enter,
.pop-out-leave-active {
  opacity: 0;
  transform: translateY(24px);
}
</style>

.


<client-only>, Vue .



  • name="customer-cart" , / ( )
  • :adaptive="true" 95%, ( , 95%)
  • :scrollable="true" body .
  • :pivot-y="0.5"
  • :reset="true" ( )
  • classes="v--modal-customer-cart"
  • @before-open="beforeOpen" ( )

<div class="close" @click="$modal.hide('customer-cart')">
  <CloseOrDeleteButton />
</div>

CloseOrDeleteButton svg ( .
click="$modal.hide('customer-cart')" ,


<template v-if="getAddedProduct">
  <p class="added-product ">
    You've added
  </p>
  <ProductsList class="" :products-from-cart="getAddedProduct" />
  <p v-if="getProducts.length > 0" class="added-product ">
    Previously added products
  </p>
</template>
<ProductsList class="products" :products-from-cart="getProducts" />

ProductsList , .
, , , You've added, Previously added products. ( , , getAddedProduct null .


getAddedProduct getProducts тАФ , addedProduct .


<div>Total: {{ getAmount | round }}</div>

| () , round ( ) .


mixins\round.js


export default {
  filters: {
    round (num) {
      return Math.round((num + Number.EPSILON) * 100) / 100
    }
  }
}


import round from '~~/mixins/round.js'
...
mixins: [round]

,


this.$modal.show('customer-cart', { addedProduct: this.product.id })

beforeOpen


  methods: {
    beforeOpen (event) {
      if (!event.params) {
        event.params = {}
      }
      if (event.params.addedProduct) {
        this.addedProduct = event.params.addedProduct
      } else {
        this.addedProduct = this.defaults.addedProduct
      }
    }
  }

, data()


  data () {
    return {
      addedProduct: null,
      defaults: {
        addedProduct: null
      }
    }
  },

store.


import { mapGetters } from 'vuex'
...
  computed: {
    ...mapGetters({
      getProductsInCart: 'cart/getProductsInCart'
    }),
...

actions .


- "" ( , ) getProductsInCart , .


    getAddedProduct () {
      const product = this.getProductsInCart.find(
        prod => prod.productId === this.addedProduct
      )
      if (product) {
        return [product]
      } else {
        return null
      }
    },
    getProducts () {
      if (this.addedProduct) {
        return this.getProductsInCart.filter(
          prod => prod.productId !== this.addedProduct
        )
      } else {
        return this.getProductsInCart
      }
    }

- , ( ). :


  watch: {
    $route: function () {
      this.$modal.hide('customer-cart')
    }

route .


, , .


  watch: {
...
    getProductsInCart: function (newVal, oldVal) {
      if (oldVal.length > 0) {
        if (this.getProductsInCart.length === 0) {
          this.$modal.hide('customer-cart')
        }
      }
    }
...

ProductsList


:


  • ( debounce , )

ProductsList.vue
<template>
  <div v-if="productsFromCart.length > 0" :class="$style.wrapper">
    <div
      v-for="product in productsFromCart"
      :key="product.productId"
      :class="$style.product"
    >
      <template>
        <CloseOrDeleteButton
          :class="$style.remove"
          button-type="delete"
          @click.native="onRemoveClickHandler(product)"
        />
        <nuxt-link :to="`/product/${product.meta.pSlug}`">
          <img
            v-lazy="product.meta.images.imgL"
            :class="$style.image"
          />
        </nuxt-link>
        <nuxt-link :class="$style.pName" :to="`/product/${product.meta.pSlug}`">
          <p>{{ product.meta.pName }}</p>
        </nuxt-link>
        <div>
          <p>Price: </p>
          <p>{{ product.meta.pPrice }}</p>
        </div>
        <div>
          <p>Quantity:</p>
          <input
            :value="product.qty"
            :class="$style.input"
            type="number"
            :min="1"
            :max="1000"
            @change.prevent="onQuantityChangeHandler($event, product)"
          />
        </div>
        <div>
          <p>Amount:</p>
          <p>{{ (product.meta.pPrice * product.qty) | round }}</p>
        </div>
      </template>
    </div>
  </div>
</template>

<script>
import CloseOrDeleteButton from '~~/components/common/input/CloseOrDeleteButton.vue'
import round from '~~/mixins/round'
import { mapActions } from 'vuex'
import debounce from 'lodash.debounce'
export default {
  components: {
    CloseOrDeleteButton
  },
  mixins: [round],
  props: {
    productsFromCart: {
      type: Array,
      default: () => []
    }
  },
  methods: {
    ...mapActions({
      setProductQuantity: 'cart/setProductQuantity',
      removeProduct: 'cart/removeProduct'
    }),
    onRemoveClickHandler (product) {
      this.removeProduct(product.productId)
    },
    onQuantityChangeHandler: debounce(function onQuantityChangeHandler (e, product) {
      const qty = e.target.value
      this.setProductQuantity({ productId: product.productId, qty })
    }, 400)

  }
}
</script>

<style lang="scss" module>
.input {
  height: 20px;
}
    .remove {
      top: -15px;
      position: absolute;
      left: -30px;
      z-index: 1;
    }
.wrapper {
  display: flex;
  flex-wrap: wrap;
  flex-direction: column;
  .product {
    position: relative;
    margin: 1em;
    display: flex;
    flex-direction: row;

    * {
      margin-right: 10px;
    }
    .pName {
      width: 150px;
    }
  }

  p {
    max-width: 270px;
    height: 35px;
  }
}
.image {
  width: 75px;
  height: 75px;
  object-fit: cover;
}
</style>

2 actions


    ...mapActions({
      setProductQuantity: 'cart/setProductQuantity',
      removeProduct: 'cart/removeProduct'
    }),


    onQuantityChangeHandler: debounce(function onQuantityChangeHandler (e, product) {
      const qty = e.target.value
      this.setProductQuantity({ productId: product.productId, qty })
    }, 400)

e.target.value setProductQuantity. debounce, lodash


import debounce from 'lodash.debounce'

round CloseOrDeleteButton


:


CloseOrDeleteButton.vue
<template>
  <div class="svg-icon-block">
    <SvgClose :class="{'svg-icon-close': buttonType === 'close', 'svg-icon-delete': buttonType === 'delete'}" />
  </div>
</template>

<script>
import SvgClose from '~~/assets/svg/baseline-close-24px.svg?inline'
export default {
  components: {
    SvgClose
  },
  props: {
    buttonType: {
      type: String,
      default: 'close'
    }
  }
}
</script>

<style lang="scss" scoped>
.svg-icon-delete {
  // background: #ddd;
  fill: #ffb2a9;
  border: 3px solid #ffb2a9;
  transition: all .3s ease;
    width: 20px;
  height: 20px;
  &:hover {
    // background: #ddd;
    fill: #fb3f4c;
    border-color: #fb3f4c;
  }
    @media (--mobile) {
    width: 20px;
    height: 20px;
    border-width: 3px;
  }
}
.svg-icon-close {
  background: hsl(0, 0%, 60%);
  fill: #fff;
  border: 8px solid hsl(0, 0%, 60%);
    width: 20px;
  height: 20px;
  &:hover {
    background: hsl(0, 0%, 33%);
    fill: #fff;
    border-color :hsl(0, 0%, 33%);
  }
}
.svg-icon-block {
  display: block;
  cursor: pointer;
}
svg {
  border-radius: 100%;
  opacity: 0.7;
  line-height: 0;

  box-sizing: content-box;
  // // noselect
  // -webkit-user-select: none; /* webkit (safari, chrome) browsers */
  // -moz-user-select: none; /* mozilla browsers */
  // -khtml-user-select: none; /* webkit (konqueror) browsers */
  // -ms-user-select: none; /* IE10+ */

}
</style>

, тАФ svg


import SvgClose from '~~/assets/svg/baseline-close-24px.svg?inline'

?inline , svg , html ( svg).




. ( 2 , LocalStorage).




components\header\CartButton.vue
<template>
  <div :class="$style.block">
    <client-only>
      <a
        :href="'#'"
        :class="$style.cartButton"
        :disabled="!productsQuantity > 0 "
        @click.prevent="onClickHandler"
      >
        <div v-if="productsQuantity > 0" :class="$style.quantity">
          {{ productsQuantity }}
        </div>
        <CartSvg :class="$style.svg1" />
      </a>
    </client-only>
  </div>
</template>
<script>
import { mapState } from 'vuex'
import CartSvg from '~~/assets/svg/shopping-cart.svg?inline'
export default {
  components: {
    CartSvg
  },
  computed: {
    ...mapState({
      products: state => state.cart.products
    }),
    productsQuantity () {
      if (this.products) {
        return this.products.length
      } else return 0
    }
  },
  methods: {
    onClickHandler () {
      this.$modal.show('customer-cart')
    }
  }
}
</script>

<style lang="scss" module>
.block {
  position: relative;
}
.cartButton {
  position: relative;
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  width: 68px;
  height: 72px;
  text-align: center;
  transition: all 0.3s ease-in-out;
}
.svg1 {
  margin-right: 3px;
  width: 40px;
  fill: #000;
  // noselect
  -webkit-user-select: none; /* webkit (safari, chrome) browsers */
  -moz-user-select: none; /* mozilla browsers */
  -khtml-user-select: none; /* webkit (konqueror) browsers */
  -ms-user-select: none; /* IE10+ */
}
.quantity {
    position: absolute;
    right: 5px;
    top: 5px;
    border-radius: 50px;
    background-color: #fb3f4c;
    width: 20px;
    color: #fff;
    height: 20px;
    text-align: center;
    line-height: 20px;
    font-size: .8rem;
    font-weight: 600;
  // noselect
  -webkit-user-select: none; /* webkit (safari, chrome) browsers */
  -moz-user-select: none; /* mozilla browsers */
  -khtml-user-select: none; /* webkit (konqueror) browsers */
  -ms-user-select: none; /* IE10+ */
}
</style>

Header.


( -) ,


    ...mapState({
      products: state => state.cart.products
    }),

, .





  • : Github .
  • : .
  • 1
  • 2
  • 3

Nuxt.
, , .
-.
, . ( ) .



, , , .


!


All Articles