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

View File

@ -7,10 +7,11 @@ import type {
GetAllProductPathsOperation,
GetAllProductsOperation,
GetProductOperation,
GetRelatedProductsOperation,
} from '../types/product'
import type { APIProvider, CommerceAPI } from '.'
const noop = () => {
const noop = (_props?: any) => {
throw new Error('Not implemented')
}
@ -22,6 +23,7 @@ export const OPERATIONS = [
'getCustomerWishlist',
'getAllProductPaths',
'getAllProducts',
'getRelatedProducts',
'getProduct',
] as const
@ -139,6 +141,22 @@ export type Operations<P extends APIProvider> = {
): 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: {
<T extends GetProductOperation>(opts: {
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> = {
data: { product?: T['product'] }
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 getAllProducts } from './get-all-products'
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 getSiteInfo } from './get-site-info'
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. */
export type HasMetafields = {
/** The metafield associated with the resource. */
/** Returns a metafield found by namespace and key. */
metafield?: Maybe<Metafield>
/** A paginated list of metafields associated with the resource. */
metafields: MetafieldConnection
@ -3908,7 +3908,7 @@ export type Product = Node &
images: ImageConnection
/** The media associated with the product. */
media: MediaConnection
/** The metafield associated with the resource. */
/** Returns a metafield found by namespace and key. */
metafield?: Maybe<Metafield>
/** A paginated list of metafields associated with the resource. */
metafields: MetafieldConnection
@ -4235,7 +4235,7 @@ export type ProductVariant = Node &
id: Scalars['ID']
/** Image associated with the product variant. This field falls back to the product image if no image is available. */
image?: Maybe<Image>
/** The metafield associated with the resource. */
/** Returns a metafield found by namespace and key. */
metafield?: Maybe<Metafield>
/** A paginated list of metafields associated with the resource. */
metafields: MetafieldConnection
@ -5265,14 +5265,7 @@ export type GetAllProductPathsQuery = { __typename?: 'QueryRoot' } & {
}
}
export type ProductConnectionFragment = { __typename?: 'ProductConnection' } & {
pageInfo: { __typename?: 'PageInfo' } & Pick<
PageInfo,
'hasNextPage' | 'hasPreviousPage'
>
edges: Array<
{ __typename?: 'ProductEdge' } & {
node: { __typename?: 'Product' } & Pick<
export type ListProductDetailsFragment = { __typename?: 'Product' } & Pick<
Product,
'id' | 'title' | 'vendor' | 'handle'
> & {
@ -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,
'id' | 'sku' | 'title'
> & {
selectedOptions: Array<
{ __typename?: 'SelectedOption' } & Pick<
SelectedOption,
'name' | 'value'
>
>
image?: Maybe<
{ __typename?: 'Image' } & Pick<
Image,
@ -5498,16 +5506,11 @@ export type GetPageQuery = { __typename?: 'QueryRoot' } & {
>
}
export type GetProductBySlugQueryVariables = Exact<{
slug: Scalars['String']
}>
export type GetProductBySlugQuery = { __typename?: 'QueryRoot' } & {
productByHandle?: Maybe<
{ __typename?: 'Product' } & Pick<
export type ProductDetailsFragment = { __typename?: 'Product' } & Pick<
Product,
| 'id'
| 'handle'
| 'availableForSale'
| 'title'
| 'productType'
| 'vendor'
@ -5539,7 +5542,7 @@ export type GetProductBySlugQuery = { __typename?: 'QueryRoot' } & {
{ __typename?: 'ProductVariantEdge' } & {
node: { __typename?: 'ProductVariant' } & Pick<
ProductVariant,
'id' | 'title' | 'sku'
'id' | 'title' | 'sku' | 'availableForSale' | 'requiresShipping'
> & {
selectedOptions: Array<
{ __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
) on FIELD_DEFINITION | OBJECT
"""
Contextualize data.
"""
directive @inContext(
"""
The country code for context.
"""
country: CountryCode!
) on QUERY | MUTATION
"""
A version of the API.
"""
@ -829,7 +819,7 @@ input CheckoutAttributesUpdateInput {
The required attributes are city, province, and country.
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.
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`.
"""
test: Boolean
test: Boolean = false
}
"""
@ -3422,7 +3412,7 @@ input CreditCardPaymentInputV2 {
"""
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 {
"""
The metafield associated with the resource.
Returns a metafield found by namespace and key.
"""
metafield(
"""
@ -7648,7 +7638,7 @@ type Product implements Node & HasMetafields {
): MediaConnection!
"""
The metafield associated with the resource.
Returns a metafield found by namespace and key.
"""
metafield(
"""
@ -8150,7 +8140,7 @@ type ProductVariant implements Node & HasMetafields {
): Image
"""
The metafield associated with the resource.
Returns a metafield found by namespace and key.
"""
metafield(
"""
@ -9298,7 +9288,7 @@ input TokenizedPaymentInput {
"""
Executes the payment in test mode if possible. Defaults to `false`.
"""
test: Boolean
test: Boolean = false
"""
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`.
"""
test: Boolean
test: Boolean = false
"""
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`.
"""
test: Boolean
test: Boolean = false
"""
Public Hash Key used for AndroidPay payments only.

View File

@ -1,11 +1,5 @@
export const productConnectionFragment = /* GraphQL */ `
fragment productConnection on ProductConnection {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
export const listProductDetailsFragment = /* GraphQL */ `
fragment listProductDetails on Product {
id
title
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 */ `

View File

@ -1,6 +1,5 @@
const getProductQuery = /* GraphQL */ `
query getProductBySlug($slug: String!) {
productByHandle(handle: $slug) {
export const productDetailsFragment = /* GraphQL */ `
fragment productDetails on Product {
id
handle
availableForSale
@ -66,7 +65,15 @@ const getProductQuery = /* GraphQL */ `
}
}
}
`
const getProductQuery = /* GraphQL */ `
query getProductBySlug($slug: String!) {
productByHandle(handle: $slug) {
...productDetails
}
}
${productDetailsFragment}
`
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,
preview,
})
const { pages } = await pagesPromise
const { categories } = await siteInfoPromise
const { product } = await productPromise
const { products: relatedProducts } = await allProductsPromise
if (!product) {
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 {
props: {
pages,