@ecomplus/storefront-components
Version:
Vue components for E-Com Plus Storefront
613 lines (570 loc) • 17.7 kB
JavaScript
import {
i19addToFavorites,
i19buy,
i19close,
i19days,
i19discountOf,
i19endsIn,
i19freeShippingFrom,
i19loadProductErrorMsg,
i19offer,
i19only,
i19outOfStock,
i19paymentOptions,
i19perUnit,
i19productionDeadline,
i19removeFromFavorites,
i19retry,
i19selectVariationMsg,
i19unavailable,
i19units,
i19unitsInStock,
i19workingDays
} from '@ecomplus/i18n'
import {
i18n,
randomObjectId as genRandomObjectId,
name as getName,
inStock as checkInStock,
onPromotion as checkOnPromotion,
price as getPrice,
img as getImg,
variationsGrids as getVariationsGrids,
specTextValue as getSpecTextValue,
specValueByText as getSpecValueByText,
formatMoney,
$ecomConfig
} from '@ecomplus/utils'
import { store, modules } from '@ecomplus/client'
import EcomSearch from '@ecomplus/search-engine'
import ecomCart from '@ecomplus/shopping-cart'
import { isMobile } from '@ecomplus/storefront-twbs'
import lozad from 'lozad'
import sortApps from './helpers/sort-apps'
import addIdleCallback from './helpers/add-idle-callback'
import scrollToElement from './helpers/scroll-to-element'
import { Portal } from '@linusborg/vue-simple-portal'
import ALink from '../ALink.vue'
import AAlert from '../AAlert.vue'
import APicture from '../APicture.vue'
import APrices from '../APrices.vue'
import AShare from '../AShare.vue'
import ProductVariations from '../ProductVariations.vue'
import KitProductVariations from '../KitProductVariations.vue'
import ProductGallery from '../ProductGallery.vue'
import QuantitySelector from '../QuantitySelector.vue'
import ShippingCalculator from '../ShippingCalculator.vue'
import PaymentOption from '../PaymentOption.vue'
import ecomPassport from '@ecomplus/passport-client'
import { toggleFavorite, checkFavorite } from './helpers/favorite-products'
const storefront = (typeof window === 'object' && window.storefront) || {}
const getContextBody = () => (storefront.context && storefront.context.body) || {}
const getContextId = () => getContextBody()._id
const sanitizeProductBody = body => {
const product = Object.assign({}, body)
delete product.body_html
delete product.body_text
delete product.specifications
delete product.inventory_records
delete product.price_change_records
return product
}
export default {
name: 'TheProduct',
components: {
Portal,
ALink,
AAlert,
APicture,
APrices,
AShare,
KitProductVariations,
ProductVariations,
ProductGallery,
QuantitySelector,
ShippingCalculator,
PaymentOption
},
props: {
productId: {
type: String,
default () {
return getContextId()
}
},
product: Object,
headingTag: {
type: String,
default: 'h1'
},
buyText: String,
galleryColClassName: {
type: String,
default: 'col-12 col-md-6'
},
hasPromotionTimer: Boolean,
hasStickyBuyButton: {
type: Boolean,
default: true
},
hasQuantitySelector: Boolean,
canAddToCart: {
type: Boolean,
default: true
},
hasBuyButton: {
type: Boolean,
default: true
},
lowQuantityToWarn: {
type: Number,
default: 12
},
maxVariationOptionsBtns: Number,
isQuickview: Boolean,
paymentAppsSort: {
type: Array,
default () {
return window.ecomPaymentApps || []
}
},
quoteLink: String,
isSSR: Boolean,
ecomPassport: {
type: Object,
default () {
return ecomPassport
}
},
accountUrl: {
type: String,
default: '/app/#/account/'
}
},
data () {
return {
body: {},
fixedPrice: null,
selectedVariationId: null,
currentGalleyImg: 1,
isOnCart: false,
qntToBuy: 1,
isStickyBuyVisible: false,
isFavorite: false,
hasClickedBuy: false,
hasLoadError: false,
paymentOptions: [],
customizations: [],
kitItems: [],
currentTimer: null
}
},
computed: {
i19addToFavorites: () => i18n(i19addToFavorites),
i19close: () => i18n(i19close),
i19days: () => i18n(i19days),
i19discountOf: () => i18n(i19discountOf),
i19endsIn: () => i18n(i19endsIn),
i19freeShippingFrom: () => i18n(i19freeShippingFrom),
i19loadProductErrorMsg: () => i18n(i19loadProductErrorMsg),
i19offer: () => i18n(i19offer),
i19only: () => i18n(i19only),
i19outOfStock: () => i18n(i19outOfStock),
i19paymentOptions: () => i18n(i19paymentOptions),
i19perUnit: () => i18n(i19perUnit),
i19productionDeadline: () => i18n(i19productionDeadline),
i19removeFromFavorites: () => i18n(i19removeFromFavorites),
i19retry: () => i18n(i19retry),
i19selectVariationMsg: () => i18n(i19selectVariationMsg),
i19unavailable: () => i18n(i19unavailable),
i19quoteProduct: () => 'Cotar produto',
i19units: () => i18n(i19units).toLowerCase(),
i19unitsInStock: () => i18n(i19unitsInStock),
i19workingDays: () => i18n(i19workingDays),
selectedVariation () {
return this.selectedVariationId
? this.body.variations.find(({ _id }) => _id === this.selectedVariationId)
: {}
},
name () {
return this.selectedVariation.name || getName(this.body)
},
isInStock () {
return checkInStock(this.body)
},
isWithoutPrice () {
return !getPrice(this.body)
},
isVariationInStock () {
return checkInStock(this.selectedVariationId ? this.selectedVariation : this.body)
},
isLogged () {
return ecomPassport.checkAuthorization()
},
thumbnail () {
return getImg(this.body, null, 'small')
},
productQuantity () {
if (this.selectedVariation.quantity) {
return this.selectedVariation.quantity
} else if (this.body.quantity) {
return this.body.quantity
}
},
isLowQuantity () {
return this.productQuantity > 0 && this.lowQuantityToWarn > 0 &&
this.productQuantity <= this.lowQuantityToWarn
},
strBuy () {
return this.buyText || i18n(i19buy)
},
discount () {
const { body } = this
const priceValue = this.fixedPrice || getPrice(body)
return checkOnPromotion(body) || (body.price > priceValue)
? Math.round(((body.base_price - priceValue) * 100) / body.base_price)
: 0
},
isOnSale () {
const { body } = this
return this.hasPromotionTimer &&
checkOnPromotion(body) &&
body.price_effective_date &&
body.price_effective_date.end &&
Date.now() < new Date(body.price_effective_date.end).getTime()
},
ghostProductForPrices () {
const prices = {}
;['price', 'base_price'].forEach(field => {
let price = this.selectedVariation[field] || this.body[field]
if (price !== undefined) {
this.customizations.forEach(customization => {
if (customization.add_to_price) {
price += this.getAdditionalPrice(customization.add_to_price)
}
})
}
prices[field] = price
})
const ghostProduct = { ...this.body }
if (this.selectedVariationId) {
Object.assign(ghostProduct, this.selectedVariation)
delete ghostProduct.variations
}
return {
...ghostProduct,
...prices
}
},
hasVariations () {
return this.body.variations && this.body.variations.length
},
isKit () {
return this.body.kit_composition && this.body.kit_composition.length
},
isKitWithVariations () {
return this.kitItems.some(item => item.variations && item.variations.length)
}
},
methods: {
getVariationsGrids,
getSpecValueByText,
setBody (data) {
this.body = {
...data,
body_html: '',
body_text: '',
inventory_records: []
}
this.$emit('update:product', data)
},
fetchProduct (isRetry = false) {
const { productId } = this
return store({
url: `/products/${productId}.json`,
axiosConfig: {
timeout: isRetry ? 2500 : 6000
}
})
.then(({ data }) => {
this.setBody(data)
if (getContextId() === productId) {
storefront.context.body = this.body
}
this.hasLoadError = false
})
.catch(err => {
console.error(err)
const { response } = err
if (!response || !(response.status >= 400 && response.status < 500)) {
if (!isRetry) {
this.fetchProduct(true)
} else {
this.setBody(getContextBody())
if (!this.body.name || !this.body.price || !this.body.pictures) {
this.hasLoadError = true
}
}
}
})
},
getAdditionalPrice ({ type, addition }) {
return type === 'fixed'
? addition
: getPrice(this.body) * addition / 100
},
formatAdditionalPrice (addToPrice) {
if (addToPrice && addToPrice.addition) {
return formatMoney(this.getAdditionalPrice(addToPrice))
}
return ''
},
setCustomizationOption (customization, text) {
const index = this.customizations.findIndex(({ _id }) => _id === customization._id)
if (text) {
if (index > -1) {
this.customizations[index].option = { text }
} else {
this.customizations.push({
_id: customization._id,
label: customization.label,
add_to_price: customization.add_to_price,
option: { text }
})
}
} else if (index > -1) {
this.customizations.splice(index, 1)
}
},
showVariationPicture (variation) {
if (variation.picture_id) {
const pictureIndex = this.body.pictures.findIndex(({ _id }) => {
return _id === variation.picture_id
})
this.currentGalleyImg = pictureIndex + 1
}
},
handleGridOption ({ gridId, gridIndex, optionText }) {
if (gridIndex === 0) {
const variation = this.body.variations.find(variation => {
return getSpecTextValue(variation, gridId) === optionText
})
if (variation) {
this.showVariationPicture(variation)
}
}
},
toggleFavorite () {
if (this.isLogged) {
this.isFavorite = toggleFavorite(this.body._id, this.ecomPassport)
}
},
buy () {
this.hasClickedBuy = true
const product = sanitizeProductBody(this.body)
let variationId
if (this.hasVariations) {
if (this.selectedVariationId) {
variationId = this.selectedVariationId
} else {
return
}
}
const customizations = [...this.customizations]
this.$emit('buy', { product, variationId, customizations })
if (this.canAddToCart) {
ecomCart.addProduct({ ...product, customizations }, variationId, this.qntToBuy)
}
this.isOnCart = true
},
buyOrScroll () {
if (this.hasVariations || this.isKit) {
scrollToElement(this.$refs.actions)
} else {
this.buy()
}
}
},
watch: {
selectedVariationId (variationId) {
if (variationId) {
if (this.hasClickedBuy) {
this.hasClickedBuy = false
}
const { pathname } = window.location
const searchParams = new URLSearchParams(window.location.search)
searchParams.set('variation_id', variationId)
window.history.pushState({
pathname,
params: searchParams.toString()
}, '', `${pathname}?${searchParams.toString()}`)
this.showVariationPicture(this.selectedVariation)
}
},
fixedPrice (price) {
if (price > 0 && !this.isQuickview) {
addIdleCallback(() => {
modules({
url: '/list_payments.json',
method: 'POST',
data: {
amount: {
total: price
},
items: [{
...sanitizeProductBody(this.body),
product_id: this.body._id
}],
currency_id: this.body.currency_id || $ecomConfig.get('currency'),
currency_symbol: this.body.currency_symbol || $ecomConfig.get('currency_symbol')
}
})
.then(({ data }) => {
if (Array.isArray(this.paymentAppsSort) && this.paymentAppsSort.length) {
sortApps(data.result, this.paymentAppsSort)
}
this.paymentOptions = data.result
.reduce((paymentOptions, appResult) => {
if (appResult.validated) {
paymentOptions.push({
app_id: appResult.app_id,
...appResult.response
})
}
return paymentOptions
}, [])
.sort((a, b) => {
return a.discount_option && a.discount_option.value &&
!(b.discount_option && b.discount_option.value)
? -1
: 1
})
})
.catch(console.error)
})
}
},
isKit: {
handler (isKit) {
if (isKit && !this.kitItems.length) {
const kitComposition = this.body.kit_composition
const ecomSearch = new EcomSearch()
ecomSearch
.setPageSize(kitComposition.length)
.setProductIds(kitComposition.map(({ _id }) => _id))
.fetch(true)
.then(() => {
ecomSearch.getItems().forEach(product => {
const { quantity } = kitComposition.find(({ _id }) => _id === product._id)
const addKitItem = variationId => {
const item = ecomCart.parseProduct(product, variationId, quantity)
if (quantity) {
item.min_quantity = item.max_quantity = quantity
} else {
item.quantity = 0
}
this.kitItems.push({
...item,
_id: genRandomObjectId()
})
}
addKitItem()
})
})
.catch(console.error)
}
},
immediate: true
}
},
created () {
const presetQntToBuy = () => {
this.qntToBuy = this.body.min_quantity || 1
}
if (this.product) {
this.body = this.product
if (this.isSSR) {
this.fetchProduct().then(presetQntToBuy)
} else {
presetQntToBuy()
}
} else {
this.fetchProduct().then(presetQntToBuy)
}
this.isFavorite = checkFavorite(this.body._id || this.productId, this.ecomPassport)
},
mounted () {
if (this.$refs.sticky && !this.isWithoutPrice) {
let isBodyPaddingSet = false
const setStickyBuyObserver = (isToVisible = true) => {
const $anchor = this.$refs[isToVisible ? 'sticky' : 'buy']
if (!$anchor) {
return
}
const $tmpDiv = document.createElement('div')
$anchor.parentNode.insertBefore($tmpDiv, $anchor)
if (isToVisible) {
$tmpDiv.style.position = 'absolute'
$tmpDiv.style.bottom = isMobile ? '-1600px' : '-1000px'
}
const obs = lozad($tmpDiv, {
rootMargin: '100px',
threshold: 0,
load: () => {
this.isStickyBuyVisible = isToVisible
if (isToVisible && !isBodyPaddingSet) {
this.$nextTick(() => {
const stickyHeight = this.$refs.sticky.offsetHeight
document.body.style.paddingBottom = `${(stickyHeight + 4)}px`
isBodyPaddingSet = true
})
}
$tmpDiv.remove()
setTimeout(() => {
const createObserver = function () {
setStickyBuyObserver(!isToVisible)
document.removeEventListener('scroll', createObserver)
}
document.addEventListener('scroll', createObserver)
}, 100)
}
})
obs.observe()
}
setStickyBuyObserver()
}
if (this.isOnSale) {
const promotionDate = new Date(this.body.price_effective_date.end)
const now = Date.now()
if (promotionDate.getTime() > now) {
let targetDate
const dayMs = 24 * 60 * 60 * 1000
const daysBetween = Math.floor((promotionDate.getTime() - now) / dayMs)
if (daysBetween > 2) {
targetDate = new Date()
targetDate.setHours(23, 59, 59, 999)
} else {
targetDate = promotionDate
}
const formatTime = (number) => number < 10 ? `0${number}` : number
const getRemainingTime = () => {
const distance = targetDate.getTime() - Date.now()
const days = Math.floor(distance / dayMs)
const hours = Math.floor((distance % dayMs) / (1000 * 60 * 60))
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60))
const seconds = Math.floor((distance % (1000 * 60)) / 1000)
return (days > 0 ? `${formatTime(days)}:` : '') +
`${formatTime(hours)}:${formatTime(minutes)}:${formatTime(seconds)}`
}
this.currentTimer = setInterval(() => {
this.$refs.timer.innerHTML = getRemainingTime()
}, 1000)
}
}
},
destroyed () {
if (this.currentTimer) {
clearInterval(this.currentTimer)
}
}
}