feat: getAllProducts and getProductBySlug

This commit is contained in:
Reza Babaei
2021-09-18 19:32:40 +03:00
parent 7aa1673e18
commit 6ef1d554e9
17 changed files with 19012 additions and 373 deletions

View File

@@ -25,3 +25,4 @@ NEXT_PUBLIC_VENDURE_SHOP_API_URL=
NEXT_PUBLIC_VENDURE_LOCAL_URL=
NEXT_PUBLIC_WOOCOMMERCE_SHOP_API_URL=
NEXT_PUBLIC_WOOCOMMERCE_IMAGES_DOMAIN=

View File

@@ -23,7 +23,7 @@ export function selectDefaultOptionFromProduct(
updater: Dispatch<SetStateAction<SelectedOptions>>
) {
// Selects the default option
product.variants[0].options?.forEach((v) => {
product?.variants[0]?.options?.forEach((v) => {
updater((choices) => ({
...choices,
[v.displayName.toLowerCase()]: v.values[0].label.toLowerCase(),

View File

@@ -6,10 +6,9 @@ import { GetAllProductPathsOperation } from '../../types/product'
import {
GetAllProductPathsQuery,
GetAllProductPathsQueryVariables,
ProductEdge,
} from '../../schema'
import type { ShopifyConfig, Provider } from '..'
import { getAllProductsQuery } from '../../utils'
import type { WooCommerceConfig, Provider } from '..'
import getAllProductsQuery from '../../wp/queries/get-all-products-paths-query'
export default function getAllProductPathsOperation({
commerce,
@@ -18,13 +17,13 @@ export default function getAllProductPathsOperation({
T extends GetAllProductPathsOperation
>(opts?: {
variables?: T['variables']
config?: ShopifyConfig
config?: WooCommerceConfig
}): Promise<T['data']>
async function getAllProductPaths<T extends GetAllProductPathsOperation>(
opts: {
variables?: T['variables']
config?: ShopifyConfig
config?: WooCommerceConfig
} & OperationOptions
): Promise<T['data']>
@@ -34,7 +33,7 @@ export default function getAllProductPathsOperation({
variables,
}: {
query?: string
config?: ShopifyConfig
config?: WooCommerceConfig
variables?: T['variables']
} = {}): Promise<T['data']> {
config = commerce.getConfig(config)
@@ -45,9 +44,11 @@ export default function getAllProductPathsOperation({
>(query, { variables })
return {
products: data.products.edges.map(({ node: { handle } }) => ({
path: `/${handle}`,
})),
products: data?.products?.edges
? data.products.edges.map(({ node: { slug } }) => ({
path: `/${slug}`,
}))
: [],
}
}

View File

@@ -6,12 +6,14 @@ import { GetAllProductsOperation } from '../../types/product'
import {
GetAllProductsQuery,
GetAllProductsQueryVariables,
Product as WooCommerceProduct,
SimpleProduct,
} from '../../schema'
import type { WooCommerceConfig, Provider } from '..'
import getAllProductsQuery from '../../utils/queries/get-all-products-query'
import getAllProductsQuery from '../../wp/queries/get-all-products-query'
import { normalizeProduct } from '../../utils'
import type { Product } from '../../types/product'
export default function getAllProductsOperation({
commerce,
}: OperationContext<Provider>) {
@@ -40,7 +42,7 @@ export default function getAllProductsOperation({
preview?: boolean
} = {}): Promise<T['data']> {
const { fetch, locale } = commerce.getConfig(config)
// console.log({ a: 'reza', query, variables, config, fetch, locale })
try {
const { data } = await fetch<
GetAllProductsQuery,
@@ -56,18 +58,18 @@ export default function getAllProductsOperation({
}),
}
)
console.log({ data })
return {
products: [],
let products: Product[] = []
if (data?.products?.edges) {
data?.products?.edges?.map(({ node }) =>
products.push(normalizeProduct(node as SimpleProduct))
)
}
// return {
// products: data?.products?.edges
// ? data.products.edges.map(({ node }) =>
// normalizeProduct(node as WooCommerceProduct)
// )
// : [],
// }
return {
products,
}
} catch (e) {
throw e
}

View File

@@ -3,23 +3,24 @@ import type {
OperationOptions,
} from '@commerce/api/operations'
import { GetProductOperation } from '../../types/product'
import { normalizeProduct, getProductQuery } from '../../utils'
import type { ShopifyConfig, Provider } from '..'
import { GetProductBySlugQuery, Product as ShopifyProduct } from '../../schema'
import { normalizeProduct } from '../../utils'
import getProductQuery from '../../wp/queries/get-product-query'
import type { WooCommerceConfig, Provider } from '..'
import { GetProductBySlugQuery, SimpleProduct } from '../../schema'
export default function getProductOperation({
commerce,
}: OperationContext<Provider>) {
async function getProduct<T extends GetProductOperation>(opts: {
variables: T['variables']
config?: Partial<ShopifyConfig>
config?: Partial<WooCommerceConfig>
preview?: boolean
}): Promise<T['data']>
async function getProduct<T extends GetProductOperation>(
opts: {
variables: T['variables']
config?: Partial<ShopifyConfig>
config?: Partial<WooCommerceConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
@@ -31,13 +32,13 @@ export default function getProductOperation({
}: {
query?: string
variables: T['variables']
config?: Partial<ShopifyConfig>
config?: Partial<WooCommerceConfig>
preview?: boolean
}): Promise<T['data']> {
const { fetch, locale } = commerce.getConfig(cfg)
const {
data: { productByHandle },
data: { product },
} = await fetch<GetProductBySlugQuery>(
query,
{
@@ -53,8 +54,8 @@ export default function getProductOperation({
)
return {
...(productByHandle && {
product: normalizeProduct(productByHandle as ShopifyProduct),
...(product && {
product: normalizeProduct(product as SimpleProduct),
}),
}
}

View File

@@ -1,7 +1,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 getProduct } from './get-product'
export { default as getAllProductPaths } from './get-all-product-paths'
export { default as getProduct } from './get-product'
export { default as getSiteInfo } from './get-site-info'
// export { default as login } from './login'

View File

@@ -9,23 +9,6 @@ const fetchGraphqlApi: GraphQLFetcher = async (
{ variables } = {},
fetchOptions
) => {
try {
console.log({
resss: {
API_URL,
...fetchOptions,
method: 'POST',
headers: {
...fetchOptions?.headers,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
variables,
}),
},
})
const res = await fetch(API_URL, {
...fetchOptions,
method: 'POST',
@@ -39,26 +22,8 @@ const fetchGraphqlApi: GraphQLFetcher = async (
}),
})
const { data, errors, status } = await res.json()
const result = await res.json()
if (errors) {
console.log({ errors: errors[0].extensions })
console.log(getError(errors, status))
}
return { data, res }
} catch (err) {
console.log({ err })
console.log(
getError(
[
{
message: `${err} \n Most likely related to an unexpected output. e.g the store might be protected with password or not available.`,
},
],
500
)
)
}
return result
}
export default fetchGraphqlApi

View File

@@ -10,7 +10,7 @@
}
],
"generates": {
"./framework/woocommerce/schema.d.ts": {
"./framework/woocommerce/schema.ts": {
"plugins": ["typescript", "typescript-operations"],
"config": {
"scalars": {

File diff suppressed because it is too large Load Diff

View File

@@ -1,197 +1,58 @@
import type { Page } from '../types/page'
import type { Product } from '../types/product'
import type { Cart, LineItem } from '../types/cart'
import type { Category } from '../types/site'
import type { Product, ProductImage } from '../types/product'
import {
Product as ShopifyProduct,
Checkout,
CheckoutLineItemEdge,
SelectedOption,
ImageConnection,
ProductVariantConnection,
MoneyV2,
ProductOption,
Page as ShopifyPage,
PageEdge,
Collection,
} from '../schema'
import { colorMap } from '@lib/colors'
import { SimpleProduct, ProductToMediaItemConnection } from '../schema'
const money = ({ amount, currencyCode }: MoneyV2) => {
const normalizeProductImages = ({
edges,
}: ProductToMediaItemConnection): ProductImage[] => {
const edges_ =
edges
?.filter((edge) => edge?.node)
.map(({ node }) => {
return {
value: +amount,
currencyCode,
}
url: node.sourceUrl,
alt: node.altText ?? node.title,
}
}) ?? []
const normalizeProductOption = ({
id,
name: displayName,
values,
}: ProductOption) => {
return {
__typename: 'MultipleChoiceOption',
id,
displayName: displayName.toLowerCase(),
values: values.map((value) => {
let output: any = {
label: value,
}
if (displayName.match(/colou?r/gi)) {
const mapedColor = colorMap[value.toLowerCase().replace(/ /g, '')]
if (mapedColor) {
output = {
...output,
hexColors: [mapedColor],
}
}
}
return output
}),
}
}
const normalizeProductImages = ({ edges }: ImageConnection) =>
edges?.map(({ node: { originalSrc: url, ...rest } }) => ({
url,
...rest,
}))
const normalizeProductVariants = ({ edges }: ProductVariantConnection) => {
return edges?.map(
({
node: {
id,
selectedOptions,
sku,
title,
priceV2,
compareAtPriceV2,
requiresShipping,
availableForSale,
},
}) => {
return {
id,
name: title,
sku: sku ?? id,
price: +priceV2.amount,
listPrice: +compareAtPriceV2?.amount,
requiresShipping,
availableForSale,
options: selectedOptions.map(({ name, value }: SelectedOption) => {
const options = normalizeProductOption({
id,
name,
values: [value],
})
return options
}),
}
}
)
return edges_
}
export function normalizeProduct({
id,
title: name,
vendor,
images,
variants,
name,
sku,
description,
descriptionHtml,
handle,
priceRange,
options,
metafields,
shortDescription,
slug,
image,
galleryImages,
price,
...rest
}: ShopifyProduct): Product {
return {
}: SimpleProduct): Product {
const images: ProductToMediaItemConnection = galleryImages ?? { edges: [] }
if (image) {
images.edges?.push({ node: image })
}
const product = {
id,
name,
vendor,
path: `/${handle}`,
slug: handle?.replace(/^\/+|\/+$/g, ''),
price: money(priceRange?.minVariantPrice),
options: [],
variants: [],
name: name ?? id,
sku: sku ?? 'sku',
path: slug ?? id,
slug: slug?.replace(/^\/+|\/+$/g, ''),
images: normalizeProductImages(images),
variants: variants ? normalizeProductVariants(variants) : [],
options: options
? options
.filter((o) => o.name !== 'Title') // By default Shopify adds a 'Title' name when there's only one option. We don't need it. https://community.shopify.com/c/Shopify-APIs-SDKs/Adding-new-product-variant-is-automatically-adding-quot-Default/td-p/358095
.map((o) => normalizeProductOption(o))
: [],
...(description && { description }),
...(descriptionHtml && { descriptionHtml }),
...rest,
}
price: { value: 0, currencyCode: 'USD' },
description: description ?? shortDescription ?? '',
descriptionHtml: description ?? shortDescription ?? '',
}
export function normalizeCart(checkout: Checkout): Cart {
return {
id: checkout.id,
url: checkout.webUrl,
customerId: '',
email: '',
createdAt: checkout.createdAt,
currency: {
code: checkout.totalPriceV2?.currencyCode,
},
taxesIncluded: checkout.taxesIncluded,
lineItems: checkout.lineItems?.edges.map(normalizeLineItem),
lineItemsSubtotalPrice: +checkout.subtotalPriceV2?.amount,
subtotalPrice: +checkout.subtotalPriceV2?.amount,
totalPrice: checkout.totalPriceV2?.amount,
discounts: [],
}
if (price) {
product.price.value = Number(price.substring(1))
}
function normalizeLineItem({
node: { id, title, variant, quantity },
}: CheckoutLineItemEdge): LineItem {
return {
id,
variantId: String(variant?.id),
productId: String(variant?.id),
name: `${title}`,
quantity,
variant: {
id: String(variant?.id),
sku: variant?.sku ?? '',
name: variant?.title!,
image: {
url: variant?.image?.originalSrc || '/product-img-placeholder.svg',
},
requiresShipping: variant?.requiresShipping ?? false,
price: variant?.priceV2?.amount,
listPrice: variant?.compareAtPriceV2?.amount,
},
path: String(variant?.product?.handle),
discounts: [],
options: variant?.title == 'Default Title' ? [] : variant?.selectedOptions,
return product
}
}
export const normalizePage = (
{ title: name, handle, ...page }: ShopifyPage,
locale: string = 'en-US'
): Page => ({
...page,
url: `/${locale}/${handle}`,
name,
})
export const normalizePages = (edges: PageEdge[], locale?: string): Page[] =>
edges?.map((edge) => normalizePage(edge.node, locale))
export const normalizeCategory = ({
title: name,
handle,
id,
}: Collection): Category => ({
id,
name,
slug: handle,
path: `/${handle}`,
})

View File

@@ -0,0 +1,17 @@
const getAllProductsPathsQuery = /* GraphQL */ `
query getAllProductPaths($first: Int = 250, $cursor: String) {
products(first: $first, after: $cursor) {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
slug
}
cursor
}
}
}
`
export default getAllProductsPathsQuery

View File

@@ -9,9 +9,43 @@ const getAllProductsQuery = /* GraphQL */ `
node {
id
name
image {
uri
sku
galleryImages {
edges {
node {
id
srcSet
title
sourceUrl
mediaItemUrl
altText
sizes
}
}
}
image {
id
srcSet
title
sourceUrl
mediaItemUrl
altText
sizes
}
description
link
shortDescription
slug
... on SimpleProduct {
id
name
price
content
uri
slug
shortDescription
regularPrice
salePrice
}
}
}

View File

@@ -0,0 +1,48 @@
const getProductQuery = /* GraphQL */ `
query getProductBySlug($slug: ID!) {
product(id: $slug, idType: SLUG) {
id
name
sku
galleryImages {
edges {
node {
id
srcSet
title
sourceUrl
mediaItemUrl
altText
sizes
}
}
}
image {
id
srcSet
title
sourceUrl
mediaItemUrl
altText
sizes
}
description
link
shortDescription
slug
... on SimpleProduct {
id
name
price
content
uri
slug
shortDescription
regularPrice
salePrice
}
}
}
`
export default getProductQuery

View File

@@ -1,72 +0,0 @@
const getProductQuery = /* GraphQL */ `
query getProductBySlug($slug: String!) {
productByHandle(handle: $slug) {
id
handle
availableForSale
title
productType
vendor
description
descriptionHtml
options {
id
name
values
}
priceRange {
maxVariantPrice {
amount
currencyCode
}
minVariantPrice {
amount
currencyCode
}
}
variants(first: 250) {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
id
title
sku
availableForSale
requiresShipping
selectedOptions {
name
value
}
priceV2 {
amount
currencyCode
}
compareAtPriceV2 {
amount
currencyCode
}
}
}
}
images(first: 250) {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
originalSrc
altText
width
height
}
}
}
}
}
`
export default getProductQuery

View File

@@ -10,6 +10,7 @@ const isShopify = provider === 'shopify'
const isSaleor = provider === 'saleor'
const isSwell = provider === 'swell'
const isVendure = provider === 'vendure'
// const isWooCommerce = provider === 'woocommerce'
module.exports = withCommerceConfig({
commerce,
@@ -17,6 +18,9 @@ module.exports = withCommerceConfig({
locales: ['en-US', 'es'],
defaultLocale: 'en-US',
},
images: {
domains: [process.env.NEXT_PUBLIC_WOOCOMMERCE_IMAGES_DOMAIN],
},
rewrites() {
return [
(isBC || isShopify || isSwell || isVendure) && {

View File

@@ -3,7 +3,7 @@ import commerce from '@lib/api/commerce'
import { Layout } from '@components/common'
import { ProductCard } from '@components/product'
import { Grid, Marquee, Hero } from '@components/ui'
// import HomeAllProductsGrid from '@components/common/HomeAllProductsGrid'
import HomeAllProductsGrid from '@components/common/HomeAllProductsGrid'
import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
export async function getStaticProps({
@@ -13,7 +13,7 @@ export async function getStaticProps({
}: GetStaticPropsContext) {
const config = { locale, locales }
const productsPromise = await commerce.getAllProducts({
variables: { first: 6 },
variables: { first: 12 },
config,
preview,
// // Saleor provider only
@@ -21,22 +21,13 @@ export async function getStaticProps({
})
// // const pagesPromise = commerce.getAllPages({ config, preview })
// // const siteInfoPromise = commerce.getSiteInfo({ config, preview })
// // const { products } = await productsPromise
const { products } = await productsPromise
// // const { pages } = await pagesPromise
// // const { categories, brands } = await siteInfoPromise
console.log({
query: {
variables: { first: 6 },
config,
preview,
// // Saleor provider only
// ...({ featured: true } as any),
},
productsPromise,
})
return {
props: {
products: [],
products,
// categories,
// brands,
// pages,
@@ -82,17 +73,17 @@ export default function Home({
}}
/>
))}
</Grid>
</Grid>*/}
<Marquee>
{products.slice(3).map((product: any, i: number) => (
<ProductCard key={product.id} product={product} variant="slim" />
))}
</Marquee> */}
{/* <HomeAllProductsGrid
newestProducts={products}
categories={categories}
brands={brands}
/> */}
</Marquee>
<HomeAllProductsGrid
products={products}
categories={[]} //{categories}
brands={[]} //{brands}
/>
</>
)
}

View File

@@ -15,23 +15,24 @@ export async function getStaticProps({
preview,
}: GetStaticPropsContext<{ slug: string }>) {
const config = { locale, locales }
const pagesPromise = commerce.getAllPages({ config, preview })
const siteInfoPromise = commerce.getSiteInfo({ config, preview })
// const pagesPromise = commerce.getAllPages({ config, preview })
// const siteInfoPromise = commerce.getSiteInfo({ config, preview })
const productPromise = commerce.getProduct({
variables: { slug: params!.slug },
config,
preview,
})
const allProductsPromise = await commerce.getAllProducts({
variables: { first: 4 },
config,
preview,
})
const { pages } = await pagesPromise
const { categories } = await siteInfoPromise
// const allProductsPromise = await commerce.getAllProducts({
// variables: { first: 4 },
// config,
// preview,
// })
// const { pages } = await pagesPromise
// const { categories } = await siteInfoPromise
const { product } = await productPromise
const { products: relatedProducts } = await allProductsPromise
// const { products: relatedProducts } = await allProductsPromise
if (!product) {
throw new Error(`Product with slug '${params!.slug}' not found`)
@@ -39,10 +40,10 @@ export async function getStaticProps({
return {
props: {
pages,
// pages,
product,
relatedProducts,
categories,
relatedProducts: [],
// categories,
},
revalidate: 200,
}
@@ -67,7 +68,7 @@ export async function getStaticPaths({ locales }: GetStaticPathsContext) {
export default function Slug({
product,
relatedProducts,
relatedProducts = [],
}: InferGetStaticPropsType<typeof getStaticProps>) {
const router = useRouter()