TEC-252 and TEC-256: Implementing fetchers, products list and search (#5)

* TEC-252: Base integration with commercetools using fetchers for REST and GraphQL endpoints

* TEC-252: WIP commenting some components that are failing while we don't have all the hooks defined

* add sdk integration

* TEC-256: Implementing product search

* TEC-256: removing unnecessary env variables

* TEC-256: review comments

* TEC-256: other remaining review fixes

Co-authored-by: nicossosa93 <nicolas.sosa@devgurus.io>
This commit is contained in:
matias-delavega-dg-dmi 2021-06-30 10:05:26 -03:00 committed by GitHub
parent 0e804d09f9
commit 716b540966
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 1541 additions and 45 deletions

View File

@ -4,7 +4,7 @@ import Link from 'next/link'
import CartItem from '../CartItem'
import s from './CartSidebarView.module.css'
import { Button } from '@components/ui'
import { UserNav } from '@components/common'
// import { UserNav } from '@components/common'
import { useUI } from '@components/ui/context'
import { Bag, Cross, Check } from '@components/icons'
import useCart from '@framework/cart/use-cart'
@ -48,9 +48,7 @@ const CartSidebarView: FC = () => {
<Cross className="h-6 w-6" />
</button>
</div>
<div className="space-y-1">
<UserNav />
</div>
<div className="space-y-1">{/* <UserNav /> */}</div>
</div>
</header>

View File

@ -40,7 +40,7 @@ const Navbar: FC<NavbarProps> = ({ links }) => (
</div>
<div className="flex justify-end flex-1 space-x-8">
<UserNav />
{/* <UserNav /> */}
</div>
</div>

View File

@ -7,7 +7,7 @@ import { Swatch, ProductSlider } from '@components/product'
import { Button, Container, Text, useUI } from '@components/ui'
import type { Product } from '@commerce/types/product'
import usePrice from '@framework/product/use-price'
import { useAddItem } from '@framework/cart'
// import { useAddItem } from '@framework/cart'
import { getVariant, SelectedOptions } from '../helpers'
import WishlistButton from '@components/wishlist/WishlistButton'
@ -20,7 +20,7 @@ interface Props {
const ProductView: FC<Props> = ({ product }) => {
// TODO: fix this missing argument issue
/* @ts-ignore */
const addItem = useAddItem()
// const addItem = useAddItem()
const { price } = usePrice({
amount: product.price.value,
baseAmount: product.price.retailPrice,
@ -32,28 +32,28 @@ const ProductView: FC<Props> = ({ product }) => {
useEffect(() => {
// Selects the default option
product.variants[0].options?.forEach((v) => {
setChoices((choices) => ({
...choices,
[v.displayName.toLowerCase()]: v.values[0].label.toLowerCase(),
}))
})
// product.variants[0].options?.forEach((v) => {
// setChoices((choices) => ({
// ...choices,
// [v.displayName.toLowerCase()]: v.values[0].label.toLowerCase(),
// }))
// })
}, [])
const variant = getVariant(product, choices)
const addToCart = async () => {
setLoading(true)
try {
await addItem({
productId: String(product.id),
variantId: String(variant ? variant.id : product.variants[0].id),
})
openSidebar()
setLoading(false)
} catch (err) {
setLoading(false)
}
// setLoading(true)
// try {
// await addItem({
// productId: String(product.id),
// variantId: String(variant ? variant.id : product.variants[0].id),
// })
// openSidebar()
// setLoading(false)
// } catch (err) {
// setLoading(false)
// }
}
return (

View File

@ -2,10 +2,10 @@ import React, { FC, useState } from 'react'
import cn from 'classnames'
import { useUI } from '@components/ui'
import { Heart } from '@components/icons'
import useAddItem from '@framework/wishlist/use-add-item'
import useCustomer from '@framework/customer/use-customer'
import useWishlist from '@framework/wishlist/use-wishlist'
import useRemoveItem from '@framework/wishlist/use-remove-item'
// import useAddItem from '@framework/wishlist/use-add-item'
// import useCustomer from '@framework/customer/use-customer'
// import useWishlist from '@framework/wishlist/use-wishlist'
// import useRemoveItem from '@framework/wishlist/use-remove-item'
import type { Product, ProductVariant } from '@commerce/types/product'
type Props = {
@ -19,10 +19,11 @@ const WishlistButton: FC<Props> = ({
className,
...props
}) => {
const { data } = useWishlist()
const addItem = useAddItem()
const removeItem = useRemoveItem()
const { data: customer } = useCustomer()
// const { data } = useWishlist()
const data = {}
// const addItem = useAddItem()
// const removeItem = useRemoveItem()
// const { data: customer } = useCustomer()
const { openModal, setModalView } = useUI()
const [loading, setLoading] = useState(false)
@ -40,21 +41,21 @@ const WishlistButton: FC<Props> = ({
if (loading) return
// A login is required before adding an item to the wishlist
if (!customer) {
setModalView('LOGIN_VIEW')
return openModal()
}
// if (!customer) {
// setModalView('LOGIN_VIEW')
// return openModal()
// }
setLoading(true)
try {
if (itemInWishlist) {
await removeItem({ id: itemInWishlist.id! })
// await removeItem({ id: itemInWishlist.id! })
} else {
await addItem({
productId,
variantId: variant?.id!,
})
// await addItem({
// productId,
// variantId: variant?.id!,
// })
}
setLoading(false)

View File

@ -7,7 +7,13 @@ const fs = require('fs')
const merge = require('deepmerge')
const prettier = require('prettier')
const PROVIDERS = ['bigcommerce', 'shopify', 'swell', 'vendure']
const PROVIDERS = [
'bigcommerce',
'shopify',
'swell',
'vendure',
'commercetools',
]
function getProviderName() {
return (
@ -18,6 +24,8 @@ function getProviderName() {
? 'shopify'
: process.env.NEXT_PUBLIC_SWELL_STORE_ID
? 'swell'
: process.env.CTP_API_URL
? 'commercetools'
: null)
)
}

View File

@ -0,0 +1,8 @@
COMMERCE_PROVIDER=commercetools
CTP_PROJECT_KEY=
CTP_CLIENT_SECRET=
CTP_CLIENT_ID=
CTP_AUTH_URL=
CTP_API_URL=
CTP_CONCURRENCY=

View File

@ -0,0 +1,18 @@
# Commercetools Provider
To deploy you will need a commercetools account with an existing project.
Then copy the `.env.template` file in this directory to `.env.local` in the main directory (which will be ignored by Git):
```bash
cp framework/commercetools/.env.template .env.local
```
Then, set the environment variables in `.env.local` to match the ones from your project.
## Contribute
Our commitment to Open Source can be found [here](https://vercel.com/oss).
If you find an issue with the provider or want a new feature, feel free to open a PR or [create a new issue](https://github.com/vercel/commerce/issues).
## Troubleshoot

View File

@ -0,0 +1,54 @@
import { ProductsEndpoint } from '.'
import { normalizeProduct } from '@framework/lib/normalize'
const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
res,
body: { search, categoryId, brandId, sort },
config,
}) => {
const queries: string[] = []
const isSearch = true
if (search) {
// TODO: TEC-264: Handle the locale properly
queries.push(`name.en: "${search}"`)
}
if (categoryId) {
queries.push(`categories.id: "${categoryId}"`)
}
if (brandId) {
queries.push(`variants.attributes.designer.key: "${brandId}"`)
}
let sorting
if (sort) {
sorting = getSortingValue(sort)
}
const query = {
filter: queries,
sort: sorting,
}
const data = await config.fetchProducts(query, isSearch)
const products = data.body.results
res.status(200).json({
data: {
found: data.body.total > 0,
products: products.map((item) => normalizeProduct(item)),
},
})
}
function getSortingValue(sort: string): string {
switch (sort) {
case 'price-asc':
return 'price asc'
case 'price-desc':
return 'price desc'
case 'latest-desc':
default:
return 'lastModifiedAt desc'
}
}
export default getProducts

View File

@ -0,0 +1,18 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import productsEndpoint from '@commerce/api/endpoints/catalog/products'
import type { ProductsSchema } from '@commerce/types/product'
import type { CommercetoolsAPI } from '@framework/api'
import getProducts from './get-products'
export type ProductsAPI = GetAPISchema<CommercetoolsAPI, ProductsSchema>
export type ProductsEndpoint = ProductsAPI['endpoint']
export const handlers: ProductsEndpoint['handlers'] = { getProducts }
const productsApi = createEndpoint<ProductsAPI>({
handler: productsEndpoint,
handlers,
})
export default productsApi

View File

@ -0,0 +1,101 @@
import type { RequestInit } from '@vercel/fetch'
import {
CommerceAPI,
CommerceAPIConfig,
getCommerceApi as commerceApi,
GraphQLFetcherResult,
CommerceAPIFetchOptions,
} from '@commerce/api'
import fetchGraphql from '@framework/api/utils/fetch-graphql-api'
import fetchProducts from '@framework/api/utils/fetch-products'
import getProduct from '@framework/api/operations/get-product'
import getAllProducts from '@framework/api/operations/get-all-products'
import getAllProductPaths from '@framework/api/operations/get-all-product-paths'
import getPage from '@framework/api/operations/get-page'
import getAllPages from '@framework/api/operations/get-all-pages'
import login from '@framework/api/operations/login'
import getCustomerWishlist from '@framework/api/operations/get-customer-wishlist'
import getSiteInfo from '@framework/api/operations/get-site-info'
export interface CommercetoolsConfig extends CommerceAPIConfig {
locale: string
projectKey: string
clientId: string
clientSecret: string
host: string
oauthHost: string
concurrency: string | number
fetch<Data = any, Variables = any>(
query: string,
queryData?: CommerceAPIFetchOptions<Variables>,
fetchOptions?: RequestInit
): Promise<GraphQLFetcherResult<Data>>
fetchProducts: typeof fetchProducts
}
const PROJECT_KEY = process.env.CTP_PROJECT_KEY || 'projectKey'
const CLIENT_ID = process.env.CTP_CLIENT_ID || 'projectKey'
const CLIENT_SECRET = process.env.CTP_CLIENT_SECRET || 'projectKey'
const AUTH_URL = process.env.CTP_AUTH_URL || 'projectKey'
const API_URL = process.env.CTP_API_URL || 'projectKey'
const CONCURRENCY = process.env.CTP_CONCURRENCY || 0
if (!API_URL) {
throw new Error(
`The environment variable CTP_API_URL is missing and it's required to access your store`
)
}
if (!PROJECT_KEY) {
throw new Error(
`The environment variable CTP_PROJECT_KEY is missing and it's required to access your store`
)
}
if (!AUTH_URL) {
throw new Error(
`The environment variables CTP_AUTH_URL have to be set in order to access your store`
)
}
const ONE_DAY = 60 * 60 * 24
const config: CommercetoolsConfig = {
locale: '',
commerceUrl: '',
host: API_URL,
projectKey: PROJECT_KEY,
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
oauthHost: AUTH_URL,
concurrency: CONCURRENCY,
apiToken: '',
cartCookie: '',
cartCookieMaxAge: 0,
customerCookie: '',
fetch: fetchGraphql,
fetchProducts: fetchProducts,
}
const operations = {
getAllPages,
getPage,
getAllProductPaths,
getAllProducts,
getProduct,
getSiteInfo,
getCustomerWishlist,
login,
}
export const provider = { config, operations }
export type Provider = typeof provider
export type CommercetoolsAPI<P extends Provider = Provider> = CommerceAPI<P>
export function getCommerceApi<P extends Provider>(
customProvider: P = provider as any
): CommercetoolsAPI<P> {
return commerceApi(customProvider)
}

View File

@ -0,0 +1,40 @@
import { CommercetoolsConfig, Provider } from '@framework/api'
import { OperationContext } from '@commerce/api/operations'
export type Page = any
export type GetAllPagesResult<
T extends { pages: any[] } = { pages: Page[] }
> = T
export default function getAllPagesOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllPages(opts?: {
config?: Partial<CommercetoolsConfig>
preview?: boolean
}): Promise<GetAllPagesResult>
async function getAllPages<T extends { pages: any[] }>(opts: {
url: string
config?: Partial<CommercetoolsConfig>
preview?: boolean
}): Promise<GetAllPagesResult<T>>
async function getAllPages({
config: cfg,
preview,
}: {
url?: string
config?: Partial<CommercetoolsConfig>
preview?: boolean
} = {}): Promise<GetAllPagesResult> {
const config = commerce.getConfig(cfg)
return {
pages: [],
}
}
return getAllPages
}

View File

@ -0,0 +1,50 @@
import { OperationContext, OperationOptions } from '@commerce/api/operations'
import { GetAllProductPathsOperation } from '@commerce/types/product'
import { CommercetoolsConfig, Provider } from '@framework/api'
export type GetAllProductPathsResult = {
products: Array<{ node: { path: string } }>
}
export default function getAllProductPathsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProductPaths<
T extends GetAllProductPathsOperation
>(opts?: {
variables?: T['variables']
config?: CommercetoolsConfig
}): Promise<T['data']>
async function getAllProductPaths<T extends GetAllProductPathsOperation>(
opts: {
variables?: T['variables']
config?: CommercetoolsConfig
} & OperationOptions
): Promise<T['data']>
async function getAllProductPaths<T extends GetAllProductPathsOperation>({
query,
variables,
config: cfg,
}: {
query?: string
variables?: T['variables']
config?: CommercetoolsConfig
} = {}): Promise<T['data']> {
const config = commerce.getConfig(cfg)
// RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `query`
const data: any = await config.fetchProducts(query)
const paths = data.body.results.map((prod: any) => ({
// TODO: TEC-264: Handle the locale properly
path: `/${prod.slug.en}`,
}))
return {
products: paths,
}
}
return getAllProductPaths
}

View File

@ -0,0 +1,31 @@
import { Product } from '@commerce/types/product'
import { Provider, CommercetoolsConfig } from '@framework/api'
import { normalizeProduct } from '@framework/lib/normalize'
import { OperationContext } from '@commerce/api/operations'
export default function getAllProductsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProducts(opts?: {
config?: Partial<CommercetoolsConfig>
preview?: boolean
}): Promise<{ products: Product[] }>
async function getAllProducts({
config: cfg,
}: {
config?: Partial<CommercetoolsConfig>
preview?: boolean
} = {}): Promise<{ products: Product[] | any[] }> {
const config = commerce.getConfig(cfg)
const data: any = await config.fetchProducts()
const prods = data.body.results.map((prod: any) => normalizeProduct(prod))
return {
products: prods,
}
}
return getAllProducts
}

View File

@ -0,0 +1,23 @@
import { OperationContext } from '@commerce/api/operations'
import { Provider, CommercetoolsConfig } from '@framework/api'
export default function getCustomerWishlistOperation({
commerce,
}: OperationContext<Provider>) {
async function getCustomerWishlist({
config: cfg,
variables,
includeProducts,
}: {
url?: string
variables: any
config?: Partial<CommercetoolsConfig>
includeProducts?: boolean
}): Promise<any> {
// Not implemented yet
const config = commerce.getConfig(cfg)
return { wishlist: {} }
}
return getCustomerWishlist
}

View File

@ -0,0 +1,45 @@
import { CommercetoolsConfig, Provider } from '@framework/api'
import { OperationContext } from '@commerce/api/operations'
export type Page = any
export type GetPageResult<T extends { page?: any } = { page?: Page }> = T
export type PageVariables = {
id: number
}
export default function getPageOperation({
commerce,
}: OperationContext<Provider>) {
async function getPage(opts: {
url?: string
variables: PageVariables
config?: Partial<CommercetoolsConfig>
preview?: boolean
}): Promise<GetPageResult>
async function getPage<T extends { page?: any }, V = any>(opts: {
url: string
variables: V
config?: Partial<CommercetoolsConfig>
preview?: boolean
}): Promise<GetPageResult<T>>
async function getPage({
url,
variables,
config: cfg,
preview,
}: {
url?: string
variables: PageVariables
config?: Partial<CommercetoolsConfig>
preview?: boolean
}): Promise<GetPageResult> {
const config = commerce.getConfig(cfg)
return {}
}
return getPage
}

View File

@ -0,0 +1,38 @@
import { Product } from '@commerce/types/product'
import { OperationContext } from '@commerce/api/operations'
import { Provider, CommercetoolsConfig } from '@framework/api'
import { normalizeProduct } from '@framework/lib/normalize'
export default function getProductOperation({
commerce,
}: OperationContext<Provider>) {
async function getProduct({
variables,
config: cfg,
}: {
variables: {
slug?: string
id?: string
locale?: string
}
config?: Partial<CommercetoolsConfig>
preview?: boolean
}): Promise<Product | {} | any> {
const config = commerce.getConfig(cfg)
// TODO: TEC-264: Handle the locale properly
const queryArg = {
where: `slug(en="${variables.slug}")`,
}
const projection = await config.fetchProducts(queryArg)
const product = projection.body.results[0]
if (product) {
return { product: normalizeProduct(product) }
}
return {}
}
return getProduct
}

View File

@ -0,0 +1,39 @@
import { Provider, CommercetoolsConfig } from '@framework/api'
import { OperationContext } from '@commerce/api/operations'
import { Category } from '@commerce/types/site'
import { getAllCategoriesAndBrandsQuery } from '@framework/utils/queries/get-category'
import { normalizeSite } from '@framework/lib/normalize'
export type GetSiteInfoResult<
T extends { categories: any[]; brands: any[] } = {
categories: Category[]
brands: any[]
}
> = T
export default function getSiteInfoOperation({
commerce,
}: OperationContext<Provider>) {
async function getSiteInfo({
query = getAllCategoriesAndBrandsQuery,
variables,
config: cfg,
}: {
query?: string
variables?: any
config?: Partial<CommercetoolsConfig>
preview?: boolean
} = {}): Promise<GetSiteInfoResult> {
const config = commerce.getConfig(cfg)
const {
data: { categories, productTypes },
}: any = await config.fetch(query)
const ctCategories = categories.results
const ctBrands =
productTypes?.results[0]?.attributeDefinitions?.results[0]?.type?.values
?.results
return normalizeSite(ctCategories, ctBrands)
}
return getSiteInfo
}

View File

@ -0,0 +1,43 @@
import type { ServerResponse } from 'http'
import type {
OperationContext,
OperationOptions,
} from '@commerce/api/operations'
import { Provider, CommercetoolsConfig } from '@framework/api'
export default function loginOperation({
commerce,
}: OperationContext<Provider>) {
async function login<T extends { variables: any; data: any }>(opts: {
variables: T['variables']
config?: Partial<CommercetoolsConfig>
res: ServerResponse
}): Promise<T['data']>
async function login<T extends { variables: any; data: any }>(
opts: {
variables: T['variables']
config?: Partial<CommercetoolsConfig>
res: ServerResponse
} & OperationOptions
): Promise<T['data']>
async function login<T extends { variables: any; data: any }>({
query = '',
variables,
res: response,
config: cfg,
}: {
query?: string
variables: T['variables']
res: ServerResponse
config?: Partial<CommercetoolsConfig>
}): Promise<T['data']> {
const config = commerce.getConfig(cfg)
return {
result: '',
}
}
return login
}

View File

@ -0,0 +1,25 @@
import type { Response } from '@vercel/fetch'
// Used for GraphQL errors
export class CommercetoolsGraphQLError extends Error {}
export class CommercetoolsApiError extends Error {
status: number
res: Response
data: any
constructor(msg: string, res: Response, data?: any) {
super(msg)
this.name = 'CommercetoolsApiError'
this.status = res.status
this.res = res
this.data = data
}
}
export class CommercetoolsNetworkError extends Error {
constructor(msg: string) {
super(msg)
this.name = 'CommercetoolsNetworkError'
}
}

View File

@ -0,0 +1,37 @@
import { FetcherError } from '@commerce/utils/errors'
import type { GraphQLFetcher } from '@commerce/api'
import Commercetools from '@framework/utils/commercetools'
import { provider } from '@framework/api'
const fetchGraphqlApi: GraphQLFetcher = async (
query: string,
{ variables } = {}
) => {
const { config } = provider
const commercetools = Commercetools({
clientId: config.clientId,
clientSecret: config.clientSecret,
projectKey: config.projectKey,
host: config.host,
oauthHost: config.oauthHost,
concurrency: config.concurrency,
})
const { requestExecute } = commercetools
try {
const result = await requestExecute
.graphql()
.post({
body: {
query,
variables,
},
})
.execute()
return result.body
} catch (err) {
throw err
}
}
export default fetchGraphqlApi

View File

@ -0,0 +1,33 @@
import Commercetools from '@framework/utils/commercetools'
import { provider } from '@framework/api'
const fetchProducts = async (query?: any, isSearch?: boolean) => {
const { config } = provider
const commercetools = Commercetools({
clientId: config.clientId,
clientSecret: config.clientSecret,
projectKey: config.projectKey,
host: config.host,
oauthHost: config.oauthHost,
concurrency: config.concurrency,
})
const { requestExecute } = commercetools
try {
if (isSearch) {
return await requestExecute
.productProjections()
.search()
.get({ queryArgs: query })
.execute()
} else {
return await requestExecute
.productProjections()
.get({ queryArgs: query })
.execute()
}
} catch (err) {
throw err
}
}
export default fetchProducts

View File

@ -0,0 +1,3 @@
import zeitFetch from '@vercel/fetch'
export default zeitFetch()

View File

@ -0,0 +1,3 @@
// export { default as useLogin } from './use-login'
// export { default as useLogout } from './use-logout'
// export { default as useSignup } from './use-signup'

View File

@ -0,0 +1,4 @@
// export { default as useAddItem } from './use-add-item'
// export { default as useCart } from './use-cart'
// export { default as useRemoveItem } from './use-remove-item'
// export { default as useUpdateItem } from './use-update-item'

View File

View File

@ -0,0 +1,9 @@
{
"provider": "commercetools",
"features": {
"wishlist": false,
"customer": false,
"cart": false,
"auth": false
}
}

View File

@ -0,0 +1 @@
// export { default as useCustomer } from './use-customer'

View File

@ -0,0 +1,41 @@
import { FetcherError } from '@commerce/utils/errors'
import type { Fetcher } from '@commerce/utils/types'
async function getText(res: Response) {
try {
return (await res.text()) || res.statusText
} catch (error) {
return res.statusText
}
}
async function getError(res: Response) {
if (res.headers.get('Content-Type')?.includes('application/json')) {
const data = await res.json()
return new FetcherError({ errors: data.errors, status: res.status })
}
return new FetcherError({ message: await getText(res), status: res.status })
}
const fetcher: Fetcher = async ({
url,
method = 'GET',
variables,
body: bodyObj,
}) => {
const hasBody = Boolean(variables || bodyObj)
const body = hasBody
? JSON.stringify(variables ? { variables } : bodyObj)
: undefined
const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined
const res = await fetch(url!, { method, body, headers })
if (res.ok) {
const { data } = await res.json()
return data
}
throw await getError(res)
}
export default fetcher

View File

@ -0,0 +1,38 @@
import type { ReactNode } from 'react'
import {
CommerceConfig,
CommerceProvider as CoreCommerceProvider,
useCommerce as useCoreCommerce,
} from '@commerce'
import {
commercetoolsProvider,
CommercetoolsProvider,
} from '@framework/provider'
export { commercetoolsProvider }
export type { CommercetoolsProvider }
export const commercetoolsConfig: CommerceConfig = {
locale: 'en-US',
cartCookie: '',
}
export type CommercetoolsConfig = Partial<CommerceConfig>
export type CommercetoolsProps = {
children?: ReactNode
locale: string
} & CommercetoolsConfig
export function CommerceProvider({ children, ...config }: CommercetoolsProps) {
return (
<CoreCommerceProvider
provider={commercetoolsProvider}
config={{ ...commercetoolsConfig, ...config }}
>
{children}
</CoreCommerceProvider>
)
}
export const useCommerce = () => useCoreCommerce<CommercetoolsProvider>()

View File

@ -0,0 +1,70 @@
export type HasParent = { id: string; parent?: { id: string } | null }
export type TreeNode<T extends HasParent> = T & {
children: Array<TreeNode<T>>
expanded: boolean
}
export type RootNode<T extends HasParent> = {
id?: string
children: Array<TreeNode<T>>
}
export function arrayToTree<T extends HasParent>(
nodes: T[],
currentState?: RootNode<T>
): RootNode<T> {
const topLevelNodes: Array<TreeNode<T>> = []
const mappedArr: { [id: string]: TreeNode<T> } = {}
const currentStateMap = treeToMap(currentState)
// First map the nodes of the array to an object -> create a hash table.
for (const node of nodes) {
mappedArr[node.id] = { ...(node as any), children: [] }
}
for (const id of nodes.map((n) => n.id)) {
if (mappedArr.hasOwnProperty(id)) {
const mappedElem = mappedArr[id]
mappedElem.expanded = currentStateMap.get(id)?.expanded ?? false
const parent = mappedElem.parent
if (!parent) {
continue
}
// If the element is not at the root level, add it to its parent array of children.
const parentIsRoot = !mappedArr[parent.id]
if (!parentIsRoot) {
if (mappedArr[parent.id]) {
mappedArr[parent.id].children.push(mappedElem)
} else {
mappedArr[parent.id] = { children: [mappedElem] } as any
}
} else {
topLevelNodes.push(mappedElem)
}
}
}
// // tslint:disable-next-line:no-non-null-assertion
// const rootId = topLevelNodes.length ? topLevelNodes[0].id : undefined
// const children = topLevelNodes.length ? topLevelNodes[0].children : []
// return { id: "root", children: topLevelNodes }
return { children: topLevelNodes }
}
/**
* Converts an existing tree (as generated by the arrayToTree function) into a flat
* Map. This is used to persist certain states (e.g. `expanded`) when re-building the
* tree.
*/
function treeToMap<T extends HasParent>(
tree?: RootNode<T>
): Map<string, TreeNode<T>> {
const nodeMap = new Map<string, TreeNode<T>>()
function visit(node: TreeNode<T>) {
nodeMap.set(node.id, node)
node.children.forEach(visit)
}
if (tree) {
visit(tree as TreeNode<T>)
}
return nodeMap
}

View File

@ -0,0 +1,5 @@
// Remove trailing and leading slash, usually included in nodes
// returned by the BigCommerce API
const getSlug = (path: string) => path.replace(/^\/|\/$/g, '')
export default getSlug

View File

@ -0,0 +1,185 @@
import type {
CommercetoolsProduct,
Product,
ProductVariant,
CommercetoolsProductVariant,
CommerceToolsProductPrice,
ProductPrice,
} from '@framework/types/product'
import type {
Cart,
CommercetoolsCart,
CommercetoolsLineItems,
LineItem,
} from '@framework/types/cart'
import type {
CommercetoolsBrands,
CommercetoolsCategory,
Category,
Brand,
} from '@framework/types/site'
import { arrayToTree } from '@framework/lib/array-to-tree'
function normalizeVariants(
variants: CommercetoolsProductVariant[],
published: boolean
): ProductVariant[] {
return variants.map((variant) => {
return {
id: variant.id,
options: [],
availableForSale: published,
}
})
}
function normalizePrice(price: CommerceToolsProductPrice): ProductPrice {
const value =
price.discounted && price.discounted.value
? price.discounted.value.centAmount
: price.value.centAmount
return {
value: value / 100,
currencyCode: price.value.currencyCode,
retailPrice: 0,
salePrice: 0,
listPrice: 0,
extendedListPrice: 0,
extendedSalePrice: 0,
}
}
export function normalizeProduct(data: CommercetoolsProduct): Product {
return {
id: data.id,
name: data.name.en,
description:
data.description && data.description.en
? data.description.en
: 'No description',
slug: data.slug.en,
path: data.slug.en,
images: data.masterVariant.images,
variants: normalizeVariants(data.variants, data.published),
options: [],
price: normalizePrice(
data.masterVariant.price
? data.masterVariant.price
: data.masterVariant.prices[0]
),
sku: data.masterVariant.sku,
}
}
function convertTaxMode(data: CommercetoolsCart): boolean {
return data && data.taxMode && data.taxMode === 'Disabled'
? false
: data && data.taxMode
? true
: false
}
export function normalizeCart(data: CommercetoolsCart): Cart {
const totalPrice =
data.taxedPrice &&
data.taxedPrice.totalGross &&
data.taxedPrice.totalGross.centAmount
? data.taxedPrice.totalGross.centAmount / 100
: data.totalPrice.centAmount / 100
return {
id: data.id,
customerId: data.customerId,
email: data.customerEmail,
createdAt: data.createdAt,
currency: { code: data.totalPrice.currencyCode },
taxesIncluded: convertTaxMode(data),
lineItems: data.lineItems.map((item) => normalizeLineItem(item)),
lineItemsSubtotalPrice: 0,
subtotalPrice: 0,
totalPrice,
discounts: [],
}
}
function normalizeLineItem(item: CommercetoolsLineItems): LineItem {
const price =
item.price && item.price.value && item.price.value.centAmount
? item.price.value.centAmount
: item.variant.prices[0].value.centAmount
return {
id: item.id,
variantId: item.variant.id,
productId: item.productId,
name: item.name,
quantity: item.quantity,
variant: {
id: item.variant.id,
sku: item.variant.sku,
name: item.variant.key,
image: {
url:
item.variant.images &&
item.variant.images[0] &&
item.variant.images[0].url
? item.variant.images[0].url
: '',
width:
item.variant.images &&
item.variant.images[0] &&
item.variant.images[0].dimensions &&
item.variant.images[0].dimensions.w
? item.variant.images[0].dimensions.w
: undefined,
height:
item.variant.images &&
item.variant.images[0] &&
item.variant.images[0].dimensions &&
item.variant.images[0].dimensions.h
? item.variant.images[0].dimensions.h
: undefined,
},
requiresShipping: false,
price: price / 100,
listPrice: 0,
},
path: item.productSlug,
discounts: [],
}
}
type Site = { categories: any[]; brands: Brand[] }
export function normalizeSite(
ctCategories: CommercetoolsCategory[],
ctBrands: CommercetoolsBrands[]
): Site {
const categories = ctCategories.map((ctCategory) => {
return {
id: ctCategory.id,
// TODO: TEC-264 we need to handle locale properly
name: ctCategory.name,
slug: ctCategory.slug,
path: ctCategory.slug,
//add a random parentId to add in children array
parent: ctCategory.parent ? ctCategory.parent : { id: 'idRoot' },
}
})
const treeCategories = arrayToTree(categories).children
const brands = ctBrands.map((ctBrand) => {
return {
node: {
name: ctBrand.label,
path: `brands/${ctBrand.key}`,
entityId: ctBrand.key,
},
}
})
return {
categories: treeCategories,
brands,
}
}

View File

@ -0,0 +1,8 @@
const commerce = require('./commerce.config.json')
module.exports = {
commerce,
images: {
domains: ['s3-eu-west-1.amazonaws.com'],
},
}

View File

@ -0,0 +1,2 @@
export { default as usePrice } from './use-price'
export { default as useSearch } from './use-search'

View File

@ -0,0 +1,2 @@
export * from '@commerce/product/use-price'
export { default } from '@commerce/product/use-price'

View File

@ -0,0 +1,54 @@
import { SWRHook } from '@commerce/utils/types'
import useSearch, { UseSearch } from '@commerce/product/use-search'
import { Product } from '@commerce/types/product'
import type { SearchProductsHook } from '../../commerce/types/product'
export default useSearch as UseSearch<typeof handler>
export type SearchProductsInput = {
search?: string
categoryId?: string
brandId?: string
sort?: string
locale?: string
}
export type SearchProductsData = {
products: Product[]
found: boolean
}
export const handler: SWRHook<SearchProductsHook> = {
fetchOptions: {
url: 'api/catalog/products',
method: 'GET',
},
async fetcher({ input, options, fetch }) {
const { search, categoryId, brandId, sort } = input
const url = new URL(options.url!, 'http://a')
if (search) url.searchParams.set('search', search)
if (categoryId) url.searchParams.set('categoryId', String(categoryId))
if (brandId) url.searchParams.set('brandId', String(brandId))
if (sort) url.searchParams.set('sort', sort)
return await fetch({
url: url.pathname + url.search,
method: options.method,
})
},
useHook: ({ useData }) => (input = {}) => {
return useData({
input: [
['search', input.search],
['categoryId', input.categoryId],
['brandId', input.brandId],
['sort', input.sort],
],
swrOptions: {
revalidateOnFocus: false,
...input.swrOptions,
},
})
},
}

View File

@ -0,0 +1,24 @@
import { Provider } from '@commerce'
// import { handler as useCart } from './cart/use-cart'
// import { handler as useAddItem } from './cart/use-add-item'
// import { handler as useUpdateItem } from './cart/use-update-item'
// import { handler as useRemoveItem } from './cart/use-remove-item'
// import { handler as useCustomer } from './customer/use-customer'
import { handler as useSearch } from './product/use-search'
// import { handler as useLogin } from './auth/use-login'
// import { handler as useLogout } from './auth/use-logout'
// import { handler as useSignup } from './auth/use-signup'
import fetcher from './fetcher'
// Export a provider with the CommerceHooks
export const commercetoolsProvider: Provider = {
locale: 'en-us',
cartCookie: 'session',
fetcher,
// cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
// customer: { useCustomer },
products: { useSearch },
// auth: { useLogin, useLogout, useSignup }
}
export type CommercetoolsProvider = typeof commercetoolsProvider

View File

@ -0,0 +1,103 @@
import * as Core from '@commerce/types/cart'
import * as Products from './product'
export * from '@commerce/types/cart'
// TODO: this type should match:
// https://developer.bigcommerce.com/api-reference/cart-checkout/server-server-cart-api/cart/getacart#responses
export type CommercetoolsCart = {
id: string
version: number
customerId: string
customerEmail: string
createdAt: string
lastModifiedAt: string
lineItems: CommercetoolsLineItems[]
totalPrice: {
currencyCode: string
centAmount: number
}
cartState: string
inventoryMode: string
taxMode: string
taxRoundingMode: string
taxedPrice?: TaxedItemPrice
discountCodes: discountCodes[]
}
export type TaxedItemPrice = {
totalNet: Products.Money
totalGross: Products.Money
}
export type CommercetoolsLineItems = {
id: string
productId: string
productSlug: Products.LocalString
name: {
en: string
}
variant: Products.CommercetoolsProductVariant
price: Products.CommerceToolsProductPrice
totalPrice: totalPrice
quantity: number
state: {
quantity: number
state: {
id: string
key: string
version: number
createdAt: string
lastModifiedAt: string
}
}
priceMode: string
}
export type discountCodes = {
discountCode: {
id: string
version: number
createdAt: string
lastModifiedAt: string
code: string
cartDiscounts: {
id: string
version: number
createdAt: string
lastModifiedAt: string
name: string
isActive: boolean
}
}
}
export type totalPrice = {
currencyCode: string
centAmount: number
}
/**
* Extend core cart types
*/
export type Cart = Core.Cart & {
lineItems: Core.LineItem[]
}
export type LineItem = Core.LineItem
export type OptionSelections = {
option_id: number
option_value: number | string
}
export type CartItemBody = Core.CartItemBody & {
productId: string // The product id is always required for BC
optionSelections?: OptionSelections
}
export type CartTypes = {
cart: Cart
item: Core.LineItem
itemBody: CartItemBody
}
export type CartHooks = Core.CartHooks<CartTypes>
export type GetCartHook = CartHooks['getCart']
export type AddItemHook = CartHooks['addItem']
export type UpdateItemHook = CartHooks['updateItem']
export type RemoveItemHook = CartHooks['removeItem']
export type CartSchema = Core.CartSchema<CartTypes>
export type CartHandlers = Core.CartHandlers<CartTypes>
export type GetCartHandler = CartHandlers['getCart']
export type AddItemHandler = CartHandlers['addItem']
export type UpdateItemHandler = CartHandlers['updateItem']
export type RemoveItemHandler = CartHandlers['removeItem']

View File

@ -0,0 +1 @@
export * from '@commerce/types/checkout'

View File

@ -0,0 +1 @@
export * from '@commerce/types/common'

View File

@ -0,0 +1,20 @@
import * as Core from '@commerce/types/customer'
export * from '@commerce/types/customer'
export type Customers = {
id: string
version: number
createdAt: string
lastModifiedAt: string
email: string
password: string
isEmailVerified: boolean
}
// get Customer
export type GetCustomerById = {
id: string
}
export type CustomerSchema = Core.CustomerSchema

View File

@ -0,0 +1,25 @@
import * as Cart from './cart'
import * as Checkout from './checkout'
import * as Common from './common'
import * as Customer from './customer'
import * as Login from './login'
import * as Logout from './logout'
import * as Page from './page'
import * as Product from './product'
import * as Signup from './signup'
import * as Site from './site'
import * as Wishlist from './wishlist'
export type {
Cart,
Checkout,
Common,
Customer,
Login,
Logout,
Page,
Product,
Signup,
Site,
Wishlist,
}

View File

@ -0,0 +1,13 @@
import * as Core from '@commerce/types/login'
import type { LoginMutationVariables } from '../schema'
export * from '@commerce/types/login'
export type CommercetoolsLogin = {
email: string
password: string
}
export type LoginOperation = Core.LoginOperation & {
variables: LoginMutationVariables
}

View File

@ -0,0 +1 @@
export * from '@commerce/types/logout'

View File

@ -0,0 +1,11 @@
import * as Core from '@commerce/types/page'
export * from '@commerce/types/page'
export type Page = Core.Page
export type PageTypes = {
page: Page
}
export type GetAllPagesOperation = Core.GetAllPagesOperation<PageTypes>
export type GetPageOperation = Core.GetPageOperation<PageTypes>

View File

@ -0,0 +1,66 @@
import * as Core from '@commerce/types/product'
export type CommercetoolsProduct = {
id: string
name: LocalString
description: LocalString
slug: LocalString
metaDescription: LocalString
masterVariant: CommercetoolsProductVariant
variants: CommercetoolsProductVariant[]
published: boolean
}
export type CommercetoolsProductVariant = {
id: string
key: string
sku: string
images: Images[]
price?: CommerceToolsProductPrice
prices: CommerceToolsProductPrice[]
attributes: ProductAttributes[]
}
export type ProductAttributes = {
name: string
value: string | AttributeDefinition | boolean | number
}
export type AttributeDefinition = {
key: string
label: string
}
export type Images = {
url: string
dimensions: {
w: number
h: number
}
}
export type CommerceToolsProductPrice = {
id: string
value: Money
discounted: DiscountedPrice
}
export type DiscountedPrice = {
value: Money
}
export type Money = {
type: string
currencyCode: string
centAmount: number
fractionDigits: number
}
export type LocalString = {
en: string
'es-AR': string
'es-CL': string
'es-PE': string
de: string
}
// get Product
export type GetProductById = {
id: string
}
export type Product = Core.Product
export type ProductVariant = Core.ProductVariant
export type ProductPrice = Core.ProductPrice
export type ProductOption = Core.ProductOption
export type ProductOptionValue = Core.ProductOptionValues
export type ProductImage = Core.ProductImage

View File

@ -0,0 +1 @@
export * from '@commerce/types/signup'

View File

@ -0,0 +1,13 @@
import * as Core from '@commerce/types/site'
export type CommercetoolsCategory = {
id: string
name: string
slug: string
parent: { id: string }
}
export type CommercetoolsBrands = {
key: string
label?: string
}
export type Brand = Core.Brand
export type Category = Core.Category

View File

@ -0,0 +1,23 @@
import * as Core from '@commerce/types/wishlist'
import { definitions } from '../api/definitions/wishlist'
import type { ProductEdge } from '../api/operations/get-all-products'
export * from '@commerce/types/wishlist'
export type WishlistItem = NonNullable<
definitions['wishlist_Full']['items']
>[0] & {
product?: ProductEdge['node']
}
export type Wishlist = Omit<definitions['wishlist_Full'], 'items'> & {
items?: WishlistItem[]
}
export type WishlistTypes = {
wishlist: Wishlist
itemBody: Core.WishlistItemBody
}
export type WishlistSchema = Core.WishlistSchema<WishlistTypes>
export type GetCustomerWishlistOperation = Core.GetCustomerWishlistOperation<WishlistTypes>

View File

@ -0,0 +1,51 @@
import { createAuthMiddlewareForClientCredentialsFlow } from '@commercetools/sdk-middleware-auth'
import { createHttpMiddleware } from '@commercetools/sdk-middleware-http'
import { createQueueMiddleware } from '@commercetools/sdk-middleware-queue'
import { createClient } from '@commercetools/sdk-client'
import { createApiBuilderFromCtpClient } from '@commercetools/platform-sdk'
import fetch from 'node-fetch'
interface Props {
clientId: string
clientSecret: string
projectKey: string
host: string
oauthHost: string
concurrency: string | number
}
export default ({
clientId,
clientSecret,
projectKey,
host,
oauthHost,
concurrency = 10,
}: Props) => {
interface Commercetools {
requestExecute: any
}
const ctpClient = createClient({
middlewares: [
createAuthMiddlewareForClientCredentialsFlow({
host: oauthHost,
projectKey,
credentials: {
clientId,
clientSecret,
},
fetch,
}),
createQueueMiddleware({ concurrency }),
createHttpMiddleware({ host, fetch }),
],
})
const apiRoot = createApiBuilderFromCtpClient(ctpClient)
const commercetools = <Commercetools>{
requestExecute: apiRoot.withProjectKey({ projectKey }),
}
return commercetools
}

View File

@ -0,0 +1,29 @@
export const getAllCategoriesAndBrandsQuery = /* GraphQL */ `
query getCategoriesAndBrands {
categories {
results {
id
name(locale: "en")
slug(locale: "en")
}
}
productTypes {
results {
attributeDefinitions(includeNames: "designer") {
results {
type {
... on EnumAttributeDefinitionType {
values {
results {
key
label
}
}
}
}
}
}
}
}
}
`

View File

@ -0,0 +1,30 @@
const getProductQuery =
/* GraphQL */
`
query getProductQuery($id: String!, $locale: Locale) {
product(id: $id) {
id
masterData {
current {
name(locale: $locale)
metaDescription(locale: $locale)
slug(locale: $locale)
masterVariant {
prices {
value {
centAmount
currencyCode
}
}
sku
images {
url
}
}
}
}
}
}
`
export default getProductQuery

View File

@ -0,0 +1,3 @@
// export { default as useAddItem } from './use-add-item'
// export { default as useWishlist } from './use-wishlist'
// export { default as useRemoveItem } from './use-remove-item'

4
global.d.ts vendored
View File

@ -1,2 +1,6 @@
// Declarations for modules without types
declare module 'next-themes'
declare module '@commercetools/sdk-middleware-auth'
declare module '@commercetools/sdk-middleware-http'
declare module '@commercetools/sdk-middleware-queue'
declare module '@commercetools/sdk-client'

View File

@ -19,6 +19,11 @@
"node": "14.x"
},
"dependencies": {
"@commercetools/platform-sdk": "^1.13.0",
"@commercetools/sdk-client": "^2.1.2",
"@commercetools/sdk-middleware-auth": "^6.1.4",
"@commercetools/sdk-middleware-http": "^6.0.11",
"@commercetools/sdk-middleware-queue": "^2.1.4",
"@reach/portal": "^0.11.2",
"@vercel/fetch": "^6.1.0",
"autoprefixer": "^10.2.4",

View File

@ -22,8 +22,8 @@
"@components/*": ["components/*"],
"@commerce": ["framework/commerce"],
"@commerce/*": ["framework/commerce/*"],
"@framework": ["framework/shopify"],
"@framework/*": ["framework/shopify/*"]
"@framework": ["framework/commercetools"],
"@framework/*": ["framework/commercetools/*"]
}
},
"include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],

View File

@ -464,6 +464,43 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
"@commercetools/platform-sdk@^1.13.0":
version "1.14.0"
resolved "https://registry.yarnpkg.com/@commercetools/platform-sdk/-/platform-sdk-1.14.0.tgz#7e5410eb7ff56d8ed3a5422dccdac67bbd2e6156"
integrity sha512-JclsCi0H8VpR3rwt5ZVHXXKGLCy56UwM6RkB3esuxTYarg3p5sMDXVRiCUchoSyr1rScf4/ksjO3mNzKxEPd5g==
dependencies:
"@commercetools/sdk-client" "^2.1.1"
"@commercetools/sdk-middleware-auth" "^6.0.4"
"@commercetools/sdk-middleware-http" "^6.0.4"
"@commercetools/sdk-middleware-logger" "^2.1.1"
"@commercetools/sdk-client@^2.1.1", "@commercetools/sdk-client@^2.1.2":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@commercetools/sdk-client/-/sdk-client-2.1.2.tgz#fe1e442f67a385f103470669784c0fa20d7a2314"
integrity sha512-YPpK39pkjfedjS1/BFg2d7CrvTeN7vIS5vfiqEkKOtAoUxiNkugv59gRSoh2Em8SOccxyM/skpgHyTqfmJzXug==
"@commercetools/sdk-middleware-auth@^6.0.4", "@commercetools/sdk-middleware-auth@^6.1.4":
version "6.1.4"
resolved "https://registry.yarnpkg.com/@commercetools/sdk-middleware-auth/-/sdk-middleware-auth-6.1.4.tgz#c5f464ae1627336715681e4590b63777e034c890"
integrity sha512-49R1DWsA+pNHH7/2K6QU5wnJSXabljKA8dvzs5HcbLwutlDp3Io0XHgIJa9qpfYhgW6k0h9dPICcLbESrQBXYw==
dependencies:
node-fetch "^2.3.0"
"@commercetools/sdk-middleware-http@^6.0.11", "@commercetools/sdk-middleware-http@^6.0.4":
version "6.0.11"
resolved "https://registry.yarnpkg.com/@commercetools/sdk-middleware-http/-/sdk-middleware-http-6.0.11.tgz#0ca16cefe881b68c1d2b77ddbd3a48733a5ee062"
integrity sha512-9Keb5rv6fvdA9qdehBEjk/JMrAzlBbg76TodsvhCZZZteaO0+ybjFgtV0ekdGyI4awxOxgsiPDZrTmQNvnI5Wg==
"@commercetools/sdk-middleware-logger@^2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@commercetools/sdk-middleware-logger/-/sdk-middleware-logger-2.1.1.tgz#9283fdc8c403a7e2d4d06637e6015770b864e64a"
integrity sha512-k/Jm3lsWbszPBHtPAvu0rINTq398p4ddv0zbAH8R4p6Yc1GkBEy6tNgHPzX/eFskI/qerPy9IsW1xK8pqgtHHQ==
"@commercetools/sdk-middleware-queue@^2.1.4":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@commercetools/sdk-middleware-queue/-/sdk-middleware-queue-2.1.4.tgz#d8b162ff83fc553cc5abef8599571389874983d6"
integrity sha512-8TxeUb+jdSemUt/wd9hEcPl2uK2sTRPd5BEwXzOYAlyQJBXMMju2GMo5ASDWz7xjKLoijEuY86Jo7D9JSP8DPQ==
"@csstools/convert-colors@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7"
@ -4486,7 +4523,7 @@ node-emoji@^1.8.1:
dependencies:
lodash.toarray "^4.4.0"
node-fetch@2.6.1, node-fetch@^2.6.1:
node-fetch@2.6.1, node-fetch@^2.3.0, node-fetch@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==