Skip to content

Commit 860dbd9

Browse files
committed
chore(template): update product page
Signed-off-by: Frederik Bußmann <[email protected]>
1 parent a3619f5 commit 860dbd9

File tree

8 files changed

+107
-87
lines changed

8 files changed

+107
-87
lines changed

template/app/components/cart/Choose.vue

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,9 @@ const props = defineProps<{
55
product: ProductFieldsFragment
66
}>()
77
8-
const { data, error } = await useStorefrontData(`product-${locale.value}-${handle.value}`, `#graphql
9-
query FetchProduct($handle: String, $language: LanguageCode, $country: CountryCode)
10-
@inContext(language: $language, country: $country) {
11-
product(handle: $handle) {
12-
...ProductFields
13-
}
14-
productRecommendations(productHandle: $handle) {
15-
...ProductFields
16-
}
17-
}
18-
${IMAGE_FRAGMENT}
19-
${PRICE_FRAGMENT}
20-
${PRODUCT_FRAGMENT}
21-
`, {
22-
variables: computed(() => productInputSchema.parse({
23-
handle: handle.value,
24-
language: language.value,
25-
country: country.value,
26-
})),
27-
})
8+
const selectedVariant = ref(props.product?.selectedOrFirstAvailableVariant)
289
2910
const open = ref(false)
30-
31-
const variant = ref(props.product.selectedOrFirstAvailableVariant!)
3211
</script>
3312

3413
<template>
@@ -66,15 +45,18 @@ const variant = ref(props.product.selectedOrFirstAvailableVariant!)
6645
/>
6746

6847
<template #body>
69-
<div class="lg:grid lg:grid-cols-12">
48+
<div
49+
v-if="product && selectedVariant"
50+
class="lg:grid lg:grid-cols-12"
51+
>
7052
<ProductGallery
71-
:product="props.product"
72-
:selected-variant="variant ?? undefined"
53+
:selected-variant="selectedVariant"
54+
:product="product"
7355
class="lg:col-span-6"
7456
/>
7557

7658
<ProductConfigurator
77-
v-model="variant"
59+
v-model="selectedVariant"
7860
class="lg:col-start-8 lg:col-span-5"
7961
/>
8062
</div>

template/app/components/collection/Products.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const { data: collection, status } = await useStorefrontData(key, `#graphql
2626
@inContext(language: $language, country: $country) {
2727
collection(handle: $handle) {
2828
...CollectionFields
29+
2930
products(
3031
after: $after,
3132
before: $before,

template/app/components/product/Configurator.vue

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,68 @@
11
<script setup lang="ts">
2-
import type { ProductFieldsFragment, ProductVariantFieldsFragment } from '#shopify/storefront'
3-
4-
const props = defineProps<{
5-
product: ProductFieldsFragment
6-
}>()
2+
import type { ProductVariantFieldsFragment } from '#shopify/storefront'
3+
import type { FormSubmitEvent } from '#ui/types'
74
85
const selectedVariant = defineModel<ProductVariantFieldsFragment>()
96
7+
const { language, country } = useLocalization()
8+
const { locale } = useI18n()
9+
const { add } = useCart()
10+
11+
const handle = computed(() => selectedVariant.value?.product.handle)
12+
1013
const state = reactive({
11-
selectedOptions: props.product.selectedOrFirstAvailableVariant?.selectedOptions || [],
1214
quantity: 1,
15+
selectedOptions: selectedVariant.value?.selectedOptions,
16+
})
17+
18+
const { data: product } = await useStorefrontData(`product-options-${locale.value}-${handle.value}`, `#graphql
19+
query FetchProductOptions($handle: String, $language: LanguageCode, $country: CountryCode, $selectedOptions: [SelectedOptionInput!])
20+
@inContext(language: $language, country: $country) {
21+
product(handle: $handle) {
22+
options(first: 250) {
23+
...ProductOptionFields
24+
}
25+
26+
selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions) {
27+
...ProductVariantFields
28+
}
29+
}
30+
}
31+
${IMAGE_FRAGMENT}
32+
${PRICE_FRAGMENT}
33+
${PRODUCT_VARIANT_FRAGMENT}
34+
${PRODUCT_OPTION_FRAGMENT}
35+
`, {
36+
variables: computed(() => productInputSchema.parse({
37+
handle: handle.value,
38+
language: language.value,
39+
country: country.value,
40+
selectedOptions: state.selectedOptions,
41+
})),
42+
transform: value => value.product,
43+
watch: [() => state.selectedOptions],
44+
cache: 'long',
45+
lazy: true,
1346
})
1447
15-
const onChange = () => selectedVariant.value = flattenConnection(props.product.variants)
16-
.find(variant => variant.selectedOptions.every(option =>
17-
state.selectedOptions.every(selectedOption =>
18-
selectedOption.name === option.name && selectedOption.value === option.value,
19-
),
20-
))
48+
const loading = ref(false)
49+
50+
watch(() => product.value?.selectedOrFirstAvailableVariant, variant => selectedVariant.value = variant ?? undefined)
51+
52+
const onSubmit = async (event: FormSubmitEvent<typeof state>) => {
53+
if (!selectedVariant.value) return
54+
55+
loading.value = true
56+
57+
await add(selectedVariant.value.id, event.data.quantity).then(() => loading.value = false)
58+
}
2159
</script>
2260

2361
<template>
2462
<div>
2563
<div class="flex-col lg:flex pb-6 lg:pb-8">
2664
<h1 class="text-4xl lg:text-5xl font-extrabold text-gray-900 mb-4">
27-
{{ props.product?.title }}
65+
{{ selectedVariant?.product?.title }}
2866
</h1>
2967

3068
<ProductPrice
@@ -36,13 +74,15 @@ const onChange = () => selectedVariant.value = flattenConnection(props.product.v
3674

3775
<USeparator class="mb-6 lg:mb-8" />
3876

39-
<UForm>
77+
<UForm
78+
v-if="product"
79+
:state="state"
80+
@submit="onSubmit"
81+
>
4082
<ProductOptionGroup
41-
v-if="product?.options"
4283
v-model="state.selectedOptions"
4384
:options="product.options"
4485
class="order-1 lg:order-2 mb-6 lg:mb-8"
45-
@update:model-value="onChange"
4686
/>
4787

4888
<div class="flex justify-between items-center">
@@ -65,8 +105,9 @@ const onChange = () => selectedVariant.value = flattenConnection(props.product.v
65105
type="submit"
66106
size="xl"
67107
variant="subtle"
68-
:trailing-icon="'i-lucide-shopping-bag'"
69-
:ui="{ trailingIcon: 'size-5' }"
108+
:disabled="!selectedVariant || loading"
109+
:trailing-icon="loading ? 'i-lucide-loader-circle' : 'i-lucide-shopping-bag'"
110+
:ui="{ trailingIcon: loading ? 'animate-spin size-5' : 'size-5' }"
70111
:label="$t('product.add')"
71112
/>
72113
</div>

template/app/components/product/option/Group.vue

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,35 +25,26 @@ const isColorSwatchOption = (option?: typeof props.options[number]) =>
2525
2626
const isImageSwatchOption = (option?: typeof props.options[number]) =>
2727
!!option?.optionValues?.every(value => value.swatch?.image?.previewImage?.url)
28+
29+
const getFilterComponent = (option: typeof props.options[number]) => {
30+
if (isColorSwatchOption(option)) return resolveComponent('ProductOptionSwatchColor')
31+
if (isImageSwatchOption(option)) return resolveComponent('ProductOptionSwatchImage')
32+
33+
return resolveComponent('ProductOptionSwatchText')
34+
}
2835
</script>
2936

3037
<template>
31-
<div
32-
v-for="option in props.options"
33-
:key="option.id"
34-
>
38+
<div>
3539
<UFormField
36-
v-if="option.optionValues.length > 1"
40+
v-for="option in props.options.filter(option => option.optionValues.length > 1)"
41+
:key="option.id"
3742
:label="option.name"
3843
:name="option.name"
3944
class="mb-6 lg:mb-8"
4045
>
41-
<ProductOptionSwatchColor
42-
v-if="isColorSwatchOption(option)"
43-
v-model="state[option.name]"
44-
:option="option"
45-
@update:model-value="onChange"
46-
/>
47-
48-
<ProductOptionSwatchImage
49-
v-else-if="isImageSwatchOption(option)"
50-
v-model="state[option.name]"
51-
:option="option"
52-
@update:model-value="onChange"
53-
/>
54-
55-
<ProductOptionSwatchText
56-
v-else
46+
<component
47+
:is="getFilterComponent(option)"
5748
v-model="state[option.name]"
5849
:option="option"
5950
@update:model-value="onChange"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const useProduct = () => {
2+
}

template/app/pages/collection/[handle].vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ if (!collection.value || error.value) {
4040
fatal: true,
4141
})
4242
}
43+
44+
useSeoMeta({
45+
title: `${collection.value?.title} | Nuxt Shopify Demo Store`,
46+
description: collection.value?.description,
47+
})
4348
</script>
4449

4550
<template>

template/app/pages/product/[handle].vue

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
<script setup lang="ts">
2-
import type { SelectedOption } from '#shopify/storefront'
3-
42
definePageMeta({
53
validate: route => typeof route.params.handle === 'string',
64
})
75
86
const { language, country } = useLocalization()
97
const localePath = useLocalePath()
108
const { locale } = useI18n()
9+
const router = useRouter()
1110
const route = useRoute()
1211
1312
const handle = computed(() => route.params.handle as string)
@@ -18,6 +17,7 @@ const { data, error } = await useStorefrontData(`product-${locale.value}-${handl
1817
product(handle: $handle) {
1918
...ProductFields
2019
}
20+
2121
productRecommendations(productHandle: $handle) {
2222
...ProductFields
2323
}
@@ -31,6 +31,7 @@ const { data, error } = await useStorefrontData(`product-${locale.value}-${handl
3131
language: language.value,
3232
country: country.value,
3333
})),
34+
cache: 'long',
3435
})
3536
3637
if (!data.value?.product || error.value) {
@@ -44,9 +45,7 @@ if (!data.value?.product || error.value) {
4445
const product = computed(() => data.value?.product)
4546
const recommendations = computed(() => data.value?.productRecommendations)
4647
47-
const selectedOptions = ref<SelectedOption[]>([])
48-
49-
const selectedVariant = computed(() => flattenConnection(data.value?.product?.variants)
48+
const selectedVariant = ref(flattenConnection(data.value?.product?.variants)
5049
.find(variant => variant.id.replace('gid://shopify/ProductVariant/', '') === route.query.variantId)
5150
?? product.value?.selectedOrFirstAvailableVariant)
5251
@@ -55,7 +54,14 @@ useSeoMeta({
5554
description: product.value?.description,
5655
})
5756
58-
watch(selectedOptions, value => console.log(value))
57+
watch(selectedVariant, (variant) => {
58+
if (variant) {
59+
router.push({
60+
path: localePath(`/product/${variant.product.handle}`),
61+
query: { variantId: variant.id.replace('gid://shopify/ProductVariant/', '') },
62+
})
63+
}
64+
})
5965
</script>
6066

6167
<template>
@@ -69,21 +75,21 @@ watch(selectedOptions, value => console.log(value))
6975
/>
7076

7177
<div
72-
v-if="product"
78+
v-if="product && selectedVariant"
7379
class="mb-12 lg:grid lg:grid-cols-12 lg:mb-16"
7480
>
7581
<ProductGallery
7682
ref="carousel"
83+
:selected-variant="selectedVariant"
7784
:product="product"
78-
:selected-variant="selectedVariant ?? undefined"
7985
class="lg:col-span-6"
8086
thumbnails
8187
/>
8288

8389
<div class="lg:col-span-4 lg:col-start-8">
8490
<div class="lg:sticky lg:top-[calc(var(--ui-header-height)+3rem)]">
8591
<ProductConfigurator
86-
:handle="handle"
92+
v-model="selectedVariant"
8793
class="mb-12 lg:mb-16"
8894
/>
8995

template/graphql/fragments/product.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ export const PRODUCT_VARIANT_FRAGMENT = `#graphql
77
name
88
value
99
}
10-
sku
1110
price {
1211
...PriceFields
1312
}
@@ -16,6 +15,7 @@ export const PRODUCT_VARIANT_FRAGMENT = `#graphql
1615
}
1716
product {
1817
handle
18+
title
1919
}
2020
}
2121
`
@@ -63,6 +63,7 @@ export const PRODUCT_FRAGMENT = `#graphql
6363
handle
6464
title
6565
description
66+
vendor
6667
featuredImage {
6768
...ImageFields
6869
}
@@ -73,9 +74,6 @@ export const PRODUCT_FRAGMENT = `#graphql
7374
}
7475
}
7576
}
76-
options(first: 250) {
77-
...ProductOptionFields
78-
}
7977
priceRange {
8078
minVariantPrice {
8179
...PriceFields
@@ -84,19 +82,13 @@ export const PRODUCT_FRAGMENT = `#graphql
8482
...PriceFields
8583
}
8684
}
87-
productType
88-
tags
89-
descriptionHtml
90-
availableForSale
91-
vendor
92-
selectedOrFirstAvailableVariant {
93-
...ProductVariantFields
94-
}
9585
variants(first: 250) {
9686
...ProductVariantConnectionFields
9787
}
88+
selectedOrFirstAvailableVariant {
89+
...ProductVariantFields
90+
}
9891
}
99-
${PRODUCT_OPTION_FRAGMENT}
10092
${PRODUCT_VARIANT_CONNECTION_FRAGMENT}
10193
`
10294

0 commit comments

Comments
 (0)