Commerce.js Provider (#548)

* commercejs: Initial commit with basic product list

* ui: Handle no variants on product

* commercejs: Support individual product pages

* commercejs: Use separate sdkFetch function

* commercejs: Show option hex colors based on option name

* commercejs: Support product search and filter

* commercejs: Enable carts feature

* commercejs: Remove unused API endpoints

* commercejs: Fix adding variants to cart

* commercejs: Fix types for update cart hook

* commercejs: Update README

* commercejs: Add sorting to product search

* commercejs: Add generic types to cart actions

* commercejs: Better cart normalization

* commercejs: Provide typing for sdkFetch function

* commercejs: Refactor product search logic

* commercejs: Update commercejs types package and export types from local directory

* commercejs: Remove unused checkout hooks

* commercejs: Enhance fetcher to allow custom API routes

* commercejs: Fix product types

* commercejs: Add checkout functionality

* commercejs: Add commercejs to README list of providers

* commercejs: Add login/logout auth hooks

* commercejs: Adds comment to sdkFetch function

* commercejs: Bring back empty useSignup hook to fix build

* commercejs: Refactor useCheckout hook logic

* commercejs: Add errors to fetcher function if using invalid resource/method

* commercejs: Remove use of hex colors for color variants

* ui: Fix undefined error when no variants

* commercejs: Handle add to cart when no variants

* commercejs: Enable customer auth feature

* commercejs: Rename public key env variable as commercejs

* commercejs: Remove duplicate customer fields

* commercejs: Use variants API to generate product variants

* commercejs: Fetch all products using sort order

* commercejs: Fix use of normalizeProduct function

* commercejs: Disable customer auth

* commercejs: Show selected variant details in cart view

* commercejs: Update to latest commercejs types

* commercejs: Fix login email

* commercejs: Remove unnecessary ts-ignore

* api: Allow parameter to be passed to login API

* api: Allow login handler to accept GET requests

* commercejs: Add login API for login callback email link

* commercejs: Remove unused argument to API

* commercejs: Add hook to fetch logged in customer

* commercejs: Rename token to match SDK name

* commercejs: Enable logout

* commercejs: Fix VERCEL_URL env variable

* commercejs: Fix using vercel deployment url

* commercejs: Add deployment url env vars to templates

* Replace yarn with npm

* commercejs: Allow checkout submit even without card/address details

* ui: Add loading and cart refresh to checkout

* commercejs: Leave link to issue on TODO comment

* Update docs/README/env.template for commercejs provider

* ui: Prevent toggle loading after component unmount

* commercejs: Handle product without images

* ui: Explicity set loading to false after checkout

* Revert "api: Allow parameter to be passed to login API"

This reverts commit c3713ec6e23f1b423a071a31221069995d419486.

* commercejs: Handle login using API redirect

* commercejs: Adds shipping and billing details to checkout data

* commercejs: Fix types for fetcher and submit checkout

* commercejs: Update README with demo url

* commercejs: Update checkout hooks to use checkout context

* commercejs: Update checkout logic to use customer fields

* ui: Clear checkout fields context after checkout

* commercejs: Remove unused clear checkout function

* commercejs: Import constants directly
This commit is contained in:
Chris Vibert 2021-12-16 19:06:52 +00:00 committed by GitHub
parent 3a69dbb5b0
commit d8b42acfa2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
88 changed files with 1706 additions and 13 deletions

View File

@ -1,4 +1,4 @@
# Available providers: local, bigcommerce, shopify, swell, saleor, spree
# Available providers: local, bigcommerce, shopify, swell, saleor, spree, commercejs
COMMERCE_PROVIDER=
BIGCOMMERCE_STOREFRONT_API_URL=
@ -34,3 +34,6 @@ KIBO_SHARED_SECRET=
KIBO_CART_COOKIE=
KIBO_CUSTOMER_COOKIE=
KIBO_API_HOST=
NEXT_PUBLIC_COMMERCEJS_PUBLIC_KEY=
NEXT_PUBLIC_COMMERCEJS_DEPLOYMENT_URL=

View File

@ -14,6 +14,7 @@ Demo live at: [demo.vercel.store](https://demo.vercel.store/)
- Saleor Demo: https://saleor.vercel.store/
- Ordercloud Demo: https://ordercloud.vercel.store/
- Spree Demo: https://spree.vercel.store/
- Commerce.js Demo: https://commercejs.vercel.store/
## Features
@ -29,7 +30,7 @@ Demo live at: [demo.vercel.store](https://demo.vercel.store/)
## Integrations
Next.js Commerce integrates out-of-the-box with BigCommerce, Shopify, Swell, Saleor, Vendure and Spree. We plan to support all major ecommerce backends.
Next.js Commerce integrates out-of-the-box with BigCommerce, Shopify, Swell, Saleor, Vendure, Spree and Commerce.js. We plan to support all major ecommerce backends.
## Considerations
@ -155,4 +156,4 @@ After Email confirmation, Checkout should be manually enabled through BigCommerc
<br>
<br>
BigCommerce team has been notified and they plan to add more details about this subject.
</details>
</details>

View File

@ -1,5 +1,5 @@
import Link from 'next/link'
import { FC } from 'react'
import { FC, useState } from 'react'
import CartItem from '@components/cart/CartItem'
import { Button, Text } from '@components/ui'
import { useUI } from '@components/ui/context'
@ -10,18 +10,29 @@ import useCheckout from '@framework/checkout/use-checkout'
import ShippingWidget from '../ShippingWidget'
import PaymentWidget from '../PaymentWidget'
import s from './CheckoutSidebarView.module.css'
import { useCheckoutContext } from '../context'
const CheckoutSidebarView: FC = () => {
const [loadingSubmit, setLoadingSubmit] = useState(false)
const { setSidebarView, closeSidebar } = useUI()
const { data: cartData } = useCart()
const { data: cartData, revalidate: refreshCart } = useCart()
const { data: checkoutData, submit: onCheckout } = useCheckout()
const { clearCheckoutFields } = useCheckoutContext()
async function handleSubmit(event: React.ChangeEvent<HTMLFormElement>) {
event.preventDefault()
try {
setLoadingSubmit(true)
event.preventDefault()
await onCheckout()
closeSidebar()
await onCheckout()
clearCheckoutFields()
setLoadingSubmit(false)
refreshCart()
closeSidebar()
} catch {
// TODO - handle error UI here.
setLoadingSubmit(false)
}
}
const { price: subTotal } = usePrice(
@ -98,6 +109,7 @@ const CheckoutSidebarView: FC = () => {
type="submit"
width="100%"
disabled={!checkoutData?.hasPayment || !checkoutData?.hasShipping}
loading={loadingSubmit}
>
Confirm Purchase
</Button>

View File

@ -31,7 +31,7 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
try {
await addItem({
productId: String(product.id),
variantId: String(variant ? variant.id : product.variants[0].id),
variantId: String(variant ? variant.id : product.variants[0]?.id),
})
openSidebar()
setLoading(false)

View File

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

View File

@ -12,6 +12,7 @@ const loginEndpoint: GetAPISchema<
if (
!isAllowedOperation(req, res, {
POST: handlers['login'],
GET: handlers['login'],
})
) {
return

View File

@ -16,7 +16,8 @@ const PROVIDERS = [
'vendure',
'ordercloud',
'kibocommerce',
'spree'
'spree',
'commercejs',
]
function getProviderName() {

View File

@ -7,6 +7,7 @@ A commerce provider is a headless e-commerce platform that integrates with the [
- BigCommerce ([framework/bigcommerce](../bigcommerce))
- Saleor ([framework/saleor](../saleor))
- Shopify ([framework/shopify](../shopify))
- Commerce.js ([framework/commercejs](../commercejs))
Adding a commerce provider means adding a new folder in `framework` with a folder structure like the next one:

View File

@ -0,0 +1,7 @@
COMMERCE_PROVIDER=commercejs
# Public key for your Commerce.js account
NEXT_PUBLIC_COMMERCEJS_PUBLIC_KEY=
# The URL for the current deployment, optional but should be used for production deployments
NEXT_PUBLIC_COMMERCEJS_DEPLOYMENT_URL=

View File

@ -0,0 +1,13 @@
# [Commerce.js](https://commercejs.com/) Provider
**Demo:** https://commercejs.vercel.store/
To use this provider you must have a [Commerce.js account](https://commercejs.com/) and you should add some products in the Commerce.js dashboard.
Next, copy the `.env.template` file in this directory to `.env.local` in the main directory (which will be ignored by Git):
```bash
cp framework/commercejs/.env.template .env.local
```
Then, set the environment variables in `.env.local` to match the ones from your store. You'll need your Commerce.js public API key, which can be found in your Commerce.js dashboard in the `Developer -> API keys` section.

View File

@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@ -0,0 +1,23 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import checkoutEndpoint from '@commerce/api/endpoints/checkout'
import type { CheckoutSchema } from '../../../types/checkout'
import type { CommercejsAPI } from '../..'
import submitCheckout from './submit-checkout'
import getCheckout from './get-checkout'
export type CheckoutAPI = GetAPISchema<CommercejsAPI, CheckoutSchema>
export type CheckoutEndpoint = CheckoutAPI['endpoint']
export const handlers: CheckoutEndpoint['handlers'] = {
submitCheckout,
getCheckout,
}
const checkoutApi = createEndpoint<CheckoutAPI>({
handler: checkoutEndpoint,
handlers,
})
export default checkoutApi

View File

@ -0,0 +1,44 @@
import type { CardFields } from '@commerce/types/customer/card'
import type { AddressFields } from '@commerce/types/customer/address'
import type { CheckoutEndpoint } from '.'
import sdkFetcherFunction from '../../utils/sdk-fetch'
import { normalizeTestCheckout } from '../../../utils/normalize-checkout'
const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({
res,
body: { item, cartId },
config: { sdkFetch },
}) => {
const sdkFetcher: typeof sdkFetcherFunction = sdkFetch
// Generate a checkout token
const { id: checkoutToken } = await sdkFetcher(
'checkout',
'generateTokenFrom',
'cart',
cartId
)
const shippingMethods = await sdkFetcher(
'checkout',
'getShippingOptions',
checkoutToken,
{
country: 'US',
}
)
const shippingMethodToUse = shippingMethods?.[0]?.id || ''
const checkoutData = normalizeTestCheckout({
paymentInfo: item?.card as CardFields,
shippingInfo: item?.address as AddressFields,
shippingOption: shippingMethodToUse,
})
// Capture the order
await sdkFetcher('checkout', 'capture', checkoutToken, checkoutData)
res.status(200).json({ data: null, errors: [] })
}
export default submitCheckout

View File

@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@ -0,0 +1,18 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import loginEndpoint from '@commerce/api/endpoints/login'
import type { LoginSchema } from '../../../types/login'
import type { CommercejsAPI } from '../..'
import login from './login'
export type LoginAPI = GetAPISchema<CommercejsAPI, LoginSchema>
export type LoginEndpoint = LoginAPI['endpoint']
export const handlers: LoginEndpoint['handlers'] = { login }
const loginApi = createEndpoint<LoginAPI>({
handler: loginEndpoint,
handlers,
})
export default loginApi

View File

@ -0,0 +1,33 @@
import { serialize } from 'cookie'
import sdkFetcherFunction from '../../utils/sdk-fetch'
import { getDeploymentUrl } from '../../../utils/get-deployment-url'
import type { LoginEndpoint } from '.'
const login: LoginEndpoint['handlers']['login'] = async ({
req,
res,
config: { sdkFetch, customerCookie },
}) => {
const sdkFetcher: typeof sdkFetcherFunction = sdkFetch
const redirectUrl = getDeploymentUrl()
try {
const loginToken = req.query?.token as string
if (!loginToken) {
res.redirect(redirectUrl)
}
const { jwt } = await sdkFetcher('customer', 'getToken', loginToken, false)
res.setHeader(
'Set-Cookie',
serialize(customerCookie, jwt, {
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24,
path: '/',
})
)
res.redirect(redirectUrl)
} catch {
res.redirect(redirectUrl)
}
}
export default login

View File

@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@ -0,0 +1,46 @@
import type { CommerceAPI, CommerceAPIConfig } from '@commerce/api'
import { getCommerceApi as commerceApi } from '@commerce/api'
import getAllPages from './operations/get-all-pages'
import getPage from './operations/get-page'
import getSiteInfo from './operations/get-site-info'
import getAllProductPaths from './operations/get-all-product-paths'
import getAllProducts from './operations/get-all-products'
import getProduct from './operations/get-product'
import sdkFetch from './utils/sdk-fetch'
import createGraphqlFetcher from './utils/graphql-fetch'
import { API_URL, CART_COOKIE, CUSTOMER_COOKIE } from '../constants'
export interface CommercejsConfig extends CommerceAPIConfig {
sdkFetch: typeof sdkFetch
}
const config: CommercejsConfig = {
commerceUrl: API_URL,
cartCookie: CART_COOKIE,
cartCookieMaxAge: 2592000,
customerCookie: CUSTOMER_COOKIE,
apiToken: '',
fetch: createGraphqlFetcher(() => getCommerceApi().getConfig()),
sdkFetch,
}
const operations = {
getAllPages,
getPage,
getSiteInfo,
getAllProductPaths,
getAllProducts,
getProduct,
}
export const provider = { config, operations }
export type Provider = typeof provider
export type CommercejsAPI<P extends Provider = Provider> = CommerceAPI<P | any>
export function getCommerceApi<P extends Provider>(
customProvider: P = provider as any
): CommercejsAPI<P> {
return commerceApi(customProvider as any)
}

View File

@ -0,0 +1,21 @@
import type { CommercejsConfig } from '..'
import { GetAllPagesOperation } from '../../types/page'
export type Page = { url: string }
export type GetAllPagesResult = { pages: Page[] }
export default function getAllPagesOperation() {
async function getAllPages<T extends GetAllPagesOperation>({
config,
preview,
}: {
url?: string
config?: Partial<CommercejsConfig>
preview?: boolean
} = {}): Promise<T['data']> {
return Promise.resolve({
pages: [],
})
}
return getAllPages
}

View File

@ -0,0 +1,35 @@
import type { OperationContext } from '@commerce/api/operations'
import type {
GetAllProductPathsOperation,
CommercejsProduct,
} from '../../types/product'
import type { CommercejsConfig, Provider } from '..'
export type GetAllProductPathsResult = {
products: Array<{ path: string }>
}
export default function getAllProductPathsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProductPaths<T extends GetAllProductPathsOperation>({
config,
}: {
config?: Partial<CommercejsConfig>
} = {}): Promise<T['data']> {
const { sdkFetch } = commerce.getConfig(config)
const { data } = await sdkFetch('products', 'list')
// Match a path for every product retrieved
const productPaths = data.map(({ permalink }: CommercejsProduct) => ({
path: `/${permalink}`,
}))
return {
products: productPaths,
}
}
return getAllProductPaths
}

View File

@ -0,0 +1,29 @@
import type { OperationContext } from '@commerce/api/operations'
import type { GetAllProductsOperation } from '../../types/product'
import type { CommercejsConfig, Provider } from '../index'
import { normalizeProduct } from '../../utils/normalize-product'
export default function getAllProductsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProducts<T extends GetAllProductsOperation>({
config,
}: {
config?: Partial<CommercejsConfig>
} = {}): Promise<T['data']> {
const { sdkFetch } = commerce.getConfig(config)
const { data } = await sdkFetch('products', 'list', {
sortBy: 'sort_order',
})
const productsFormatted =
data?.map((product) => normalizeProduct(product)) || []
return {
products: productsFormatted,
}
}
return getAllProducts
}

View File

@ -0,0 +1,15 @@
import { GetPageOperation } from '../../types/page'
export type Page = any
export type GetPageResult = { page?: Page }
export type PageVariables = {
id: number
}
export default function getPageOperation() {
async function getPage<T extends GetPageOperation>(): Promise<T['data']> {
return Promise.resolve({})
}
return getPage
}

View File

@ -0,0 +1,44 @@
import type { OperationContext } from '@commerce/api/operations'
import type { GetProductOperation } from '../../types/product'
import type { CommercejsConfig, Provider } from '../index'
import { normalizeProduct } from '../../utils/normalize-product'
export default function getProductOperation({
commerce,
}: OperationContext<Provider>) {
async function getProduct<T extends GetProductOperation>({
config,
variables,
}: {
query?: string
variables?: T['variables']
config?: Partial<CommercejsConfig>
preview?: boolean
} = {}): Promise<T['data']> {
const { sdkFetch } = commerce.getConfig(config)
// Fetch a product by its permalink.
const product = await sdkFetch(
'products',
'retrieve',
variables?.slug || '',
{
type: 'permalink',
}
)
const { data: variants } = await sdkFetch(
'products',
'getVariants',
product.id
)
const productFormatted = normalizeProduct(product, variants)
return {
product: productFormatted,
}
}
return getProduct
}

View File

@ -0,0 +1,36 @@
import type { OperationContext } from '@commerce/api/operations'
import type { Category, GetSiteInfoOperation } from '../../types/site'
import { normalizeCategory } from '../../utils/normalize-category'
import type { CommercejsConfig, Provider } from '../index'
export type GetSiteInfoResult<
T extends { categories: any[]; brands: any[] } = {
categories: Category[]
brands: any[]
}
> = T
export default function getSiteInfoOperation({
commerce,
}: OperationContext<Provider>) {
async function getSiteInfo<T extends GetSiteInfoOperation>({
config,
}: {
query?: string
variables?: any
config?: Partial<CommercejsConfig>
preview?: boolean
} = {}): Promise<T['data']> {
const { sdkFetch } = commerce.getConfig(config)
const { data: categories } = await sdkFetch('categories', 'list')
const formattedCategories = categories.map(normalizeCategory)
return {
categories: formattedCategories,
brands: [],
}
}
return getSiteInfo
}

View File

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

View File

@ -0,0 +1,14 @@
import type { GraphQLFetcher } from '@commerce/api'
import type { CommercejsConfig } from '../'
import { FetcherError } from '@commerce/utils/errors'
const fetchGraphqlApi: (getConfig: () => CommercejsConfig) => GraphQLFetcher =
() => async () => {
throw new FetcherError({
errors: [{ message: 'GraphQL fetch is not implemented' }],
status: 500,
})
}
export default fetchGraphqlApi

View File

@ -0,0 +1,19 @@
import { commerce } from '../../lib/commercejs'
import Commerce from '@chec/commerce.js'
type MethodKeys<T> = {
[K in keyof T]: T[K] extends (...args: any) => infer R ? K : never
}[keyof T]
// Calls the relevant Commerce.js SDK method based on resource and method arguments.
export default async function sdkFetch<
Resource extends keyof Commerce,
Method extends MethodKeys<Commerce[Resource]>
>(
resource: Resource,
method: Method,
...variables: Parameters<Commerce[Resource][Method]>
): Promise<ReturnType<Commerce[Resource][Method]>> {
const data = await commerce[resource][method](...variables)
return data
}

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,34 @@
import { useCallback } from 'react'
import { MutationHook } from '@commerce/utils/types'
import useLogin, { UseLogin } from '@commerce/auth/use-login'
import type { LoginHook } from '@commerce/types/login'
import { getDeploymentUrl } from '../utils/get-deployment-url'
export default useLogin as UseLogin<typeof handler>
const getLoginCallbackUrl = () => {
const baseUrl = getDeploymentUrl()
const API_ROUTE_PATH = 'api/login'
return `${baseUrl}/${API_ROUTE_PATH}`
}
export const handler: MutationHook<LoginHook> = {
fetchOptions: {
query: 'customer',
method: 'login',
},
async fetcher({ input, options: { query, method }, fetch }) {
await fetch({
query,
method,
variables: [input.email, getLoginCallbackUrl()],
})
return null
},
useHook: ({ fetch }) =>
function useHook() {
return useCallback(async function login(input) {
return fetch({ input })
}, [])
},
}

View File

@ -0,0 +1,27 @@
import { useCallback } from 'react'
import Cookies from 'js-cookie'
import { MutationHook } from '@commerce/utils/types'
import useLogout, { UseLogout } from '@commerce/auth/use-logout'
import type { LogoutHook } from '@commerce/types/logout'
import useCustomer from '../customer/use-customer'
import { CUSTOMER_COOKIE } from '../constants'
export default useLogout as UseLogout<typeof handler>
export const handler: MutationHook<LogoutHook> = {
fetchOptions: {
query: '_',
method: '_',
},
useHook: () => () => {
const { mutate } = useCustomer()
return useCallback(
async function logout() {
Cookies.remove(CUSTOMER_COOKIE)
await mutate(null, false)
return null
},
[mutate]
)
},
}

View File

@ -0,0 +1,17 @@
import { MutationHook } from '@commerce/utils/types'
import useSignup, { UseSignup } from '@commerce/auth/use-signup'
export default useSignup as UseSignup<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher() {
return null
},
useHook:
({ fetch }) =>
() =>
() => {},
}

View File

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

View File

@ -0,0 +1,45 @@
import type { AddItemHook } from '@commerce/types/cart'
import type { MutationHook } from '@commerce/utils/types'
import { useCallback } from 'react'
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
import type { CommercejsCart } from '../types/cart'
import { normalizeCart } from '../utils/normalize-cart'
import useCart from './use-cart'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = {
fetchOptions: {
query: 'cart',
method: 'add',
},
async fetcher({ input: item, options, fetch }) {
// Frontend stringifies variantId even if undefined.
const hasVariant = !item.variantId || item.variantId !== 'undefined'
const variables = [item.productId, item?.quantity || 1]
if (hasVariant) {
variables.push(item.variantId)
}
const { cart } = await fetch<{ cart: CommercejsCart }>({
query: options.query,
method: options.method,
variables,
})
return normalizeCart(cart)
},
useHook: ({ fetch }) =>
function useHook() {
const { mutate } = useCart()
return useCallback(
async function addItem(input) {
const cart = await fetch({ input })
await mutate(cart, false)
return cart
},
[mutate]
)
},
}

View File

@ -0,0 +1,41 @@
import { useMemo } from 'react'
import type { GetCartHook } from '@commerce/types/cart'
import { SWRHook } from '@commerce/utils/types'
import useCart, { UseCart } from '@commerce/cart/use-cart'
import type { CommercejsCart } from '../types/cart'
import { normalizeCart } from '../utils/normalize-cart'
export default useCart as UseCart<typeof handler>
export const handler: SWRHook<GetCartHook> = {
fetchOptions: {
query: 'cart',
method: 'retrieve',
},
async fetcher({ options, fetch }) {
const cart = await fetch<CommercejsCart>({
query: options.query,
method: options.method,
})
return normalizeCart(cart)
},
useHook: ({ useData }) =>
function useHook(input) {
const response = useData({
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
})
return useMemo(
() =>
Object.create(response, {
isEmpty: {
get() {
return (response.data?.lineItems?.length ?? 0) <= 0
},
enumerable: true,
},
}),
[response]
)
},
}

View File

@ -0,0 +1,36 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import type { RemoveItemHook } from '@commerce/types/cart'
import useRemoveItem, { UseRemoveItem } from '@commerce/cart/use-remove-item'
import type { CommercejsCart } from '../types/cart'
import { normalizeCart } from '../utils/normalize-cart'
import useCart from './use-cart'
export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler: MutationHook<RemoveItemHook> = {
fetchOptions: {
query: 'cart',
method: 'remove',
},
async fetcher({ input, options, fetch }) {
const { cart } = await fetch<{ cart: CommercejsCart }>({
query: options.query,
method: options.method,
variables: input.itemId,
})
return normalizeCart(cart)
},
useHook: ({ fetch }) =>
function useHook() {
const { mutate } = useCart()
return useCallback(
async function removeItem(input) {
const cart = await fetch({ input: { itemId: input.id } })
await mutate(cart, false)
return cart
},
[mutate]
)
},
}

View File

@ -0,0 +1,76 @@
import type { UpdateItemHook, LineItem } from '@commerce/types/cart'
import type {
HookFetcherContext,
MutationHookContext,
} from '@commerce/utils/types'
import { ValidationError } from '@commerce/utils/errors'
import debounce from 'lodash.debounce'
import { useCallback } from 'react'
import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item'
import type { CommercejsCart } from '../types/cart'
import { normalizeCart } from '../utils/normalize-cart'
import useCart from './use-cart'
export default useUpdateItem as UseUpdateItem<typeof handler>
export type UpdateItemActionInput<T = any> = T extends LineItem
? Partial<UpdateItemHook['actionInput']>
: UpdateItemHook['actionInput']
export const handler = {
fetchOptions: {
query: 'cart',
method: 'update',
},
async fetcher({ input, options, fetch }: HookFetcherContext<UpdateItemHook>) {
const variables = [input.itemId, { quantity: input.item.quantity }]
const { cart } = await fetch<{ cart: CommercejsCart }>({
query: options.query,
method: options.method,
variables,
})
return normalizeCart(cart)
},
useHook:
({ fetch }: MutationHookContext<UpdateItemHook>) =>
<T extends LineItem | undefined = undefined>(
ctx: {
item?: T
wait?: number
} = {}
) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { mutate } = useCart() as any
const { item } = ctx
// eslint-disable-next-line react-hooks/rules-of-hooks
return useCallback(
debounce(async (input: UpdateItemActionInput<T>) => {
const itemId = input.id ?? item?.id
const productId = input.productId ?? item?.productId
const variantId = input.productId ?? item?.variantId
const quantity = input?.quantity ?? item?.quantity
if (!itemId || !productId || !variantId) {
throw new ValidationError({
message: 'Invalid input for updating cart item',
})
}
const cart = await fetch({
input: {
itemId,
item: {
quantity,
productId,
variantId,
},
},
})
await mutate(cart, false)
return cart
}, ctx.wait ?? 500),
[mutate, item]
)
},
}

View File

@ -0,0 +1,2 @@
export { default as useSubmitCheckout } from './use-submit-checkout'
export { default as useCheckout } from './use-checkout'

View File

@ -0,0 +1,52 @@
import type { GetCheckoutHook } from '@commerce/types/checkout'
import { useMemo } from 'react'
import { SWRHook } from '@commerce/utils/types'
import useCheckout, { UseCheckout } from '@commerce/checkout/use-checkout'
import useSubmitCheckout from './use-submit-checkout'
import { useCheckoutContext } from '@components/checkout/context'
export default useCheckout as UseCheckout<typeof handler>
export const handler: SWRHook<GetCheckoutHook> = {
fetchOptions: {
query: '_',
method: '_',
},
useHook: () =>
function useHook() {
const { cardFields, addressFields } = useCheckoutContext()
const submit = useSubmitCheckout()
// Basic validation - check that at least one field has a value.
const hasEnteredCard = Object.values(cardFields).some(
(fieldValue) => !!fieldValue
)
const hasEnteredAddress = Object.values(addressFields).some(
(fieldValue) => !!fieldValue
)
const response = useMemo(
() => ({
data: {
hasPayment: hasEnteredCard,
hasShipping: hasEnteredAddress,
},
}),
[hasEnteredCard, hasEnteredAddress]
)
return useMemo(
() =>
Object.create(response, {
submit: {
get() {
return submit
},
enumerable: true,
},
}),
[submit, response]
)
},
}

View File

@ -0,0 +1,38 @@
import type { SubmitCheckoutHook } from '@commerce/types/checkout'
import type { MutationHook } from '@commerce/utils/types'
import { useCallback } from 'react'
import useSubmitCheckout, {
UseSubmitCheckout,
} from '@commerce/checkout/use-submit-checkout'
import { useCheckoutContext } from '@components/checkout/context'
export default useSubmitCheckout as UseSubmitCheckout<typeof handler>
export const handler: MutationHook<SubmitCheckoutHook> = {
fetchOptions: {
url: '/api/checkout',
method: 'POST',
},
async fetcher({ input: item, options, fetch }) {
const data = await fetch({
...options,
body: { item },
})
return data
},
useHook: ({ fetch }) =>
function useHook() {
const { cardFields, addressFields } = useCheckoutContext()
return useCallback(
async function onSubmitCheckout(input) {
const data = await fetch({
input: { card: cardFields, address: addressFields },
})
return data
},
[cardFields, addressFields]
)
},
}

View File

@ -0,0 +1,10 @@
{
"provider": "commercejs",
"features": {
"cart": true,
"search": true,
"customCheckout": true,
"customerAuth": true,
"wishlist": false
}
}

View File

@ -0,0 +1,4 @@
export const CART_COOKIE = 'commercejs_cart_id'
export const CUSTOMER_COOKIE = 'commercejs_customer_token'
export const API_URL = 'https://api.chec.io/v1'
export const LOCALE = 'en-us'

View File

@ -0,0 +1,2 @@
export { default as useAddresses } from './use-addresses'
export { default as useAddItem } from './use-add-item'

View File

@ -0,0 +1,25 @@
import type { AddItemHook } from '@commerce/types/customer/address'
import type { MutationHook } from '@commerce/utils/types'
import { useCallback } from 'react'
import useAddItem, { UseAddItem } from '@commerce/customer/address/use-add-item'
import { useCheckoutContext } from '@components/checkout/context'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = {
fetchOptions: {
query: '_',
method: '_',
},
useHook: () =>
function useHook() {
const { setAddressFields } = useCheckoutContext()
return useCallback(
async function addItem(input) {
setAddressFields(input)
return undefined
},
[setAddressFields]
)
},
}

View File

@ -0,0 +1,34 @@
import type { GetAddressesHook } from '@commerce/types/customer/address'
import { useMemo } from 'react'
import { SWRHook } from '@commerce/utils/types'
import useAddresses, {
UseAddresses,
} from '@commerce/customer/address/use-addresses'
export default useAddresses as UseAddresses<typeof handler>
export const handler: SWRHook<GetAddressesHook> = {
fetchOptions: {
url: '_',
method: '_',
},
useHook: () =>
function useHook() {
return useMemo(
() =>
Object.create(
{},
{
isEmpty: {
get() {
return true
},
enumerable: true,
},
}
),
[]
)
},
}

View File

@ -0,0 +1,2 @@
export { default as useCards } from './use-cards'
export { default as useAddItem } from './use-add-item'

View File

@ -0,0 +1,25 @@
import type { AddItemHook } from '@commerce/types/customer/card'
import type { MutationHook } from '@commerce/utils/types'
import { useCallback } from 'react'
import useAddItem, { UseAddItem } from '@commerce/customer/card/use-add-item'
import { useCheckoutContext } from '@components/checkout/context'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = {
fetchOptions: {
url: '_',
method: '_',
},
useHook: () =>
function useHook() {
const { setCardFields } = useCheckoutContext()
return useCallback(
async function addItem(input) {
setCardFields(input)
return undefined
},
[setCardFields]
)
},
}

View File

@ -0,0 +1,31 @@
import type { GetCardsHook } from '@commerce/types/customer/card'
import { useMemo } from 'react'
import { SWRHook } from '@commerce/utils/types'
import useCard, { UseCards } from '@commerce/customer/card/use-cards'
export default useCard as UseCards<typeof handler>
export const handler: SWRHook<GetCardsHook> = {
fetchOptions: {
query: '_',
method: '_',
},
useHook: () =>
function useHook() {
return useMemo(
() =>
Object.create(
{},
{
isEmpty: {
get() {
return true
},
enumerable: true,
},
}
),
[]
)
},
}

View File

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

View File

@ -0,0 +1,44 @@
import Cookies from 'js-cookie'
import { decode } from 'jsonwebtoken'
import { SWRHook } from '@commerce/utils/types'
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
import { CUSTOMER_COOKIE, API_URL } from '../constants'
import type { CustomerHook } from '../types/customer'
export default useCustomer as UseCustomer<typeof handler>
export const handler: SWRHook<CustomerHook> = {
fetchOptions: {
query: 'customer',
method: '_request',
},
async fetcher({ options, fetch }) {
const token = Cookies.get(CUSTOMER_COOKIE)
if (!token) {
return null
}
const decodedToken = decode(token) as { cid: string }
const customer = await fetch({
query: options.query,
method: options.method,
variables: [
`${API_URL}/customers/${decodedToken.cid}`,
'get',
null,
{},
token,
],
})
return customer
},
useHook:
({ useData }) =>
(input) => {
return useData({
swrOptions: {
revalidateOnFocus: false,
...input?.swrOptions,
},
})
},
}

View File

@ -0,0 +1,61 @@
import { commerce } from './lib/commercejs'
import type { Fetcher } from '@commerce/utils/types'
import { FetcherError } from '@commerce/utils/errors'
function isValidSDKQuery(query?: string): query is keyof typeof commerce {
if (!query) return false
return query in commerce
}
// Fetches from an API route within /api/endpoints directory
const customFetcher: Fetcher = async ({ method, url, body }) => {
const response = await fetch(url!, {
method,
body: body ? JSON.stringify(body) : undefined,
headers: {
'Content-Type': 'application/json',
},
})
.then((response) => response.json())
.then((response) => response.data)
return response
}
const fetcher: Fetcher = async ({ url, query, method, variables, body }) => {
// If a URL is passed, it means that the fetch needs to be passed on to a custom API route.
const isCustomFetch = !!url
if (isCustomFetch) {
const data = await customFetcher({ url, method, body })
return data
}
// Fetch using the Commerce.js SDK, but make sure that it's a valid method.
if (!isValidSDKQuery(query)) {
throw new FetcherError({
errors: [
{ message: `Query ${query} does not exist on Commerce.js SDK.` },
],
status: 400,
})
}
const resource: any = commerce[query]
if (!method || !resource[method]) {
throw new FetcherError({
errors: [
{
message: `Method ${method} does not exist on Commerce.js SDK ${query} resource.`,
},
],
status: 400,
})
}
const variablesArgument = Array.isArray(variables) ? variables : [variables]
const data = await resource[method](...variablesArgument)
return data
}
export default fetcher

View File

@ -0,0 +1,9 @@
import { commercejsProvider, CommercejsProvider } from './provider'
import { getCommerceProvider, useCommerce as useCoreCommerce } from '@commerce'
export { commercejsProvider }
export type { CommercejsProvider }
export const CommerceProvider = getCommerceProvider(commercejsProvider)
export const useCommerce = () => useCoreCommerce()

View File

@ -0,0 +1,11 @@
import Commerce from '@chec/commerce.js'
const commercejsPublicKey = process.env
.NEXT_PUBLIC_COMMERCEJS_PUBLIC_KEY as string
const devEnvironment = process.env.NODE_ENV === 'development'
if (devEnvironment && !commercejsPublicKey) {
throw Error('A Commerce.js public API key must be provided')
}
export const commerce = new Commerce(commercejsPublicKey, devEnvironment)

View File

@ -0,0 +1,16 @@
const commerce = require('./commerce.config.json')
module.exports = {
commerce,
images: {
domains: ['cdn.chec.io'],
},
rewrites() {
return [
{
source: '/api/login/:token',
destination: '/api/login?token=:token',
},
]
},
}

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,53 @@
import { SWRHook } from '@commerce/utils/types'
import useSearch, { UseSearch } from '@commerce/product/use-search'
import { SearchProductsHook } from '@commerce/types/product'
import type { CommercejsProduct } from '../types/product'
import { getProductSearchVariables } from '../utils/product-search'
import { normalizeProduct } from '../utils/normalize-product'
export default useSearch as UseSearch<typeof handler>
export const handler: SWRHook<SearchProductsHook> = {
fetchOptions: {
query: 'products',
method: 'list',
},
async fetcher({ input, options, fetch }) {
const { data, meta } = await fetch<{
data: CommercejsProduct[]
meta: {
pagination: {
total: number
}
}
}>({
query: options.query,
method: options.method,
variables: getProductSearchVariables(input),
})
const formattedProducts =
data?.map((product) => normalizeProduct(product)) || []
return {
products: formattedProducts,
found: meta.pagination.total > 0,
}
},
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,55 @@
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 { handler as useCheckout } from './checkout/use-checkout'
import { handler as useSubmitCheckout } from './checkout/use-submit-checkout'
import { handler as useCards } from './customer/card/use-cards'
import { handler as useAddCardItem } from './customer/card/use-add-item'
import { handler as useAddresses } from './customer/address/use-addresses'
import { handler as useAddAddressItem } from './customer/address/use-add-item'
import { CART_COOKIE, CUSTOMER_COOKIE, LOCALE } from './constants'
import { default as sdkFetcher } from './fetcher'
export const commercejsProvider = {
locale: LOCALE,
cartCookie: CART_COOKIE,
customerCookie: CUSTOMER_COOKIE,
fetcher: sdkFetcher,
cart: {
useCart,
useAddItem,
useUpdateItem,
useRemoveItem,
},
checkout: {
useCheckout,
useSubmitCheckout,
},
customer: {
useCustomer,
card: {
useCards,
useAddItem: useAddCardItem,
},
address: {
useAddresses,
useAddItem: useAddAddressItem,
},
},
products: { useSearch },
auth: { useLogin, useLogout, useSignup },
}
export type CommercejsProvider = typeof commercejsProvider

View File

@ -0,0 +1,4 @@
export * from '@commerce/types/cart'
export type { Cart as CommercejsCart } from '@chec/commerce.js/types/cart'
export type { LineItem as CommercejsLineItem } from '@chec/commerce.js/types/line-item'

View File

@ -0,0 +1,3 @@
export * from '@commerce/types/checkout'
export type { CheckoutCapture as CommercejsCheckoutCapture } from '@chec/commerce.js/types/checkout-capture'

View File

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

View File

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

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,9 @@
import { LoginBody, LoginTypes } from '@commerce/types/login'
export * from '@commerce/types/login'
export type LoginHook<T extends LoginTypes = LoginTypes> = {
data: null
actionInput: LoginBody
fetcherInput: LoginBody
body: T['body']
}

View File

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

View File

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

View File

@ -0,0 +1,4 @@
export * from '@commerce/types/product'
export type { Product as CommercejsProduct } from '@chec/commerce.js/types/product'
export type { Variant as CommercejsVariant } from '@chec/commerce.js/types/variant'

View File

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

View File

@ -0,0 +1,3 @@
export * from '@commerce/types/site'
export type { Category as CommercejsCategory } from '@chec/commerce.js/types/category'

View File

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

View File

@ -0,0 +1,12 @@
export const getDeploymentUrl = () => {
// Custom environment variable.
if (process.env.NEXT_PUBLIC_COMMERCEJS_DEPLOYMENT_URL) {
return process.env.NEXT_PUBLIC_COMMERCEJS_DEPLOYMENT_URL
}
// Automatic Vercel deployment URL.
if (process.env.NEXT_PUBLIC_VERCEL_URL) {
return `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
}
// Assume local development.
return 'http://localhost:3000'
}

View File

@ -0,0 +1,74 @@
import type {
Cart,
LineItem,
CommercejsCart,
CommercejsLineItem,
} from '../types/cart'
type CommercejsLineItemType = CommercejsLineItem & { image: { url: string } }
const normalizeLineItem = (
commercejsLineItem: CommercejsLineItemType
): LineItem => {
const {
id,
sku,
quantity,
price,
product_id,
product_name,
permalink,
variant,
image,
selected_options,
} = commercejsLineItem
return {
id,
variantId: variant?.id ?? '',
productId: product_id,
name: product_name,
quantity,
discounts: [],
path: permalink,
options: selected_options?.map(({ group_name, option_name }) => ({
name: group_name,
value: option_name,
})),
variant: {
id: variant?.id ?? id,
sku: variant?.sku ?? sku,
name: product_name,
requiresShipping: false,
price: variant?.price?.raw ?? price.raw,
listPrice: variant?.price?.raw ?? price.raw,
image: {
url: image?.url,
},
},
}
}
export const normalizeCart = (commercejsCart: CommercejsCart): Cart => {
const {
id,
created,
subtotal: { raw: rawPrice },
currency,
line_items,
} = commercejsCart
return {
id,
createdAt: new Date(created * 1000).toISOString(),
currency: {
code: currency.code,
},
taxesIncluded: false,
lineItems: line_items.map((item) => {
return normalizeLineItem(item as CommercejsLineItemType)
}),
lineItemsSubtotalPrice: rawPrice,
subtotalPrice: rawPrice,
totalPrice: rawPrice,
}
}

View File

@ -0,0 +1,14 @@
import type { Category } from '@commerce/types/site'
import type { Category as CommercejsCategory } from '@chec/commerce.js/types/category'
export function normalizeCategory(
commercejsCatgeory: CommercejsCategory
): Category {
const { id, name, slug } = commercejsCatgeory
return {
id,
name,
slug,
path: slug,
}
}

View File

@ -0,0 +1,63 @@
import type { CardFields } from '@commerce/types/customer/card'
import type { AddressFields } from '@commerce/types/customer/address'
import type { CommercejsCheckoutCapture } from '../types/checkout'
/**
* Creates a checkout payload suitable for test checkouts.
* 1. Hard-codes the payment values for the Commerce.js test gateway.
* 2. Hard-codes the email until an email field exists on the checkout form.
* 3. Gets as much as much checkout info as possible from the checkout form, and uses fallback values.
*/
export function normalizeTestCheckout({
paymentInfo,
shippingInfo,
shippingOption,
}: {
paymentInfo?: CardFields
shippingInfo?: AddressFields
shippingOption: string
}): CommercejsCheckoutCapture {
const firstName =
shippingInfo?.firstName || paymentInfo?.firstName || 'Nextjs'
const lastName = shippingInfo?.lastName || paymentInfo?.lastName || 'Commerce'
const fullName = `${firstName} ${lastName}`
const postalCode = shippingInfo?.zipCode || paymentInfo?.zipCode || '94103'
const street =
shippingInfo?.streetNumber || paymentInfo?.streetNumber || 'Test Street'
const townCity = shippingInfo?.city || paymentInfo?.city || 'Test Town'
return {
payment: {
gateway: 'test_gateway',
card: {
number: '4242 4242 4242 4242',
expiry_month: '01',
expiry_year: '2024',
cvc: '123',
postal_zip_code: postalCode,
},
},
customer: {
email: 'nextcommerce@test.com',
firstname: firstName,
lastname: lastName,
},
shipping: {
name: fullName,
street,
town_city: townCity,
country: 'US',
},
billing: {
name: fullName,
street,
town_city: townCity,
postal_zip_code: postalCode,
county_state: 'California',
country: 'US',
},
fulfillment: {
shipping_method: shippingOption,
},
}
}

View File

@ -0,0 +1,77 @@
import type {
Product,
CommercejsProduct,
CommercejsVariant,
} from '../types/product'
function getOptionsFromVariantGroups(
variantGroups: CommercejsProduct['variant_groups']
): Product['options'] {
const optionsFromVariantGroups = variantGroups.map(
({ id, name: variantName, options }) => ({
id,
displayName: variantName,
values: options.map(({ name: optionName }) => ({
label: optionName,
})),
})
)
return optionsFromVariantGroups
}
function normalizeVariants(
variants: Array<CommercejsVariant> = [],
variantGroups: CommercejsProduct['variant_groups']
) {
if (!Array.isArray(variants)) return []
return variants?.map((variant) => ({
id: variant.id,
options: Object.entries(variant.options).map(
([variantGroupId, variantOptionId]) => {
const variantGroupFromId = variantGroups.find(
(group) => group.id === variantGroupId
)
const valueLabel = variantGroupFromId?.options.find(
(option) => option.id === variantOptionId
)?.name
return {
id: variantOptionId,
displayName: variantGroupFromId?.name || '',
__typename: 'MultipleChoiceOption' as 'MultipleChoiceOption',
values: [
{
label: valueLabel || '',
},
],
}
}
),
}))
}
export function normalizeProduct(
commercejsProduct: CommercejsProduct,
commercejsProductVariants: Array<CommercejsVariant> = []
): Product {
const { id, name, description, permalink, assets, price, variant_groups } =
commercejsProduct
return {
id,
name,
description,
descriptionHtml: description,
slug: permalink,
path: permalink,
images: assets.map(({ url, description, filename }) => ({
url,
alt: description || filename,
})),
price: {
value: price.raw,
currencyCode: 'USD',
},
variants: normalizeVariants(commercejsProductVariants, variant_groups),
options: getOptionsFromVariantGroups(variant_groups),
}
}

View File

@ -0,0 +1,54 @@
import { SearchProductsBody } from '@commerce/types/product'
const getFilterVariables = ({
search,
categoryId,
}: {
search?: string
categoryId?: string | number
}) => {
let filterVariables: { [key: string]: any } = {}
if (search) {
filterVariables.query = search
}
if (categoryId) {
filterVariables['category_id'] = categoryId
}
return filterVariables
}
const getSortVariables = ({ sort }: { sort?: string }) => {
let sortVariables: { [key: string]: any } = {}
switch (sort) {
case 'trending-desc':
case 'latest-desc':
sortVariables = {
sortBy: 'updated',
sortDirection: 'desc',
}
break
case 'price-asc':
sortVariables = {
sortBy: 'price',
sortDirection: 'asc',
}
break
case 'price-desc':
sortVariables = {
sortBy: 'price',
sortDirection: 'desc',
}
break
}
return sortVariables
}
export const getProductSearchVariables = (input: SearchProductsBody) => {
const { search, categoryId, sort } = input
const filterVariables = getFilterVariables({ search, categoryId })
const sortVariables = getSortVariables({ sort })
return {
...filterVariables,
...sortVariables,
}
}

View File

@ -0,0 +1,13 @@
import { useCallback } from 'react'
export function emptyHook() {
const useEmptyHook = async (options = {}) => {
return useCallback(async function () {
return Promise.resolve()
}, [])
}
return useEmptyHook
}
export default emptyHook

View File

@ -0,0 +1,17 @@
import { useCallback } from 'react'
type Options = {
includeProducts?: boolean
}
export function emptyHook(options?: Options) {
const useEmptyHook = async ({ id }: { id: string | number }) => {
return useCallback(async function () {
return Promise.resolve()
}, [])
}
return useEmptyHook
}
export default emptyHook

View File

@ -0,0 +1,40 @@
import { HookFetcher } from '@commerce/utils/types'
export type Wishlist = {
items: [
{
product_id: number
variant_id: number
id: number
product: any
}
]
}
export interface UseWishlistOptions {
includeProducts?: boolean
}
export interface UseWishlistInput extends UseWishlistOptions {
customerId?: number
}
export const fetcher: HookFetcher<Wishlist | null, UseWishlistInput> = () => {
return null
}
export function extendHook(
customFetcher: typeof fetcher,
// swrOptions?: SwrOptions<Wishlist | null, UseWishlistInput>
swrOptions?: any
) {
const useWishlist = ({ includeProducts }: UseWishlistOptions = {}) => {
return { data: null }
}
useWishlist.extend = extendHook
return useWishlist
}
export default extendHook(fetcher)

78
package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@chec/commerce.js": "^2.8.0",
"@react-spring/web": "^9.2.1",
"@spree/storefront-api-v2-sdk": "^5.0.1",
"@vercel/fetch": "^6.1.1",
@ -47,6 +48,7 @@
"@graphql-codegen/typescript-operations": "^1.18.1",
"@next/bundle-analyzer": "^10.2.3",
"@types/body-scroll-lock": "^2.6.1",
"@types/chec__commerce.js": "^2.8.4",
"@types/cookie": "^0.4.0",
"@types/js-cookie": "^2.2.6",
"@types/lodash.debounce": "^4.0.6",
@ -1282,6 +1284,15 @@
"to-fast-properties": "^2.0.0"
}
},
"node_modules/@chec/commerce.js": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@chec/commerce.js/-/commerce.js-2.8.0.tgz",
"integrity": "sha512-OPBphT/hU33iDp52zzYOqz/oSXLhEuhGVUg2UNvYtmBW4eCNmtsM0dqW0+wu+6K0d6fZojurCBdVQMKb2R7l3g==",
"dependencies": {
"@babel/runtime": "^7.7.4",
"axios": "^0.21.1"
}
},
"node_modules/@csstools/convert-colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz",
@ -2509,6 +2520,15 @@
"integrity": "sha512-PPFm/2A6LfKmSpvMg58gHtSqwwMChbcKKGhSCRIhY4MyFzhY8moAN6HrTCpOeZQUqkFdTFfMqr7njeqGLKt72Q==",
"dev": true
},
"node_modules/@types/chec__commerce.js": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/@types/chec__commerce.js/-/chec__commerce.js-2.8.4.tgz",
"integrity": "sha512-hyR2OXEB3gIRp/ESWOQaFStefBG+C5OdnkxGC1Gmp0ePVzl/wk5FyvaK5NsT1ddNC/y1YsmDAVPe+DArr6/9Jg==",
"dev": true,
"dependencies": {
"@types/chec__commerce.js": "*"
}
},
"node_modules/@types/cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz",
@ -3254,6 +3274,14 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"dependencies": {
"follow-redirects": "^1.14.0"
}
},
"node_modules/axobject-query": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@ -5809,6 +5837,25 @@
"deprecated": "flatten is deprecated in favor of utility frameworks such as lodash.",
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.14.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
"integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/foreach": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",
@ -16303,6 +16350,15 @@
"to-fast-properties": "^2.0.0"
}
},
"@chec/commerce.js": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@chec/commerce.js/-/commerce.js-2.8.0.tgz",
"integrity": "sha512-OPBphT/hU33iDp52zzYOqz/oSXLhEuhGVUg2UNvYtmBW4eCNmtsM0dqW0+wu+6K0d6fZojurCBdVQMKb2R7l3g==",
"requires": {
"@babel/runtime": "^7.7.4",
"axios": "^0.21.1"
}
},
"@csstools/convert-colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz",
@ -17254,6 +17310,15 @@
"integrity": "sha512-PPFm/2A6LfKmSpvMg58gHtSqwwMChbcKKGhSCRIhY4MyFzhY8moAN6HrTCpOeZQUqkFdTFfMqr7njeqGLKt72Q==",
"dev": true
},
"@types/chec__commerce.js": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/@types/chec__commerce.js/-/chec__commerce.js-2.8.4.tgz",
"integrity": "sha512-hyR2OXEB3gIRp/ESWOQaFStefBG+C5OdnkxGC1Gmp0ePVzl/wk5FyvaK5NsT1ddNC/y1YsmDAVPe+DArr6/9Jg==",
"dev": true,
"requires": {
"@types/chec__commerce.js": "*"
}
},
"@types/cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz",
@ -17843,6 +17908,14 @@
"integrity": "sha512-5LMaDRWm8ZFPAEdzTYmgjjEdj1YnQcpfrVajO/sn/LhbpGp0Y0H64c2hLZI1gRMxfA+w1S71Uc/nHaOXgcCvGg==",
"dev": true
},
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"requires": {
"follow-redirects": "^1.14.0"
}
},
"axobject-query": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@ -19897,6 +19970,11 @@
"integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==",
"dev": true
},
"follow-redirects": {
"version": "1.14.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
"integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA=="
},
"foreach": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",

View File

@ -21,6 +21,7 @@
"node": ">=14.x"
},
"dependencies": {
"@chec/commerce.js": "^2.8.0",
"@react-spring/web": "^9.2.1",
"@spree/storefront-api-v2-sdk": "^5.0.1",
"@vercel/fetch": "^6.1.1",
@ -59,6 +60,7 @@
"@graphql-codegen/typescript-operations": "^1.18.1",
"@next/bundle-analyzer": "^10.2.3",
"@types/body-scroll-lock": "^2.6.1",
"@types/chec__commerce.js": "^2.8.4",
"@types/cookie": "^0.4.0",
"@types/js-cookie": "^2.2.6",
"@types/lodash.debounce": "^4.0.6",

View File

@ -34,6 +34,7 @@
"./framework/shopify",
"./framework/swell",
"./framework/vendure",
"./framework/saleor"
"./framework/saleor",
"./framework/commercejs"
]
}