Skip to content

Commit

Permalink
fix(product): fix variant selector
Browse files Browse the repository at this point in the history
  • Loading branch information
thibaultrey committed Jan 15, 2025
1 parent 7650c5e commit 402e0a5
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 91 deletions.
15 changes: 4 additions & 11 deletions components/product/ProductDetail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,7 @@
</nuxt-link>
</slot>
</div>
<div
class="content__variants"
v-if="variants !== null && variants.length > 1"
>
<div class="content__variants" v-if="variants !== null && variants.length > 1">
<!-- @slot Variants content -->
<slot
name="variants"
Expand Down Expand Up @@ -188,14 +185,10 @@ onMounted(() => {
})
const changeVariant = (product: Product) => {
const item = new Product({
...product.data,
variants: props.product.variants
})
variant.value = item
if (item?.sku) {
variant.value = product
if (product?.sku) {
const route = useRoute()
router.push(localePath({ path: route.fullPath, query: { sku: item.sku } }))
router.push(localePath({ path: route.fullPath, query: { sku: product.sku } }))
}
}
Expand Down
206 changes: 127 additions & 79 deletions components/product/ProductVariantsSelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,41 @@
<icon name="error" class="text-error" />
{{ error }}
</div>
<div class="product-variant-selector__axis">
<label v-for="(axis, name) of variantAxes" :key="name" class="variant-axis">
<div class="variant-axis__name">
{{ name }}
</div>
<div v-else class="product-variant-selector__axis">
<label v-for="(values, name) of variantAttributes" :key="name" class="variant-axis">
<div class="variant-axis__name">{{ name }}</div>
<div class="variant-axis__values">
<template v-if="(axis as string)?.length < 6">
<div v-for="value in axis" :key="value">
<template v-if="values?.length < 6">
<div v-for="value in values" :key="value.value">
<button
type="button"
class="values__btn"
:class="{
'values__btn--selected':
(value as string)?.toLowerCase() ==
(selectValues[name] as string)?.toLowerCase(),
'values__btn--unselected':
(value as string)?.toLowerCase() !=
(selectValues[name] as string)?.toLowerCase()
'values__btn--selected': value.selected,
'values__btn--unselected': !value.selected
}"
@click="selectVariant(name as string, value)"
@click="onSelectVariant(name, value)"
:disabled="!value.variant"
>
{{ value }}
{{ value.value }}
</button>
</div>
</template>
<select
v-else
class="values__select"
v-model="selectValues[name]"
@change="changeVariant"
@change="
() => onSelectVariant(name, values.find((v) => v.value === selectValues[name])!)
"
>
<option v-for="value of axis" :key="value" :value="value">
{{ value }}
<option
v-for="value of values"
:key="value.value"
:value="value.value"
:disabled="!value.variant"
>
{{ value.value }}
</option>
</select>
</div>
Expand All @@ -46,92 +48,138 @@
</template>
<script lang="ts" setup>
import type { Product, VariantAttributes } from '#models'
import isEqual from '~/utils/IsEqual'
interface VariantAttributeOptions {
[key: string]: (string | number)[]
}
interface VariantAttributeSelector {
[key: string]: VariantAttributeSelectorItem[]
}
interface VariantAttributeSelectorItem {
key: string
value: string | number
selected: boolean
variant: Product | null
}
const props = defineProps({
product: {
type: Object as PropType<Product>,
required: true
}
})
const emit = defineEmits(['selectVariant'])
const { t } = useI18n()
const loading = ref(true)
const products = ref<Product[]>([])
const selectValues = ref({ ...props.product.variantAttributes })
const selectedVariant = ref(props.product)
const productService = useShopinvaderService('products')
const error = ref<string | null>(null)
const loading = ref(true)
const variantAxes = ref<VariantAttributes>({})
const selectValues = reactive({ ...props.product.variantAttributes })
const emit = defineEmits(['selectVariant'])
const findProduct = async (
variantAttributes: VariantAttributes
): Promise<{
axes: any
product: Product | null
}> => {
let axes = []
let product = null
let data = null
try {
loading.value = true
const productService = useShopinvaderService('products')
data = await productService.getVariantsAggregation(
props.product.urlKey || '',
variantAttributes
)
axes = data?.axes
product = data?.product
if (!product) {
/* if the current selection does not exists */
let haschange = false
for (const [key, value] of Object.entries(variantAttributes)) {
if (!axes?.[key]?.includes(value)) {
variantAttributes[key] = axes[key][0]
haschange = true
}
/** computeds */
const filterByKeys = (obj: any, keys: string[]) => {
return Object.fromEntries(Object.entries(obj).filter(([key]) => keys.includes(key)))
}
const variantOptions = computed((): VariantAttributeOptions => {
return products.value.reduce((acc, item) => {
for (const axe in item.variantAttributes) {
const value = item.variantAttributes[axe]
if (!acc?.[axe]) {
acc[axe] = []
}
if (!acc[axe].includes(value)) {
acc[axe].push(value)
}
if (haschange) {
data = await findProduct(variantAttributes)
axes = data?.axes
product = data?.product
}
return acc
}, {} as VariantAttributeOptions)
})
const variantAttributes = computed((): VariantAttributeSelector => {
const product: Product | null = selectedVariant.value || null
const attributes: VariantAttributeSelector = {}
if (!product) return attributes
const variantOptionsEntries = Object.entries(variantOptions.value)
const keys: string[] = []
/** Loop on attribute type (ex color, size...) */
for (const index in variantOptionsEntries) {
const [key, values] = variantOptionsEntries[index]
keys.push(key)
/** Loop on attribute values (ex blue, white, red...) */
for (const value of values) {
const searchedVariant = filterByKeys(product.variantAttributes, keys)
const filteredVariant: Product[] | null =
products.value.filter((v) =>
isEqual(filterByKeys(v.variantAttributes, keys), {
...searchedVariant,
[key]: value
})
) || []
const variant =
filteredVariant?.find((v) => v.variantAttributes[key] === value) ||
filteredVariant?.[0] ||
null
if (!attributes[key]) {
attributes[key] = []
}
attributes[key].push({
key,
value,
selected: searchedVariant[key] === value,
variant
})
}
}
return attributes
})
/** Methods */
const getVariants = async (product: Product): Promise<Product[]> => {
try {
loading.value = true
const { urlKey, variantCount } = product
return (await productService.getVariantsByURLKey(urlKey || '', variantCount)) || []
} catch (err) {
console.error('Error while fetching variant axes', err)
error.value = t('error.generic')
variantAxes.value = {}
return []
} finally {
loading.value = false
}
return {
axes,
product
}
}
const changeVariant = async (_value: any) => {
const { product, axes } = await findProduct(selectValues)
variantAxes.value = axes
if (product) {
emit('selectVariant', product)
}
}
const selectVariant = async (name: string, value: any) => {
selectValues[name] = value
const onSelectVariant = async (name: string | number, value: VariantAttributeSelectorItem) => {
selectValues.value = value.variant?.variantAttributes || {}
changeVariant(value)
}
try {
const productService = useShopinvaderService('products')
if (props.product && props.product?.urlKey) {
const { urlKey, variantAttributes } = props.product
const result = await productService.getVariantsAggregation(urlKey, variantAttributes)
variantAxes.value = result.axes
const changeVariant = async ({ variant }: VariantAttributeSelectorItem) => {
if (variant) {
variant.variants = products.value.filter((v) => v.id === variant.id)
selectedVariant.value = variant
setTimeout(() => {
emit('selectVariant', variant)
}, 100)
}
} catch (err) {
console.error('Error while fetching variant axes', err)
error.value = t('error.generic')
} finally {
loading.value = false
}
products.value = await getVariants(props.product)
/** Watcher */
watch(props.product, async (product, oldProduct) => {
if (product.urlKey !== oldProduct.urlKey) {
products.value = await getVariants(product)
}
if (selectedVariant.value.id !== product.id) {
selectedVariant.value = product
selectValues.value = { ...product.variantAttributes }
}
})
</script>
<style lang="scss">
.product-variant-selector {
Expand Down
6 changes: 5 additions & 1 deletion services/ProductService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,11 @@ export class ProductService extends BaseServiceElastic {
product: hits?.[0] || null
}
}

async getVariantsByURLKey(urlKey: string, size: number): Promise<Product[]> {
const body = esb.requestBodySearch().query(new TermQuery('url_key', urlKey)).size(size)
const result = await this.elasticSearch(body.toJSON())
return this.hits(result?.hits?.hits || [])
}
jsonToModel(json: any): Product {
const role = this.store()?.getCurrentRole
return new Product(json, role)
Expand Down

0 comments on commit 402e0a5

Please sign in to comment.