diff --git a/components/cart/CartSidebarView/CartSidebarView.tsx b/components/cart/CartSidebarView/CartSidebarView.tsx index 326390327..e4f1ff951 100644 --- a/components/cart/CartSidebarView/CartSidebarView.tsx +++ b/components/cart/CartSidebarView/CartSidebarView.tsx @@ -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 = () => { -
- -
+
{/* */}
diff --git a/components/common/Navbar/Navbar.tsx b/components/common/Navbar/Navbar.tsx index 16088485f..d0ed2bdae 100644 --- a/components/common/Navbar/Navbar.tsx +++ b/components/common/Navbar/Navbar.tsx @@ -40,7 +40,7 @@ const Navbar: FC = ({ links }) => (
- + {/* */}
diff --git a/components/product/ProductView/ProductView.tsx b/components/product/ProductView/ProductView.tsx index a9385b2f0..070333d2f 100644 --- a/components/product/ProductView/ProductView.tsx +++ b/components/product/ProductView/ProductView.tsx @@ -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 = ({ 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 = ({ 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 ( diff --git a/components/wishlist/WishlistButton/WishlistButton.tsx b/components/wishlist/WishlistButton/WishlistButton.tsx index 5841de11e..ddb3b05cd 100644 --- a/components/wishlist/WishlistButton/WishlistButton.tsx +++ b/components/wishlist/WishlistButton/WishlistButton.tsx @@ -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 = ({ 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 = ({ 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) diff --git a/framework/commerce/config.js b/framework/commerce/config.js index 48f0d526b..d95ed1c26 100644 --- a/framework/commerce/config.js +++ b/framework/commerce/config.js @@ -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) ) } diff --git a/framework/commercetools/.env.template b/framework/commercetools/.env.template new file mode 100644 index 000000000..5694588d3 --- /dev/null +++ b/framework/commercetools/.env.template @@ -0,0 +1,8 @@ +COMMERCE_PROVIDER=commercetools + +CTP_PROJECT_KEY= +CTP_CLIENT_SECRET= +CTP_CLIENT_ID= +CTP_AUTH_URL= +CTP_API_URL= +CTP_CONCURRENCY= \ No newline at end of file diff --git a/framework/commercetools/README.md b/framework/commercetools/README.md new file mode 100644 index 000000000..c9d35760e --- /dev/null +++ b/framework/commercetools/README.md @@ -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 diff --git a/framework/commercetools/api/endpoints/catalog/products/get-products.ts b/framework/commercetools/api/endpoints/catalog/products/get-products.ts new file mode 100644 index 000000000..987170bdb --- /dev/null +++ b/framework/commercetools/api/endpoints/catalog/products/get-products.ts @@ -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 diff --git a/framework/commercetools/api/endpoints/catalog/products/index.ts b/framework/commercetools/api/endpoints/catalog/products/index.ts new file mode 100644 index 000000000..0438fba8f --- /dev/null +++ b/framework/commercetools/api/endpoints/catalog/products/index.ts @@ -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 + +export type ProductsEndpoint = ProductsAPI['endpoint'] + +export const handlers: ProductsEndpoint['handlers'] = { getProducts } + +const productsApi = createEndpoint({ + handler: productsEndpoint, + handlers, +}) + +export default productsApi diff --git a/framework/commercetools/api/index.ts b/framework/commercetools/api/index.ts new file mode 100644 index 000000000..6b7b9fea4 --- /dev/null +++ b/framework/commercetools/api/index.ts @@ -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( + query: string, + queryData?: CommerceAPIFetchOptions, + fetchOptions?: RequestInit + ): Promise> + 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

= CommerceAPI

+ +export function getCommerceApi

( + customProvider: P = provider as any +): CommercetoolsAPI

{ + return commerceApi(customProvider) +} diff --git a/framework/commercetools/api/operations/get-all-pages.ts b/framework/commercetools/api/operations/get-all-pages.ts new file mode 100644 index 000000000..078157687 --- /dev/null +++ b/framework/commercetools/api/operations/get-all-pages.ts @@ -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) { + async function getAllPages(opts?: { + config?: Partial + preview?: boolean + }): Promise + + async function getAllPages(opts: { + url: string + config?: Partial + preview?: boolean + }): Promise> + + async function getAllPages({ + config: cfg, + preview, + }: { + url?: string + config?: Partial + preview?: boolean + } = {}): Promise { + const config = commerce.getConfig(cfg) + + return { + pages: [], + } + } + + return getAllPages +} diff --git a/framework/commercetools/api/operations/get-all-product-paths.ts b/framework/commercetools/api/operations/get-all-product-paths.ts new file mode 100644 index 000000000..483aa5a45 --- /dev/null +++ b/framework/commercetools/api/operations/get-all-product-paths.ts @@ -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) { + async function getAllProductPaths< + T extends GetAllProductPathsOperation + >(opts?: { + variables?: T['variables'] + config?: CommercetoolsConfig + }): Promise + + async function getAllProductPaths( + opts: { + variables?: T['variables'] + config?: CommercetoolsConfig + } & OperationOptions + ): Promise + + async function getAllProductPaths({ + query, + variables, + config: cfg, + }: { + query?: string + variables?: T['variables'] + config?: CommercetoolsConfig + } = {}): Promise { + 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 +} diff --git a/framework/commercetools/api/operations/get-all-products.ts b/framework/commercetools/api/operations/get-all-products.ts new file mode 100644 index 000000000..04bc2da5d --- /dev/null +++ b/framework/commercetools/api/operations/get-all-products.ts @@ -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) { + async function getAllProducts(opts?: { + config?: Partial + preview?: boolean + }): Promise<{ products: Product[] }> + + async function getAllProducts({ + config: cfg, + }: { + config?: Partial + 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 +} diff --git a/framework/commercetools/api/operations/get-customer-wishlist.ts b/framework/commercetools/api/operations/get-customer-wishlist.ts new file mode 100644 index 000000000..4ca0d879d --- /dev/null +++ b/framework/commercetools/api/operations/get-customer-wishlist.ts @@ -0,0 +1,23 @@ +import { OperationContext } from '@commerce/api/operations' +import { Provider, CommercetoolsConfig } from '@framework/api' + +export default function getCustomerWishlistOperation({ + commerce, +}: OperationContext) { + async function getCustomerWishlist({ + config: cfg, + variables, + includeProducts, + }: { + url?: string + variables: any + config?: Partial + includeProducts?: boolean + }): Promise { + // Not implemented yet + const config = commerce.getConfig(cfg) + return { wishlist: {} } + } + + return getCustomerWishlist +} diff --git a/framework/commercetools/api/operations/get-page.ts b/framework/commercetools/api/operations/get-page.ts new file mode 100644 index 000000000..68d32125f --- /dev/null +++ b/framework/commercetools/api/operations/get-page.ts @@ -0,0 +1,45 @@ +import { CommercetoolsConfig, Provider } from '@framework/api' +import { OperationContext } from '@commerce/api/operations' + +export type Page = any + +export type GetPageResult = T + +export type PageVariables = { + id: number +} + +export default function getPageOperation({ + commerce, +}: OperationContext) { + async function getPage(opts: { + url?: string + variables: PageVariables + config?: Partial + preview?: boolean + }): Promise + + async function getPage(opts: { + url: string + variables: V + config?: Partial + preview?: boolean + }): Promise> + + async function getPage({ + url, + variables, + config: cfg, + preview, + }: { + url?: string + variables: PageVariables + config?: Partial + preview?: boolean + }): Promise { + const config = commerce.getConfig(cfg) + return {} + } + + return getPage +} diff --git a/framework/commercetools/api/operations/get-product.ts b/framework/commercetools/api/operations/get-product.ts new file mode 100644 index 000000000..589fb4dd3 --- /dev/null +++ b/framework/commercetools/api/operations/get-product.ts @@ -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) { + async function getProduct({ + variables, + config: cfg, + }: { + variables: { + slug?: string + id?: string + locale?: string + } + config?: Partial + preview?: boolean + }): Promise { + 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 +} diff --git a/framework/commercetools/api/operations/get-site-info.ts b/framework/commercetools/api/operations/get-site-info.ts new file mode 100644 index 000000000..d1a113fdf --- /dev/null +++ b/framework/commercetools/api/operations/get-site-info.ts @@ -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) { + async function getSiteInfo({ + query = getAllCategoriesAndBrandsQuery, + variables, + config: cfg, + }: { + query?: string + variables?: any + config?: Partial + preview?: boolean + } = {}): Promise { + 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 +} diff --git a/framework/commercetools/api/operations/login.ts b/framework/commercetools/api/operations/login.ts new file mode 100644 index 000000000..501890992 --- /dev/null +++ b/framework/commercetools/api/operations/login.ts @@ -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) { + async function login(opts: { + variables: T['variables'] + config?: Partial + res: ServerResponse + }): Promise + + async function login( + opts: { + variables: T['variables'] + config?: Partial + res: ServerResponse + } & OperationOptions + ): Promise + + async function login({ + query = '', + variables, + res: response, + config: cfg, + }: { + query?: string + variables: T['variables'] + res: ServerResponse + config?: Partial + }): Promise { + const config = commerce.getConfig(cfg) + return { + result: '', + } + } + + return login +} diff --git a/framework/commercetools/api/utils/errors.ts b/framework/commercetools/api/utils/errors.ts new file mode 100644 index 000000000..49839fa69 --- /dev/null +++ b/framework/commercetools/api/utils/errors.ts @@ -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' + } +} diff --git a/framework/commercetools/api/utils/fetch-graphql-api.ts b/framework/commercetools/api/utils/fetch-graphql-api.ts new file mode 100644 index 000000000..579596aac --- /dev/null +++ b/framework/commercetools/api/utils/fetch-graphql-api.ts @@ -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 diff --git a/framework/commercetools/api/utils/fetch-products.ts b/framework/commercetools/api/utils/fetch-products.ts new file mode 100644 index 000000000..56501199b --- /dev/null +++ b/framework/commercetools/api/utils/fetch-products.ts @@ -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 diff --git a/framework/commercetools/api/utils/fetch.ts b/framework/commercetools/api/utils/fetch.ts new file mode 100644 index 000000000..9d9fff3ed --- /dev/null +++ b/framework/commercetools/api/utils/fetch.ts @@ -0,0 +1,3 @@ +import zeitFetch from '@vercel/fetch' + +export default zeitFetch() diff --git a/framework/commercetools/auth/index.ts b/framework/commercetools/auth/index.ts new file mode 100644 index 000000000..6e321b17f --- /dev/null +++ b/framework/commercetools/auth/index.ts @@ -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' diff --git a/framework/commercetools/auth/use-login.ts b/framework/commercetools/auth/use-login.ts new file mode 100644 index 000000000..e69de29bb diff --git a/framework/commercetools/auth/use-logout.ts b/framework/commercetools/auth/use-logout.ts new file mode 100644 index 000000000..e69de29bb diff --git a/framework/commercetools/auth/use-signup.ts b/framework/commercetools/auth/use-signup.ts new file mode 100644 index 000000000..e69de29bb diff --git a/framework/commercetools/cart/index.ts b/framework/commercetools/cart/index.ts new file mode 100644 index 000000000..0782570b1 --- /dev/null +++ b/framework/commercetools/cart/index.ts @@ -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' diff --git a/framework/commercetools/cart/use-add-item.ts b/framework/commercetools/cart/use-add-item.ts new file mode 100644 index 000000000..e69de29bb diff --git a/framework/commercetools/cart/use-cart.ts b/framework/commercetools/cart/use-cart.ts new file mode 100644 index 000000000..e69de29bb diff --git a/framework/commercetools/cart/use-remove-item.ts b/framework/commercetools/cart/use-remove-item.ts new file mode 100644 index 000000000..e69de29bb diff --git a/framework/commercetools/cart/use-update-item.ts b/framework/commercetools/cart/use-update-item.ts new file mode 100644 index 000000000..e69de29bb diff --git a/framework/commercetools/commerce.config.json b/framework/commercetools/commerce.config.json new file mode 100644 index 000000000..b40393a75 --- /dev/null +++ b/framework/commercetools/commerce.config.json @@ -0,0 +1,9 @@ +{ + "provider": "commercetools", + "features": { + "wishlist": false, + "customer": false, + "cart": false, + "auth": false + } +} diff --git a/framework/commercetools/customer/index.ts b/framework/commercetools/customer/index.ts new file mode 100644 index 000000000..89f003631 --- /dev/null +++ b/framework/commercetools/customer/index.ts @@ -0,0 +1 @@ +// export { default as useCustomer } from './use-customer' diff --git a/framework/commercetools/customer/use-customer.ts b/framework/commercetools/customer/use-customer.ts new file mode 100644 index 000000000..e69de29bb diff --git a/framework/commercetools/fetcher.ts b/framework/commercetools/fetcher.ts new file mode 100644 index 000000000..f8ca0c578 --- /dev/null +++ b/framework/commercetools/fetcher.ts @@ -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 diff --git a/framework/commercetools/index.tsx b/framework/commercetools/index.tsx new file mode 100644 index 000000000..87c7bdae4 --- /dev/null +++ b/framework/commercetools/index.tsx @@ -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 + +export type CommercetoolsProps = { + children?: ReactNode + locale: string +} & CommercetoolsConfig + +export function CommerceProvider({ children, ...config }: CommercetoolsProps) { + return ( + + {children} + + ) +} + +export const useCommerce = () => useCoreCommerce() diff --git a/framework/commercetools/lib/array-to-tree.ts b/framework/commercetools/lib/array-to-tree.ts new file mode 100644 index 000000000..f220a4cbe --- /dev/null +++ b/framework/commercetools/lib/array-to-tree.ts @@ -0,0 +1,70 @@ +export type HasParent = { id: string; parent?: { id: string } | null } +export type TreeNode = T & { + children: Array> + expanded: boolean +} +export type RootNode = { + id?: string + children: Array> +} + +export function arrayToTree( + nodes: T[], + currentState?: RootNode +): RootNode { + const topLevelNodes: Array> = [] + const mappedArr: { [id: string]: TreeNode } = {} + 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( + tree?: RootNode +): Map> { + const nodeMap = new Map>() + function visit(node: TreeNode) { + nodeMap.set(node.id, node) + node.children.forEach(visit) + } + if (tree) { + visit(tree as TreeNode) + } + return nodeMap +} diff --git a/framework/commercetools/lib/get-slug.ts b/framework/commercetools/lib/get-slug.ts new file mode 100644 index 000000000..329c5a27e --- /dev/null +++ b/framework/commercetools/lib/get-slug.ts @@ -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 diff --git a/framework/commercetools/lib/normalize.ts b/framework/commercetools/lib/normalize.ts new file mode 100644 index 000000000..da3118adf --- /dev/null +++ b/framework/commercetools/lib/normalize.ts @@ -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, + } +} diff --git a/framework/commercetools/next.config.js b/framework/commercetools/next.config.js new file mode 100644 index 000000000..06887bece --- /dev/null +++ b/framework/commercetools/next.config.js @@ -0,0 +1,8 @@ +const commerce = require('./commerce.config.json') + +module.exports = { + commerce, + images: { + domains: ['s3-eu-west-1.amazonaws.com'], + }, +} diff --git a/framework/commercetools/product/index.ts b/framework/commercetools/product/index.ts new file mode 100644 index 000000000..426a3edcd --- /dev/null +++ b/framework/commercetools/product/index.ts @@ -0,0 +1,2 @@ +export { default as usePrice } from './use-price' +export { default as useSearch } from './use-search' diff --git a/framework/commercetools/product/use-price.ts b/framework/commercetools/product/use-price.ts new file mode 100644 index 000000000..0174faf5e --- /dev/null +++ b/framework/commercetools/product/use-price.ts @@ -0,0 +1,2 @@ +export * from '@commerce/product/use-price' +export { default } from '@commerce/product/use-price' diff --git a/framework/commercetools/product/use-search.ts b/framework/commercetools/product/use-search.ts new file mode 100644 index 000000000..873974bb5 --- /dev/null +++ b/framework/commercetools/product/use-search.ts @@ -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 + +export type SearchProductsInput = { + search?: string + categoryId?: string + brandId?: string + sort?: string + locale?: string +} + +export type SearchProductsData = { + products: Product[] + found: boolean +} + +export const handler: SWRHook = { + 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, + }, + }) + }, +} diff --git a/framework/commercetools/provider.ts b/framework/commercetools/provider.ts new file mode 100644 index 000000000..525fd7549 --- /dev/null +++ b/framework/commercetools/provider.ts @@ -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 diff --git a/framework/commercetools/types/cart.ts b/framework/commercetools/types/cart.ts new file mode 100644 index 000000000..ef27465f2 --- /dev/null +++ b/framework/commercetools/types/cart.ts @@ -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 +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 +export type CartHandlers = Core.CartHandlers +export type GetCartHandler = CartHandlers['getCart'] +export type AddItemHandler = CartHandlers['addItem'] +export type UpdateItemHandler = CartHandlers['updateItem'] +export type RemoveItemHandler = CartHandlers['removeItem'] diff --git a/framework/commercetools/types/checkout.ts b/framework/commercetools/types/checkout.ts new file mode 100644 index 000000000..4e2412ef6 --- /dev/null +++ b/framework/commercetools/types/checkout.ts @@ -0,0 +1 @@ +export * from '@commerce/types/checkout' diff --git a/framework/commercetools/types/common.ts b/framework/commercetools/types/common.ts new file mode 100644 index 000000000..b52c33a4d --- /dev/null +++ b/framework/commercetools/types/common.ts @@ -0,0 +1 @@ +export * from '@commerce/types/common' diff --git a/framework/commercetools/types/customer.ts b/framework/commercetools/types/customer.ts new file mode 100644 index 000000000..75cee4531 --- /dev/null +++ b/framework/commercetools/types/customer.ts @@ -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 diff --git a/framework/commercetools/types/index.ts b/framework/commercetools/types/index.ts new file mode 100644 index 000000000..7ab0b7f64 --- /dev/null +++ b/framework/commercetools/types/index.ts @@ -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, +} diff --git a/framework/commercetools/types/login.ts b/framework/commercetools/types/login.ts new file mode 100644 index 000000000..db536076f --- /dev/null +++ b/framework/commercetools/types/login.ts @@ -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 +} diff --git a/framework/commercetools/types/logout.ts b/framework/commercetools/types/logout.ts new file mode 100644 index 000000000..9f0a466af --- /dev/null +++ b/framework/commercetools/types/logout.ts @@ -0,0 +1 @@ +export * from '@commerce/types/logout' diff --git a/framework/commercetools/types/page.ts b/framework/commercetools/types/page.ts new file mode 100644 index 000000000..2bccfade2 --- /dev/null +++ b/framework/commercetools/types/page.ts @@ -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 +export type GetPageOperation = Core.GetPageOperation diff --git a/framework/commercetools/types/product.ts b/framework/commercetools/types/product.ts new file mode 100644 index 000000000..f304e3aa3 --- /dev/null +++ b/framework/commercetools/types/product.ts @@ -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 diff --git a/framework/commercetools/types/signup.ts b/framework/commercetools/types/signup.ts new file mode 100644 index 000000000..58543c6f6 --- /dev/null +++ b/framework/commercetools/types/signup.ts @@ -0,0 +1 @@ +export * from '@commerce/types/signup' diff --git a/framework/commercetools/types/site.ts b/framework/commercetools/types/site.ts new file mode 100644 index 000000000..0fbafdfe9 --- /dev/null +++ b/framework/commercetools/types/site.ts @@ -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 diff --git a/framework/commercetools/types/wishlist.ts b/framework/commercetools/types/wishlist.ts new file mode 100644 index 000000000..1e148b88c --- /dev/null +++ b/framework/commercetools/types/wishlist.ts @@ -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 & { + items?: WishlistItem[] +} + +export type WishlistTypes = { + wishlist: Wishlist + itemBody: Core.WishlistItemBody +} + +export type WishlistSchema = Core.WishlistSchema +export type GetCustomerWishlistOperation = Core.GetCustomerWishlistOperation diff --git a/framework/commercetools/utils/commercetools/index.ts b/framework/commercetools/utils/commercetools/index.ts new file mode 100644 index 000000000..a80b47aea --- /dev/null +++ b/framework/commercetools/utils/commercetools/index.ts @@ -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 = { + requestExecute: apiRoot.withProjectKey({ projectKey }), + } + + return commercetools +} diff --git a/framework/commercetools/utils/queries/get-category.ts b/framework/commercetools/utils/queries/get-category.ts new file mode 100644 index 000000000..b681ac4bc --- /dev/null +++ b/framework/commercetools/utils/queries/get-category.ts @@ -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 + } + } + } + } + } + } + } + } + } +` diff --git a/framework/commercetools/utils/queries/get-product-query.ts b/framework/commercetools/utils/queries/get-product-query.ts new file mode 100644 index 000000000..138a9940e --- /dev/null +++ b/framework/commercetools/utils/queries/get-product-query.ts @@ -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 diff --git a/framework/commercetools/wishlist/index.ts b/framework/commercetools/wishlist/index.ts new file mode 100644 index 000000000..6493beb1b --- /dev/null +++ b/framework/commercetools/wishlist/index.ts @@ -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' diff --git a/framework/commercetools/wishlist/use-add-item.ts b/framework/commercetools/wishlist/use-add-item.ts new file mode 100644 index 000000000..e69de29bb diff --git a/framework/commercetools/wishlist/use-remove-item.ts b/framework/commercetools/wishlist/use-remove-item.ts new file mode 100644 index 000000000..e69de29bb diff --git a/framework/commercetools/wishlist/use-wishlist.ts b/framework/commercetools/wishlist/use-wishlist.ts new file mode 100644 index 000000000..e69de29bb diff --git a/global.d.ts b/global.d.ts index 498a1f9fe..3826995f3 100644 --- a/global.d.ts +++ b/global.d.ts @@ -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' diff --git a/package.json b/package.json index 85daa3158..6bb23b15f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/tsconfig.json b/tsconfig.json index 96e4359e5..1d19b894e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"], diff --git a/yarn.lock b/yarn.lock index 1b1fce30e..d67d19b03 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==