Skip to content

Commit 402e0a5

Browse files
committed
fix(product): fix variant selector
1 parent 7650c5e commit 402e0a5

File tree

3 files changed

+136
-91
lines changed

3 files changed

+136
-91
lines changed

components/product/ProductDetail.vue

+4-11
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,7 @@
7373
</nuxt-link>
7474
</slot>
7575
</div>
76-
<div
77-
class="content__variants"
78-
v-if="variants !== null && variants.length > 1"
79-
>
76+
<div class="content__variants" v-if="variants !== null && variants.length > 1">
8077
<!-- @slot Variants content -->
8178
<slot
8279
name="variants"
@@ -188,14 +185,10 @@ onMounted(() => {
188185
})
189186
190187
const changeVariant = (product: Product) => {
191-
const item = new Product({
192-
...product.data,
193-
variants: props.product.variants
194-
})
195-
variant.value = item
196-
if (item?.sku) {
188+
variant.value = product
189+
if (product?.sku) {
197190
const route = useRoute()
198-
router.push(localePath({ path: route.fullPath, query: { sku: item.sku } }))
191+
router.push(localePath({ path: route.fullPath, query: { sku: product.sku } }))
199192
}
200193
}
201194

components/product/ProductVariantsSelector.vue

+127-79
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,41 @@
44
<icon name="error" class="text-error" />
55
{{ error }}
66
</div>
7-
<div class="product-variant-selector__axis">
8-
<label v-for="(axis, name) of variantAxes" :key="name" class="variant-axis">
9-
<div class="variant-axis__name">
10-
{{ name }}
11-
</div>
7+
<div v-else class="product-variant-selector__axis">
8+
<label v-for="(values, name) of variantAttributes" :key="name" class="variant-axis">
9+
<div class="variant-axis__name">{{ name }}</div>
1210
<div class="variant-axis__values">
13-
<template v-if="(axis as string)?.length < 6">
14-
<div v-for="value in axis" :key="value">
11+
<template v-if="values?.length < 6">
12+
<div v-for="value in values" :key="value.value">
1513
<button
1614
type="button"
1715
class="values__btn"
1816
:class="{
19-
'values__btn--selected':
20-
(value as string)?.toLowerCase() ==
21-
(selectValues[name] as string)?.toLowerCase(),
22-
'values__btn--unselected':
23-
(value as string)?.toLowerCase() !=
24-
(selectValues[name] as string)?.toLowerCase()
17+
'values__btn--selected': value.selected,
18+
'values__btn--unselected': !value.selected
2519
}"
26-
@click="selectVariant(name as string, value)"
20+
@click="onSelectVariant(name, value)"
21+
:disabled="!value.variant"
2722
>
28-
{{ value }}
23+
{{ value.value }}
2924
</button>
3025
</div>
3126
</template>
3227
<select
3328
v-else
3429
class="values__select"
3530
v-model="selectValues[name]"
36-
@change="changeVariant"
31+
@change="
32+
() => onSelectVariant(name, values.find((v) => v.value === selectValues[name])!)
33+
"
3734
>
38-
<option v-for="value of axis" :key="value" :value="value">
39-
{{ value }}
35+
<option
36+
v-for="value of values"
37+
:key="value.value"
38+
:value="value.value"
39+
:disabled="!value.variant"
40+
>
41+
{{ value.value }}
4042
</option>
4143
</select>
4244
</div>
@@ -46,92 +48,138 @@
4648
</template>
4749
<script lang="ts" setup>
4850
import type { Product, VariantAttributes } from '#models'
49-
51+
import isEqual from '~/utils/IsEqual'
52+
interface VariantAttributeOptions {
53+
[key: string]: (string | number)[]
54+
}
55+
interface VariantAttributeSelector {
56+
[key: string]: VariantAttributeSelectorItem[]
57+
}
58+
interface VariantAttributeSelectorItem {
59+
key: string
60+
value: string | number
61+
selected: boolean
62+
variant: Product | null
63+
}
5064
const props = defineProps({
5165
product: {
5266
type: Object as PropType<Product>,
5367
required: true
5468
}
5569
})
70+
const emit = defineEmits(['selectVariant'])
71+
5672
const { t } = useI18n()
57-
const loading = ref(true)
73+
const products = ref<Product[]>([])
74+
const selectValues = ref({ ...props.product.variantAttributes })
75+
const selectedVariant = ref(props.product)
76+
const productService = useShopinvaderService('products')
5877
const error = ref<string | null>(null)
78+
const loading = ref(true)
5979
60-
const variantAxes = ref<VariantAttributes>({})
61-
const selectValues = reactive({ ...props.product.variantAttributes })
62-
const emit = defineEmits(['selectVariant'])
63-
const findProduct = async (
64-
variantAttributes: VariantAttributes
65-
): Promise<{
66-
axes: any
67-
product: Product | null
68-
}> => {
69-
let axes = []
70-
let product = null
71-
let data = null
72-
try {
73-
loading.value = true
74-
const productService = useShopinvaderService('products')
75-
data = await productService.getVariantsAggregation(
76-
props.product.urlKey || '',
77-
variantAttributes
78-
)
79-
axes = data?.axes
80-
product = data?.product
81-
if (!product) {
82-
/* if the current selection does not exists */
83-
let haschange = false
84-
for (const [key, value] of Object.entries(variantAttributes)) {
85-
if (!axes?.[key]?.includes(value)) {
86-
variantAttributes[key] = axes[key][0]
87-
haschange = true
88-
}
80+
/** computeds */
81+
const filterByKeys = (obj: any, keys: string[]) => {
82+
return Object.fromEntries(Object.entries(obj).filter(([key]) => keys.includes(key)))
83+
}
84+
85+
const variantOptions = computed((): VariantAttributeOptions => {
86+
return products.value.reduce((acc, item) => {
87+
for (const axe in item.variantAttributes) {
88+
const value = item.variantAttributes[axe]
89+
if (!acc?.[axe]) {
90+
acc[axe] = []
91+
}
92+
if (!acc[axe].includes(value)) {
93+
acc[axe].push(value)
8994
}
90-
if (haschange) {
91-
data = await findProduct(variantAttributes)
92-
axes = data?.axes
93-
product = data?.product
95+
}
96+
return acc
97+
}, {} as VariantAttributeOptions)
98+
})
99+
100+
const variantAttributes = computed((): VariantAttributeSelector => {
101+
const product: Product | null = selectedVariant.value || null
102+
103+
const attributes: VariantAttributeSelector = {}
104+
if (!product) return attributes
105+
const variantOptionsEntries = Object.entries(variantOptions.value)
106+
const keys: string[] = []
107+
/** Loop on attribute type (ex color, size...) */
108+
for (const index in variantOptionsEntries) {
109+
const [key, values] = variantOptionsEntries[index]
110+
keys.push(key)
111+
/** Loop on attribute values (ex blue, white, red...) */
112+
for (const value of values) {
113+
const searchedVariant = filterByKeys(product.variantAttributes, keys)
114+
const filteredVariant: Product[] | null =
115+
products.value.filter((v) =>
116+
isEqual(filterByKeys(v.variantAttributes, keys), {
117+
...searchedVariant,
118+
[key]: value
119+
})
120+
) || []
121+
const variant =
122+
filteredVariant?.find((v) => v.variantAttributes[key] === value) ||
123+
filteredVariant?.[0] ||
124+
null
125+
126+
if (!attributes[key]) {
127+
attributes[key] = []
94128
}
129+
attributes[key].push({
130+
key,
131+
value,
132+
selected: searchedVariant[key] === value,
133+
variant
134+
})
95135
}
136+
}
137+
return attributes
138+
})
139+
140+
/** Methods */
141+
142+
const getVariants = async (product: Product): Promise<Product[]> => {
143+
try {
144+
loading.value = true
145+
const { urlKey, variantCount } = product
146+
return (await productService.getVariantsByURLKey(urlKey || '', variantCount)) || []
96147
} catch (err) {
97148
console.error('Error while fetching variant axes', err)
98149
error.value = t('error.generic')
99-
variantAxes.value = {}
150+
return []
100151
} finally {
101152
loading.value = false
102153
}
103-
return {
104-
axes,
105-
product
106-
}
107154
}
108155
109-
const changeVariant = async (_value: any) => {
110-
const { product, axes } = await findProduct(selectValues)
111-
variantAxes.value = axes
112-
if (product) {
113-
emit('selectVariant', product)
114-
}
115-
}
116-
117-
const selectVariant = async (name: string, value: any) => {
118-
selectValues[name] = value
156+
const onSelectVariant = async (name: string | number, value: VariantAttributeSelectorItem) => {
157+
selectValues.value = value.variant?.variantAttributes || {}
119158
changeVariant(value)
120159
}
121160
122-
try {
123-
const productService = useShopinvaderService('products')
124-
if (props.product && props.product?.urlKey) {
125-
const { urlKey, variantAttributes } = props.product
126-
const result = await productService.getVariantsAggregation(urlKey, variantAttributes)
127-
variantAxes.value = result.axes
161+
const changeVariant = async ({ variant }: VariantAttributeSelectorItem) => {
162+
if (variant) {
163+
variant.variants = products.value.filter((v) => v.id === variant.id)
164+
selectedVariant.value = variant
165+
setTimeout(() => {
166+
emit('selectVariant', variant)
167+
}, 100)
128168
}
129-
} catch (err) {
130-
console.error('Error while fetching variant axes', err)
131-
error.value = t('error.generic')
132-
} finally {
133-
loading.value = false
134169
}
170+
171+
products.value = await getVariants(props.product)
172+
173+
/** Watcher */
174+
watch(props.product, async (product, oldProduct) => {
175+
if (product.urlKey !== oldProduct.urlKey) {
176+
products.value = await getVariants(product)
177+
}
178+
if (selectedVariant.value.id !== product.id) {
179+
selectedVariant.value = product
180+
selectValues.value = { ...product.variantAttributes }
181+
}
182+
})
135183
</script>
136184
<style lang="scss">
137185
.product-variant-selector {

services/ProductService.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,11 @@ export class ProductService extends BaseServiceElastic {
151151
product: hits?.[0] || null
152152
}
153153
}
154-
154+
async getVariantsByURLKey(urlKey: string, size: number): Promise<Product[]> {
155+
const body = esb.requestBodySearch().query(new TermQuery('url_key', urlKey)).size(size)
156+
const result = await this.elasticSearch(body.toJSON())
157+
return this.hits(result?.hits?.hits || [])
158+
}
155159
jsonToModel(json: any): Product {
156160
const role = this.store()?.getCurrentRole
157161
return new Product(json, role)

0 commit comments

Comments
 (0)