Add Shopify related products

This commit is contained in:
cond0r 2021-06-10 13:18:57 +03:00
parent 74dda1aa55
commit 396a708e23
11 changed files with 379 additions and 228 deletions

View File

@ -61,6 +61,7 @@ const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
<ProductSidebar product={product} className={s.sidebar} /> <ProductSidebar product={product} className={s.sidebar} />
</div> </div>
<hr className="mt-7 border-accent-2" /> <hr className="mt-7 border-accent-2" />
{relatedProducts.length && (
<section className="py-12 px-6 mb-10"> <section className="py-12 px-6 mb-10">
<Text variant="sectionHeading">Related Products</Text> <Text variant="sectionHeading">Related Products</Text>
<div className={s.relatedProductsGrid}> <div className={s.relatedProductsGrid}>
@ -84,6 +85,7 @@ const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
))} ))}
</div> </div>
</section> </section>
)}
</Container> </Container>
<NextSeo <NextSeo
title={product.name} title={product.name}

View File

@ -7,10 +7,11 @@ import type {
GetAllProductPathsOperation, GetAllProductPathsOperation,
GetAllProductsOperation, GetAllProductsOperation,
GetProductOperation, GetProductOperation,
GetRelatedProductsOperation,
} from '../types/product' } from '../types/product'
import type { APIProvider, CommerceAPI } from '.' import type { APIProvider, CommerceAPI } from '.'
const noop = () => { const noop = (_props?: any) => {
throw new Error('Not implemented') throw new Error('Not implemented')
} }
@ -22,6 +23,7 @@ export const OPERATIONS = [
'getCustomerWishlist', 'getCustomerWishlist',
'getAllProductPaths', 'getAllProductPaths',
'getAllProducts', 'getAllProducts',
'getRelatedProducts',
'getProduct', 'getProduct',
] as const ] as const
@ -139,6 +141,22 @@ export type Operations<P extends APIProvider> = {
): Promise<T['data']> ): Promise<T['data']>
} }
getRelatedProducts: {
<T extends GetRelatedProductsOperation>(opts: {
variables: T['variables']
config?: P['config']
preview?: boolean
}): Promise<T['data']>
<T extends GetAllProductsOperation>(
opts: {
variables: T['variables']
config?: P['config']
preview?: boolean
} & OperationOptions
): Promise<T['data']>
}
getProduct: { getProduct: {
<T extends GetProductOperation>(opts: { <T extends GetProductOperation>(opts: {
variables: T['variables'] variables: T['variables']

View File

@ -93,6 +93,16 @@ export type GetAllProductsOperation<T extends ProductTypes = ProductTypes> = {
} }
} }
export type GetRelatedProductsOperation<
T extends ProductTypes = ProductTypes
> = {
data: { products: T['product'][] }
variables: {
productId: string
first?: number
}
}
export type GetProductOperation<T extends ProductTypes = ProductTypes> = { export type GetProductOperation<T extends ProductTypes = ProductTypes> = {
data: { product?: T['product'] } data: { product?: T['product'] }
variables: { path: string; slug?: never } | { path?: never; slug: string } variables: { path: string; slug?: never } | { path?: never; slug: string }

View File

@ -0,0 +1,74 @@
import type {
OperationContext,
OperationOptions,
} from '@commerce/api/operations'
import { GetRelatedProductsOperation } from '../../types/product'
import {
GetRelatedProductsQuery,
GetRelatedProductsQueryVariables,
Product as ShopifyProduct,
} from '../../schema'
import type { ShopifyConfig, Provider } from '..'
import getRelatedProductsQuery from '../../utils/queries/get-related-products-query'
import { normalizeProduct } from '../../utils'
export default function getRelatedProductsOperation({
commerce,
}: OperationContext<Provider>) {
async function getRelatedProductsOperation<
T extends GetRelatedProductsOperation
>(opts: {
variables: T['variables']
config?: Partial<ShopifyConfig>
preview?: boolean
}): Promise<T['data']>
async function getRelatedProductsOperation<
T extends GetRelatedProductsOperation
>(
opts: {
variables: T['variables']
config?: Partial<ShopifyConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
async function getRelatedProductsOperation<
T extends GetRelatedProductsOperation
>({
query = getRelatedProductsQuery,
variables,
config,
}: {
query?: string
variables: T['variables']
config?: Partial<ShopifyConfig>
preview?: boolean
}): Promise<T['data']> {
const { fetch, locale } = commerce.getConfig(config)
const { data } = await fetch<
GetRelatedProductsQuery,
GetRelatedProductsQueryVariables
>(
query,
{ variables },
{
...(locale && {
headers: {
'Accept-Language': locale,
},
}),
}
)
return {
products:
data.productRecommendations
?.map((product) => normalizeProduct(product as ShopifyProduct))
.splice(0, variables?.first || 4) ?? [],
}
}
return getRelatedProductsOperation
}

View File

@ -2,6 +2,7 @@ export { default as getAllPages } from './get-all-pages'
export { default as getPage } from './get-page' export { default as getPage } from './get-page'
export { default as getAllProducts } from './get-all-products' export { default as getAllProducts } from './get-all-products'
export { default as getAllProductPaths } from './get-all-product-paths' export { default as getAllProductPaths } from './get-all-product-paths'
export { default as getRelatedProducts } from './get-related-products'
export { default as getProduct } from './get-product' export { default as getProduct } from './get-product'
export { default as getSiteInfo } from './get-site-info' export { default as getSiteInfo } from './get-site-info'
export { default as login } from './login' export { default as login } from './login'

View File

@ -2635,7 +2635,7 @@ export type FulfillmentTrackingInfo = {
/** Represents information about the metafields associated to the specified resource. */ /** Represents information about the metafields associated to the specified resource. */
export type HasMetafields = { export type HasMetafields = {
/** The metafield associated with the resource. */ /** Returns a metafield found by namespace and key. */
metafield?: Maybe<Metafield> metafield?: Maybe<Metafield>
/** A paginated list of metafields associated with the resource. */ /** A paginated list of metafields associated with the resource. */
metafields: MetafieldConnection metafields: MetafieldConnection
@ -3908,7 +3908,7 @@ export type Product = Node &
images: ImageConnection images: ImageConnection
/** The media associated with the product. */ /** The media associated with the product. */
media: MediaConnection media: MediaConnection
/** The metafield associated with the resource. */ /** Returns a metafield found by namespace and key. */
metafield?: Maybe<Metafield> metafield?: Maybe<Metafield>
/** A paginated list of metafields associated with the resource. */ /** A paginated list of metafields associated with the resource. */
metafields: MetafieldConnection metafields: MetafieldConnection
@ -4235,7 +4235,7 @@ export type ProductVariant = Node &
id: Scalars['ID'] id: Scalars['ID']
/** Image associated with the product variant. This field falls back to the product image if no image is available. */ /** Image associated with the product variant. This field falls back to the product image if no image is available. */
image?: Maybe<Image> image?: Maybe<Image>
/** The metafield associated with the resource. */ /** Returns a metafield found by namespace and key. */
metafield?: Maybe<Metafield> metafield?: Maybe<Metafield>
/** A paginated list of metafields associated with the resource. */ /** A paginated list of metafields associated with the resource. */
metafields: MetafieldConnection metafields: MetafieldConnection
@ -5265,17 +5265,10 @@ export type GetAllProductPathsQuery = { __typename?: 'QueryRoot' } & {
} }
} }
export type ProductConnectionFragment = { __typename?: 'ProductConnection' } & { export type ListProductDetailsFragment = { __typename?: 'Product' } & Pick<
pageInfo: { __typename?: 'PageInfo' } & Pick<
PageInfo,
'hasNextPage' | 'hasPreviousPage'
>
edges: Array<
{ __typename?: 'ProductEdge' } & {
node: { __typename?: 'Product' } & Pick<
Product, Product,
'id' | 'title' | 'vendor' | 'handle' 'id' | 'title' | 'vendor' | 'handle'
> & { > & {
priceRange: { __typename?: 'ProductPriceRange' } & { priceRange: { __typename?: 'ProductPriceRange' } & {
minVariantPrice: { __typename?: 'MoneyV2' } & Pick< minVariantPrice: { __typename?: 'MoneyV2' } & Pick<
MoneyV2, MoneyV2,
@ -5297,6 +5290,15 @@ export type ProductConnectionFragment = { __typename?: 'ProductConnection' } & {
> >
} }
} }
export type ProductConnectionFragment = { __typename?: 'ProductConnection' } & {
pageInfo: { __typename?: 'PageInfo' } & Pick<
PageInfo,
'hasNextPage' | 'hasPreviousPage'
>
edges: Array<
{ __typename?: 'ProductEdge' } & {
node: { __typename?: 'Product' } & ListProductDetailsFragment
} }
> >
} }
@ -5344,6 +5346,12 @@ export type CheckoutDetailsFragment = { __typename?: 'Checkout' } & Pick<
ProductVariant, ProductVariant,
'id' | 'sku' | 'title' 'id' | 'sku' | 'title'
> & { > & {
selectedOptions: Array<
{ __typename?: 'SelectedOption' } & Pick<
SelectedOption,
'name' | 'value'
>
>
image?: Maybe< image?: Maybe<
{ __typename?: 'Image' } & Pick< { __typename?: 'Image' } & Pick<
Image, Image,
@ -5498,22 +5506,17 @@ export type GetPageQuery = { __typename?: 'QueryRoot' } & {
> >
} }
export type GetProductBySlugQueryVariables = Exact<{ export type ProductDetailsFragment = { __typename?: 'Product' } & Pick<
slug: Scalars['String']
}>
export type GetProductBySlugQuery = { __typename?: 'QueryRoot' } & {
productByHandle?: Maybe<
{ __typename?: 'Product' } & Pick<
Product, Product,
| 'id' | 'id'
| 'handle' | 'handle'
| 'availableForSale'
| 'title' | 'title'
| 'productType' | 'productType'
| 'vendor' | 'vendor'
| 'description' | 'description'
| 'descriptionHtml' | 'descriptionHtml'
> & { > & {
options: Array< options: Array<
{ __typename?: 'ProductOption' } & Pick< { __typename?: 'ProductOption' } & Pick<
ProductOption, ProductOption,
@ -5539,7 +5542,7 @@ export type GetProductBySlugQuery = { __typename?: 'QueryRoot' } & {
{ __typename?: 'ProductVariantEdge' } & { { __typename?: 'ProductVariantEdge' } & {
node: { __typename?: 'ProductVariant' } & Pick< node: { __typename?: 'ProductVariant' } & Pick<
ProductVariant, ProductVariant,
'id' | 'title' | 'sku' 'id' | 'title' | 'sku' | 'availableForSale' | 'requiresShipping'
> & { > & {
selectedOptions: Array< selectedOptions: Array<
{ __typename?: 'SelectedOption' } & Pick< { __typename?: 'SelectedOption' } & Pick<
@ -5576,6 +5579,22 @@ export type GetProductBySlugQuery = { __typename?: 'QueryRoot' } & {
> >
} }
} }
export type GetProductBySlugQueryVariables = Exact<{
slug: Scalars['String']
}>
export type GetProductBySlugQuery = { __typename?: 'QueryRoot' } & {
productByHandle?: Maybe<{ __typename?: 'Product' } & ProductDetailsFragment>
}
export type GetRelatedProductsQueryVariables = Exact<{
productId: Scalars['ID']
}>
export type GetRelatedProductsQuery = { __typename?: 'QueryRoot' } & {
productRecommendations?: Maybe<
Array<{ __typename?: 'Product' } & ListProductDetailsFragment>
> >
} }

View File

@ -13,16 +13,6 @@ directive @accessRestricted(
reason: String = null reason: String = null
) on FIELD_DEFINITION | OBJECT ) on FIELD_DEFINITION | OBJECT
"""
Contextualize data.
"""
directive @inContext(
"""
The country code for context.
"""
country: CountryCode!
) on QUERY | MUTATION
""" """
A version of the API. A version of the API.
""" """
@ -829,7 +819,7 @@ input CheckoutAttributesUpdateInput {
The required attributes are city, province, and country. The required attributes are city, province, and country.
Full validation of the addresses is still done at complete time. Full validation of the addresses is still done at complete time.
""" """
allowPartialAddresses: Boolean allowPartialAddresses: Boolean = false
} }
""" """
@ -872,7 +862,7 @@ input CheckoutAttributesUpdateV2Input {
The required attributes are city, province, and country. The required attributes are city, province, and country.
Full validation of the addresses is still done at complete time. Full validation of the addresses is still done at complete time.
""" """
allowPartialAddresses: Boolean allowPartialAddresses: Boolean = false
} }
""" """
@ -3391,7 +3381,7 @@ input CreditCardPaymentInput {
""" """
Executes the payment in test mode if possible. Defaults to `false`. Executes the payment in test mode if possible. Defaults to `false`.
""" """
test: Boolean test: Boolean = false
} }
""" """
@ -3422,7 +3412,7 @@ input CreditCardPaymentInputV2 {
""" """
Executes the payment in test mode if possible. Defaults to `false`. Executes the payment in test mode if possible. Defaults to `false`.
""" """
test: Boolean test: Boolean = false
} }
""" """
@ -5325,7 +5315,7 @@ Represents information about the metafields associated to the specified resource
""" """
interface HasMetafields { interface HasMetafields {
""" """
The metafield associated with the resource. Returns a metafield found by namespace and key.
""" """
metafield( metafield(
""" """
@ -7648,7 +7638,7 @@ type Product implements Node & HasMetafields {
): MediaConnection! ): MediaConnection!
""" """
The metafield associated with the resource. Returns a metafield found by namespace and key.
""" """
metafield( metafield(
""" """
@ -8150,7 +8140,7 @@ type ProductVariant implements Node & HasMetafields {
): Image ): Image
""" """
The metafield associated with the resource. Returns a metafield found by namespace and key.
""" """
metafield( metafield(
""" """
@ -9298,7 +9288,7 @@ input TokenizedPaymentInput {
""" """
Executes the payment in test mode if possible. Defaults to `false`. Executes the payment in test mode if possible. Defaults to `false`.
""" """
test: Boolean test: Boolean = false
""" """
Public Hash Key used for AndroidPay payments only. Public Hash Key used for AndroidPay payments only.
@ -9334,7 +9324,7 @@ input TokenizedPaymentInputV2 {
""" """
Whether to execute the payment in test mode, if possible. Test mode is not supported in production stores. Defaults to `false`. Whether to execute the payment in test mode, if possible. Test mode is not supported in production stores. Defaults to `false`.
""" """
test: Boolean test: Boolean = false
""" """
Public Hash Key used for AndroidPay payments only. Public Hash Key used for AndroidPay payments only.
@ -9375,7 +9365,7 @@ input TokenizedPaymentInputV3 {
""" """
Whether to execute the payment in test mode, if possible. Test mode is not supported in production stores. Defaults to `false`. Whether to execute the payment in test mode, if possible. Test mode is not supported in production stores. Defaults to `false`.
""" """
test: Boolean test: Boolean = false
""" """
Public Hash Key used for AndroidPay payments only. Public Hash Key used for AndroidPay payments only.

View File

@ -1,11 +1,5 @@
export const productConnectionFragment = /* GraphQL */ ` export const listProductDetailsFragment = /* GraphQL */ `
fragment productConnection on ProductConnection { fragment listProductDetails on Product {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
id id
title title
vendor vendor
@ -31,8 +25,21 @@ export const productConnectionFragment = /* GraphQL */ `
} }
} }
} }
`
export const productConnectionFragment = /* GraphQL */ `
fragment productConnection on ProductConnection {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
...listProductDetails
} }
} }
}
${listProductDetailsFragment}
` `
const getAllProductsQuery = /* GraphQL */ ` const getAllProductsQuery = /* GraphQL */ `

View File

@ -1,6 +1,5 @@
const getProductQuery = /* GraphQL */ ` export const productDetailsFragment = /* GraphQL */ `
query getProductBySlug($slug: String!) { fragment productDetails on Product {
productByHandle(handle: $slug) {
id id
handle handle
availableForSale availableForSale
@ -66,7 +65,15 @@ const getProductQuery = /* GraphQL */ `
} }
} }
} }
`
const getProductQuery = /* GraphQL */ `
query getProductBySlug($slug: String!) {
productByHandle(handle: $slug) {
...productDetails
} }
}
${productDetailsFragment}
` `
export default getProductQuery export default getProductQuery

View File

@ -0,0 +1,11 @@
import { listProductDetailsFragment } from './get-all-products-query'
const getRelatedProductsQuery = /* GraphQL */ `
query getRelatedProducts($productId: ID!) {
productRecommendations(productId: $productId) {
...listProductDetails
}
}
${listProductDetailsFragment}
`
export default getRelatedProductsQuery

View File

@ -28,15 +28,27 @@ export async function getStaticProps({
config, config,
preview, preview,
}) })
const { pages } = await pagesPromise const { pages } = await pagesPromise
const { categories } = await siteInfoPromise const { categories } = await siteInfoPromise
const { product } = await productPromise const { product } = await productPromise
const { products: relatedProducts } = await allProductsPromise
if (!product) { if (!product) {
throw new Error(`Product with slug '${params!.slug}' not found`) throw new Error(`Product with slug '${params!.slug}' not found`)
} }
const relatedProductsPromise = commerce.getRelatedProducts({
variables: { productId: product.id, first: 4 },
config,
preview,
})
// Temporary conditional query
const { products: relatedProducts } =
process.env.COMMERCE_PROVIDER === 'shopify'
? await relatedProductsPromise
: await allProductsPromise
return { return {
props: { props: {
pages, pages,