Merge branch 'master' of github.com:vercel/commerce into outgrow-reaction-commerce-provider

Signed-off-by: Loan Laux <loan@outgrow.io>
This commit is contained in:
Loan Laux 2021-05-27 20:18:10 +04:00
commit 1332345da4
No known key found for this signature in database
GPG Key ID: AF9E9BD6548AD52E
79 changed files with 17217 additions and 748 deletions

View File

@ -1,5 +1,5 @@
# Available providers: bigcommerce, shopify
COMMERCE_PROVIDER=bigcommerce
# Available providers: bigcommerce, shopify, swell
COMMERCE_PROVIDER=
BIGCOMMERCE_STOREFRONT_API_URL=
BIGCOMMERCE_STOREFRONT_API_TOKEN=
@ -10,3 +10,6 @@ BIGCOMMERCE_CHANNEL_ID=
NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=
NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=
NEXT_PUBLIC_SWELL_STORE_ID=
NEXT_PUBLIC_SWELL_PUBLIC_KEY=

View File

@ -7,8 +7,9 @@ Start right now at [nextjs.org/commerce](https://nextjs.org/commerce)
Demo live at: [demo.vercel.store](https://demo.vercel.store/)
- Shopify Demo: https://shopify.demo.vercel.store/
- BigCommerce Demo: https://bigcommerce.demo.vercel.store/
- Shopify Demo: https://shopify.vercel.store/
- Swell Demo: https://swell.vercel.store/
- BigCommerce Demo: https://bigcommerce.vercel.store/
## Features
@ -40,6 +41,23 @@ Next.js Commerce integrates out-of-the-box with BigCommerce and Shopify. We plan
Open `.env.local` and change the value of `COMMERCE_PROVIDER` to the provider you would like to use, then set the environment variables for that provider (use `.env.template` as the base).
The setup for Shopify would look like this for example:
```
COMMERCE_PROVIDER=shopify
NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=xxxxxxx.myshopify.com
```
And change the `tsconfig.json` to resolve to the chosen provider:
```
"@framework": ["framework/shopify"],
"@framework/*": ["framework/shopify/*"]
```
That's it!
### Features
Every provider defines the features that it supports under `framework/{provider}/commerce.config.json`

View File

@ -1,7 +1,9 @@
.root {
@apply text-center p-6 bg-primary text-sm flex-row justify-center items-center font-medium fixed bottom-0 w-full z-30 transition-all duration-300 ease-out;
}
@screen md {
.root {
@apply flex text-left;
}
}

View File

@ -44,20 +44,6 @@ const Footer: FC<Props> = ({ className, pages }) => {
</a>
</Link>
</li>
<li className="py-3 md:py-0 md:pb-4">
<Link href="/">
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
Careers
</a>
</Link>
</li>
<li className="py-3 md:py-0 md:pb-4">
<Link href="/blog">
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
Blog
</a>
</Link>
</li>
{sitePages.map((page) => (
<li key={page.url} className="py-3 md:py-0 md:pb-4">
<Link href={page.url!}>

View File

@ -16,14 +16,16 @@
.dropdownMenu {
@apply fixed right-0 top-12 mt-2 origin-top-right outline-none bg-primary z-40 w-full h-full;
}
@screen lg {
.dropdownMenu {
@apply absolute border border-accents-1 shadow-lg w-56 h-auto;
}
}
.closeButton {
@screen md {
.closeButton {
@apply hidden;
}
}

View File

@ -1,7 +1,9 @@
.dropdownMenu {
@apply fixed right-0 mt-2 origin-top-right outline-none bg-primary z-40 w-full h-full;
}
@screen lg {
.dropdownMenu {
@apply absolute top-10 border border-accents-1 shadow-lg w-56 h-auto;
}
}

View File

@ -3,10 +3,6 @@
@apply grid grid-cols-1 gap-0;
min-height: var(--row-height);
@screen lg {
@apply grid-cols-3 grid-rows-2;
}
& > * {
@apply row-span-1 bg-transparent box-border overflow-hidden;
height: 500px;
@ -19,6 +15,17 @@
}
}
@screen lg {
.root {
@apply grid-cols-3 grid-rows-2;
}
.root & > * {
@apply col-span-1;
height: inherit;
}
}
.default {
& > * {
@apply bg-transparent;

View File

@ -1,6 +1,9 @@
.root {
@apply mx-auto grid grid-cols-1 py-32 gap-4;
}
@screen md {
.root {
@apply grid-cols-2;
}
}

View File

@ -21,7 +21,7 @@ const Hero: FC<Props> = ({ headline, description }) => {
<p className="mt-5 text-xl leading-7 text-accent-2 text-white">
{description}
</p>
<Link href="/blog">
<Link href="/">
<a className="text-white pt-3 font-bold hover:underline flex flex-row cursor-pointer w-max-content">
Read it here
<RightArrow width="20" heigh="20" className="ml-1" />

View File

@ -7,7 +7,7 @@ const fs = require('fs')
const merge = require('deepmerge')
const prettier = require('prettier')
const PROVIDERS = ['bigcommerce', 'reactioncommerce', 'shopify']
const PROVIDERS = ['bigcommerce', 'shopify', 'swell', 'reactioncommerce']
function getProviderName() {
return (
@ -16,6 +16,8 @@ function getProviderName() {
? 'bigcommerce'
: process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN
? 'shopify'
: process.env.NEXT_PUBLIC_SWELL_STORE_ID
? 'swell'
: null)
)
}

View File

@ -0,0 +1,5 @@
SWELL_STORE_DOMAIN=
SWELL_STOREFRONT_ACCESS_TOKEN=
NEXT_PUBLIC_SWELL_STORE_ID=
NEXT_PUBLIC_SWELL_PUBLIC_KEY=

View File

@ -0,0 +1 @@
export default function () {}

View File

@ -0,0 +1 @@
export default function () {}

View File

@ -0,0 +1 @@
export default function () {}

View File

@ -0,0 +1,20 @@
import createApiHandler, { SwellApiHandler } from '../utils/create-api-handler'
import { SWELL_CHECKOUT_URL_COOKIE } from '../../const'
import { getConfig } from '..'
const checkoutApi: SwellApiHandler<any> = async (req, res, config) => {
config = getConfig()
const { cookies } = req
const checkoutUrl = cookies[SWELL_CHECKOUT_URL_COOKIE]
if (checkoutUrl) {
res.redirect(checkoutUrl)
} else {
res.redirect('/cart')
}
}
export default createApiHandler(checkoutApi, {}, {})

View File

@ -0,0 +1 @@
export default function () {}

View File

@ -0,0 +1 @@
export default function () {}

View File

@ -0,0 +1 @@
export default function () {}

View File

@ -0,0 +1 @@
export default function () {}

View File

@ -0,0 +1 @@
export default function () {}

View File

@ -0,0 +1,50 @@
import type { CommerceAPIConfig } from '@commerce/api'
import {
SWELL_CHECKOUT_ID_COOKIE,
SWELL_CUSTOMER_TOKEN_COOKIE,
SWELL_COOKIE_EXPIRE,
} from '../const'
import fetchApi from './utils/fetch-swell-api'
export interface SwellConfig extends CommerceAPIConfig {
fetch: any
}
export class Config {
private config: SwellConfig
constructor(config: SwellConfig) {
this.config = config
}
getConfig(userConfig: Partial<SwellConfig> = {}) {
return Object.entries(userConfig).reduce<SwellConfig>(
(cfg, [key, value]) => Object.assign(cfg, { [key]: value }),
{ ...this.config }
)
}
setConfig(newConfig: Partial<SwellConfig>) {
Object.assign(this.config, newConfig)
}
}
const config = new Config({
locale: 'en-US',
commerceUrl: '',
apiToken: ''!,
cartCookie: SWELL_CHECKOUT_ID_COOKIE,
cartCookieMaxAge: SWELL_COOKIE_EXPIRE,
fetch: fetchApi,
customerCookie: SWELL_CUSTOMER_TOKEN_COOKIE,
})
export function getConfig(userConfig?: Partial<SwellConfig>) {
return config.getConfig(userConfig)
}
export function setConfig(newConfig: Partial<SwellConfig>) {
return config.setConfig(newConfig)
}

View File

@ -0,0 +1,25 @@
import { Page } from '../../schema'
import { SwellConfig, getConfig } from '..'
export type GetPageResult<T extends { page?: any } = { page?: Page }> = T
export type PageVariables = {
id: string
}
async function getPage({
url,
variables,
config,
preview,
}: {
url?: string
variables: PageVariables
config?: SwellConfig
preview?: boolean
}): Promise<GetPageResult> {
config = getConfig(config)
return {}
}
export default getPage

View File

@ -0,0 +1,58 @@
import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
import { SwellConfig, getConfig } from '..'
export type SwellApiHandler<
T = any,
H extends SwellHandlers = {},
Options extends {} = {}
> = (
req: NextApiRequest,
res: NextApiResponse<SwellApiResponse<T>>,
config: SwellConfig,
handlers: H,
// Custom configs that may be used by a particular handler
options: Options
) => void | Promise<void>
export type SwellHandler<T = any, Body = null> = (options: {
req: NextApiRequest
res: NextApiResponse<SwellApiResponse<T>>
config: SwellConfig
body: Body
}) => void | Promise<void>
export type SwellHandlers<T = any> = {
[k: string]: SwellHandler<T, any>
}
export type SwellApiResponse<T> = {
data: T | null
errors?: { message: string; code?: string }[]
}
export default function createApiHandler<
T = any,
H extends SwellHandlers = {},
Options extends {} = {}
>(
handler: SwellApiHandler<T, H, Options>,
handlers: H,
defaultOptions: Options
) {
return function getApiHandler({
config,
operations,
options,
}: {
config?: SwellConfig
operations?: Partial<H>
options?: Options extends {} ? Partial<Options> : never
} = {}): NextApiHandler {
const ops = { ...operations, ...handlers }
const opts = { ...defaultOptions, ...options }
return function apiHandler(req, res) {
return handler(req, res, getConfig(config), ops, opts)
}
}
}

View File

@ -0,0 +1,7 @@
import { swellConfig } from '../..'
const fetchApi = async (query: string, method: string, variables: [] = []) => {
const { swell } = swellConfig
return swell[query][method](...variables)
}
export default fetchApi

View File

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

View File

@ -0,0 +1,28 @@
import type { NextApiRequest, NextApiResponse } from 'next'
export default function isAllowedMethod(
req: NextApiRequest,
res: NextApiResponse,
allowedMethods: string[]
) {
const methods = allowedMethods.includes('OPTIONS')
? allowedMethods
: [...allowedMethods, 'OPTIONS']
if (!req.method || !methods.includes(req.method)) {
res.status(405)
res.setHeader('Allow', methods.join(', '))
res.end()
return false
}
if (req.method === 'OPTIONS') {
res.status(200)
res.setHeader('Allow', methods.join(', '))
res.setHeader('Content-Length', '0')
res.end()
return false
}
return true
}

View File

@ -0,0 +1,2 @@
export type WishlistItem = { product: any; id: number }
export default function () {}

View File

@ -0,0 +1,76 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError, ValidationError } from '@commerce/utils/errors'
import useCustomer from '../customer/use-customer'
import {
CustomerAccessTokenCreateInput,
CustomerUserError,
Mutation,
MutationCheckoutCreateArgs,
} from '../schema'
import useLogin, { UseLogin } from '@commerce/auth/use-login'
import { setCustomerToken } from '../utils'
export default useLogin as UseLogin<typeof handler>
const getErrorMessage = ({ code, message }: CustomerUserError) => {
switch (code) {
case 'UNIDENTIFIED_CUSTOMER':
message = 'Cannot find an account that matches the provided credentials'
break
}
return message
}
export const handler: MutationHook<null, {}, CustomerAccessTokenCreateInput> = {
fetchOptions: {
query: 'account',
method: 'login',
},
async fetcher({ input: { email, password }, options, fetch }) {
if (!(email && password)) {
throw new CommerceError({
message:
'A first name, last name, email and password are required to login',
})
}
const { customerAccessTokenCreate } = await fetch<
Mutation,
MutationCheckoutCreateArgs
>({
...options,
variables: [email, password],
})
const errors = customerAccessTokenCreate?.customerUserErrors
if (errors && errors.length) {
throw new ValidationError({
message: getErrorMessage(errors[0]),
})
}
const customerAccessToken = customerAccessTokenCreate?.customerAccessToken
const accessToken = customerAccessToken?.accessToken
if (accessToken) {
setCustomerToken(accessToken)
}
return null
},
useHook:
({ fetch }) =>
() => {
const { revalidate } = useCustomer()
return useCallback(
async function login(input) {
const data = await fetch({ input })
await revalidate()
return data
},
[fetch, revalidate]
)
},
}

View File

@ -0,0 +1,38 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import useLogout, { UseLogout } from '@commerce/auth/use-logout'
import useCustomer from '../customer/use-customer'
import { getCustomerToken, setCustomerToken } from '../utils/customer-token'
export default useLogout as UseLogout<typeof handler>
export const handler: MutationHook<null> = {
fetchOptions: {
query: 'account',
method: 'logout',
},
async fetcher({ options, fetch }) {
await fetch({
...options,
variables: {
customerAccessToken: getCustomerToken(),
},
})
setCustomerToken(null)
return null
},
useHook:
({ fetch }) =>
() => {
const { mutate } = useCustomer()
return useCallback(
async function logout() {
const data = await fetch()
await mutate(null, false)
return data
},
[fetch, mutate]
)
},
}

View File

@ -0,0 +1,67 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import useSignup, { UseSignup } from '@commerce/auth/use-signup'
import useCustomer from '../customer/use-customer'
import { CustomerCreateInput } from '../schema'
import handleLogin from '../utils/handle-login'
export default useSignup as UseSignup<typeof handler>
export const handler: MutationHook<
null,
{},
CustomerCreateInput,
CustomerCreateInput
> = {
fetchOptions: {
query: 'account',
method: 'create',
},
async fetcher({
input: { firstName, lastName, email, password },
options,
fetch,
}) {
if (!(firstName && lastName && email && password)) {
throw new CommerceError({
message:
'A first name, last name, email and password are required to signup',
})
}
const data = await fetch({
...options,
variables: {
first_name: firstName,
last_name: lastName,
email,
password,
},
})
try {
const loginData = await fetch({
query: 'account',
method: 'login',
variables: [email, password],
})
handleLogin(loginData)
} catch (error) {}
return data
},
useHook:
({ fetch }) =>
() => {
const { revalidate } = useCustomer()
return useCallback(
async function signup(input) {
const data = await fetch({ input })
await revalidate()
return data
},
[fetch, revalidate]
)
},
}

View File

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

View File

@ -0,0 +1,61 @@
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
import useCart from './use-cart'
import { Cart, CartItemBody } from '../types'
import { checkoutToCart } from './utils'
import { getCheckoutId } from '../utils'
import { useCallback } from 'react'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<Cart, {}, CartItemBody> = {
fetchOptions: {
query: 'cart',
method: 'addItem',
},
async fetcher({ input: item, options, fetch }) {
if (
item.quantity &&
(!Number.isInteger(item.quantity) || item.quantity! < 1)
) {
throw new CommerceError({
message: 'The item quantity has to be a valid integer greater than 0',
})
}
const variables: {
product_id: string
variant_id?: string
checkoutId?: string
quantity?: number
} = {
checkoutId: getCheckoutId(),
product_id: item.productId,
quantity: item.quantity,
}
if (item.productId !== item.variantId) {
variables.variant_id = item.variantId
}
const response = await fetch({
...options,
variables,
})
return checkoutToCart(response) as any
},
useHook:
({ fetch }) =>
() => {
const { mutate } = useCart()
return useCallback(
async function addItem(input) {
const data = await fetch({ input })
await mutate(data, false)
return data
},
[fetch, mutate]
)
},
}

View File

@ -0,0 +1,39 @@
import useCart, { UseCart } from '@commerce/cart/use-cart'
import { Cart } from '@commerce/types'
import { SWRHook } from '@commerce/utils/types'
import { useMemo } from 'react'
import { normalizeCart } from '../utils/normalize'
import { checkoutCreate, checkoutToCart } from './utils'
export default useCart as UseCart<typeof handler>
export const handler: SWRHook<Cart | null, {}, any, { isEmpty?: boolean }> = {
fetchOptions: {
query: 'cart',
method: 'get',
},
async fetcher({ fetch }) {
const cart = await checkoutCreate(fetch)
return cart ? normalizeCart(cart) : null
},
useHook:
({ useData }) =>
(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,67 @@
import { useCallback } from 'react'
import type {
MutationHookContext,
HookFetcherContext,
} from '@commerce/utils/types'
import { ValidationError } from '@commerce/utils/errors'
import useRemoveItem, {
RemoveItemInput as RemoveItemInputBase,
UseRemoveItem,
} from '@commerce/cart/use-remove-item'
import useCart from './use-cart'
import { checkoutToCart } from './utils'
import { Cart, LineItem } from '../types'
import { RemoveCartItemBody } from '@commerce/types'
export type RemoveItemFn<T = any> = T extends LineItem
? (input?: RemoveItemInput<T>) => Promise<Cart | null>
: (input: RemoveItemInput<T>) => Promise<Cart | null>
export type RemoveItemInput<T = any> = T extends LineItem
? Partial<RemoveItemInputBase>
: RemoveItemInputBase
export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler = {
fetchOptions: {
query: 'cart',
method: 'removeItem',
},
async fetcher({
input: { itemId },
options,
fetch,
}: HookFetcherContext<RemoveCartItemBody>) {
const response = await fetch({
...options,
variables: [itemId],
})
return checkoutToCart(response)
},
useHook:
({ fetch }: MutationHookContext<Cart | null, RemoveCartItemBody>) =>
<T extends LineItem | undefined = undefined>(ctx: { item?: T } = {}) => {
const { item } = ctx
const { mutate } = useCart()
const removeItem: RemoveItemFn<LineItem> = async (input) => {
const itemId = input?.id ?? item?.id
if (!itemId) {
throw new ValidationError({
message: 'Invalid input used for this operation',
})
}
const data = await fetch({ input: { itemId } })
await mutate(data, false)
return data
}
return useCallback(removeItem as RemoveItemFn<T>, [fetch, mutate])
},
}

View File

@ -0,0 +1,93 @@
import { useCallback } from 'react'
import debounce from 'lodash.debounce'
import type {
HookFetcherContext,
MutationHookContext,
} from '@commerce/utils/types'
import { ValidationError } from '@commerce/utils/errors'
import useUpdateItem, {
UpdateItemInput as UpdateItemInputBase,
UseUpdateItem,
} from '@commerce/cart/use-update-item'
import useCart from './use-cart'
import { handler as removeItemHandler } from './use-remove-item'
import type { Cart, LineItem, UpdateCartItemBody } from '../types'
import { checkoutToCart } from './utils'
export type UpdateItemInput<T = any> = T extends LineItem
? Partial<UpdateItemInputBase<LineItem>>
: UpdateItemInputBase<LineItem>
export default useUpdateItem as UseUpdateItem<typeof handler>
export const handler = {
fetchOptions: {
query: 'cart',
method: 'updateItem',
},
async fetcher({
input: { itemId, item },
options,
fetch,
}: HookFetcherContext<UpdateCartItemBody>) {
if (Number.isInteger(item.quantity)) {
// Also allow the update hook to remove an item if the quantity is lower than 1
if (item.quantity! < 1) {
return removeItemHandler.fetcher({
options: removeItemHandler.fetchOptions,
input: { itemId },
fetch,
})
}
} else if (item.quantity) {
throw new ValidationError({
message: 'The item quantity has to be a valid integer',
})
}
const response = await fetch({
...options,
variables: [itemId, { quantity: item.quantity }],
})
return checkoutToCart(response)
},
useHook:
({ fetch }: MutationHookContext<Cart | null, UpdateCartItemBody>) =>
<T extends LineItem | undefined = undefined>(
ctx: {
item?: T
wait?: number
} = {}
) => {
const { item } = ctx
const { mutate, data: cartData } = useCart() as any
return useCallback(
debounce(async (input: UpdateItemInput<T>) => {
const itemId = cartData.lineItems[0].id
const productId = cartData.lineItems[0].productId
const variantId = cartData.lineItems[0].variant.id
if (!itemId || !productId) {
throw new ValidationError({
message: 'Invalid input used for this operation',
})
}
const data = await fetch({
input: {
item: {
productId,
variantId,
quantity: input.quantity,
},
itemId,
},
})
await mutate(data, false)
return data
}, ctx.wait ?? 500),
[fetch, mutate]
)
},
}

View File

@ -0,0 +1,28 @@
import { SWELL_CHECKOUT_URL_COOKIE } from '../../const'
import Cookies from 'js-cookie'
export const checkoutCreate = async (fetch: any) => {
const cart = await fetch({
query: 'cart',
method: 'get',
})
if (!cart) {
const cart = await fetch({
query: 'cart',
method: 'setItems',
variables: [[]],
})
}
const checkoutUrl = cart?.checkout_url
if (checkoutUrl) {
Cookies.set(SWELL_CHECKOUT_URL_COOKIE, checkoutUrl)
}
return cart
}
export default checkoutCreate

View File

@ -0,0 +1,26 @@
import { Cart } from '../../types'
import { CommerceError } from '@commerce/utils/errors'
import {
CheckoutLineItemsAddPayload,
CheckoutLineItemsRemovePayload,
CheckoutLineItemsUpdatePayload,
Maybe,
} from '../../schema'
import { normalizeCart } from '../../utils'
export type CheckoutPayload =
| CheckoutLineItemsAddPayload
| CheckoutLineItemsUpdatePayload
| CheckoutLineItemsRemovePayload
const checkoutToCart = (checkoutPayload?: Maybe<CheckoutPayload>): Cart => {
if (!checkoutPayload) {
throw new CommerceError({
message: 'Invalid response from Swell',
})
}
return normalizeCart(checkoutPayload as any)
}
export default checkoutToCart

View File

@ -0,0 +1,33 @@
import { HookFetcherFn } from '@commerce/utils/types'
import { Cart } from '@commerce/types'
// import { checkoutCreate, checkoutToCart } from '.'
import { FetchCartInput } from '@commerce/cart/use-cart'
import { data } from 'autoprefixer'
import { normalizeCart } from '../../utils'
const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = async ({
options,
// input: { cartId: checkoutId },
fetch,
}) => {
let checkout
// if (checkoutId) {
const data = await fetch({
query: 'cart',
method: 'get',
// variables: { category: categoryId },
})
// checkout = data.node
// }
// if (checkout?.completedAt || !checkoutId) {
// checkout = await checkoutCreate(fetch)
// }
// TODO: Fix this type
// return checkoutToCart({ checkout } as any)
return normalizeCart(data)
}
export default fetcher

View File

@ -0,0 +1,2 @@
export { default as checkoutToCart } from './checkout-to-cart'
export { default as checkoutCreate } from './checkout-create'

View File

@ -0,0 +1,6 @@
{
"provider": "swell",
"features": {
"wishlist": false
}
}

View File

@ -0,0 +1,37 @@
import { getConfig, SwellConfig } from '../api'
type Variables = {
first?: number
}
type ReturnType = {
pages: Page[]
}
export type Page = {
id: string
name: string
url: string
sort_order?: number
body: string
}
const getAllPages = async (options?: {
variables?: Variables
config: SwellConfig
preview?: boolean
}): Promise<ReturnType> => {
let { config, variables = { first: 250 } } = options ?? {}
config = getConfig(config)
const { locale, fetch } = config
const data = await fetch('content', 'list', ['pages'])
const pages =
data?.results?.map(({ slug, ...rest }: { slug: string }) => ({
url: `/${locale}/${slug}`,
...rest,
})) ?? []
return { pages }
}
export default getAllPages

View File

@ -0,0 +1,33 @@
import { getConfig, SwellConfig } from '../api'
import { Page } from './get-all-pages'
type Variables = {
id: string
}
export type GetPageResult<T extends { page?: any } = { page?: Page }> = T
const getPage = async (options: {
variables: Variables
config: SwellConfig
preview?: boolean
}): Promise<GetPageResult> => {
let { config, variables } = options ?? {}
config = getConfig(config)
const { locale } = config
const { id } = variables
const result = await config.fetch('content', 'get', ['pages', id])
const page = result
return {
page: page
? {
...page,
url: `/${locale}/${page.slug}`,
}
: null,
}
}
export default getPage

View File

@ -0,0 +1,31 @@
import getCategories, { Category } from '../utils/get-categories'
import getVendors, { Brands } from '../utils/get-vendors'
import { getConfig, SwellConfig } from '../api'
export type GetSiteInfoResult<
T extends { categories: any[]; brands: any[] } = {
categories: Category[]
brands: Brands
}
> = T
const getSiteInfo = async (options?: {
variables?: any
config: SwellConfig
preview?: boolean
}): Promise<GetSiteInfoResult> => {
let { config } = options ?? {}
config = getConfig(config)
const categories = await getCategories(config)
const brands = await getVendors(config)
return {
categories,
brands,
}
}
export default getSiteInfo

13
framework/swell/const.ts Normal file
View File

@ -0,0 +1,13 @@
export const SWELL_CHECKOUT_ID_COOKIE = 'SWELL_checkoutId'
export const SWELL_CHECKOUT_URL_COOKIE = 'swell_checkoutUrl'
export const SWELL_CUSTOMER_TOKEN_COOKIE = 'swell_customerToken'
export const STORE_DOMAIN = process.env.NEXT_PUBLIC_SWELL_STORE_DOMAIN
export const SWELL_COOKIE_EXPIRE = 30
export const SWELL_STORE_ID = process.env.NEXT_PUBLIC_SWELL_STORE_ID
export const SWELL_PUBLIC_KEY = process.env.NEXT_PUBLIC_SWELL_PUBLIC_KEY

View File

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

View File

@ -0,0 +1,52 @@
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
import { Customer } from '@commerce/types'
import { SWRHook } from '@commerce/utils/types'
import { normalizeCustomer } from '../utils/normalize'
export default useCustomer as UseCustomer<typeof handler>
export const handler: SWRHook<Customer | null> = {
fetchOptions: {
query: 'account',
method: 'get',
},
async fetcher({ options, fetch }) {
const data = await fetch<any | null>({
...options,
})
return data ? normalizeCustomer(data) : null
},
useHook:
({ useData }) =>
(input) => {
return useData({
swrOptions: {
revalidateOnFocus: false,
...input?.swrOptions,
},
})
},
}
// const handler = (): { data: Customer } => {
// const swell = getContext();
// const response = swell.account.get();
// const { firstName, lastName, email, company, customerGroupId, notes, phone,
// entityId, addressCount, attributeCount, storeCredit } = response;
// return {
// data: {
// firstName,
// lastName,
// email,
// company,
// customerGroupId,
// notes,
// phone,
// entityId,
// addressCount,
// attributeCount,
// storeCredit
// }
// }
// }
// export default handler;

View File

@ -0,0 +1,28 @@
import { Fetcher } from '@commerce/utils/types'
import { handleFetchResponse } from './utils'
import { swellConfig } from './index'
import { CommerceError } from '@commerce/utils/errors'
const fetcher: Fetcher = async ({ method = 'get', variables, query }) => {
const { swell } = swellConfig
async function callSwell() {
if (Array.isArray(variables)) {
const arg1 = variables[0]
const arg2 = variables[1]
const response = await swell[query!][method](arg1, arg2)
return handleFetchResponse(response)
} else {
const response = await swell[query!][method](variables)
return handleFetchResponse(response)
}
}
if (query && query in swell) {
return await callSwell()
} else {
throw new CommerceError({ message: 'Invalid query argument!' })
}
}
export default fetcher

47
framework/swell/index.tsx Normal file
View File

@ -0,0 +1,47 @@
import * as React from 'react'
import swell from 'swell-js'
import { ReactNode } from 'react'
import {
CommerceConfig,
CommerceProvider as CoreCommerceProvider,
useCommerce as useCoreCommerce,
} from '@commerce'
import { swellProvider, SwellProvider } from './provider'
import {
SWELL_CHECKOUT_ID_COOKIE,
SWELL_STORE_ID,
SWELL_PUBLIC_KEY,
} from './const'
swell.init(SWELL_STORE_ID, SWELL_PUBLIC_KEY)
export { swellProvider }
export type { SwellProvider }
export const swellConfig: any = {
locale: 'en-us',
cartCookie: SWELL_CHECKOUT_ID_COOKIE,
swell,
}
export type SwellConfig = Partial<CommerceConfig>
export type SwellProps = {
children?: ReactNode
locale: string
} & SwellConfig
export function CommerceProvider({ children, ...config }: SwellProps) {
return (
<CoreCommerceProvider
// TODO: Fix this type
provider={swellProvider as any}
config={{ ...swellConfig, ...config }}
>
{children}
</CoreCommerceProvider>
)
}
export const useCommerce = () => useCoreCommerce()

View File

@ -0,0 +1,8 @@
const commerce = require('./commerce.config.json')
module.exports = {
commerce,
images: {
domains: ['cdn.schema.io'],
},
}

View File

@ -0,0 +1,28 @@
import { CollectionEdge } from '../schema'
import { getConfig, SwellConfig } from '../api'
const getAllCollections = async (options?: {
variables?: any
config: SwellConfig
preview?: boolean
}) => {
let { config, variables = { limit: 25 } } = options ?? {}
config = getConfig(config)
const response = await config.fetch('categories', 'list', { variables })
const edges = response.results ?? []
const categories = edges.map(
({ node: { id: entityId, title: name, handle } }: CollectionEdge) => ({
entityId,
name,
path: `/${handle}`,
})
)
return {
categories,
}
}
export default getAllCollections

View File

@ -0,0 +1,39 @@
import { SwellProduct } from '../types'
import { getConfig, SwellConfig } from '../api'
type ProductPath = {
path: string
}
export type ProductPathNode = {
node: ProductPath
}
type ReturnType = {
products: ProductPathNode[]
}
const getAllProductPaths = async (options?: {
variables?: any
config?: SwellConfig
preview?: boolean
}): Promise<ReturnType> => {
let { config, variables = [{ limit: 100 }] } = options ?? {}
config = getConfig(config)
const { results } = await config.fetch('products', 'list', [
{
limit: variables.first,
},
])
return {
products: results?.map(({ slug: handle }: SwellProduct) => ({
node: {
path: `/${handle}`,
},
})),
}
}
export default getAllProductPaths

View File

@ -0,0 +1,36 @@
import { getConfig, SwellConfig } from '../api'
import { normalizeProduct } from '../utils/normalize'
import { Product } from '@commerce/types'
import { SwellProduct } from '../types'
type Variables = {
first?: number
field?: string
}
type ReturnType = {
products: Product[]
}
const getAllProducts = async (options: {
variables?: Variables
config?: SwellConfig
preview?: boolean
}): Promise<ReturnType> => {
let { config, variables = { first: 250 } } = options ?? {}
config = getConfig(config)
const { results } = await config.fetch('products', 'list', [
{
limit: variables.first,
},
])
const products = results.map((product: SwellProduct) =>
normalizeProduct(product)
)
return {
products,
}
}
export default getAllProducts

View File

@ -0,0 +1,32 @@
import { GraphQLFetcherResult } from '@commerce/api'
import { getConfig, SwellConfig } from '../api'
import { normalizeProduct } from '../utils'
type Variables = {
slug: string
}
type ReturnType = {
product: any
}
const getProduct = async (options: {
variables: Variables
config: SwellConfig
preview?: boolean
}): Promise<ReturnType> => {
let { config, variables } = options ?? {}
config = getConfig(config)
const product = await config.fetch('products', 'get', [variables.slug])
if (product && product.variants) {
product.variants = product.variants?.results
}
return {
product: product ? normalizeProduct(product) : null,
}
}
export default getProduct

View File

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

View File

@ -0,0 +1,73 @@
import { SWRHook } from '@commerce/utils/types'
import useSearch, { UseSearch } from '@commerce/product/use-search'
import { normalizeProduct } from '../utils'
import { Product } from '@commerce/types'
import { SwellProduct } from '../types'
export default useSearch as UseSearch<typeof handler>
export type SearchProductsInput = {
search?: string
categoryId?: string
brandId?: string
sort?: string
}
export type SearchProductsData = {
products: Product[]
found: boolean
}
export const handler: SWRHook<
SearchProductsData,
SearchProductsInput,
SearchProductsInput
> = {
fetchOptions: {
query: 'products', // String(Math.random()),
method: 'list',
},
async fetcher({ input, options, fetch }) {
const sortMap = new Map([
['latest-desc', ''],
['price-asc', 'price_asc'],
['price-desc', 'price_desc'],
['trending-desc', 'popularity'],
])
const { categoryId, search, sort = 'latest-desc' } = input
const mappedSort = sortMap.get(sort)
const { results, count: found } = await fetch({
query: 'products',
method: 'list',
variables: { category: categoryId, search, sort: mappedSort },
})
const products = results.map((product: SwellProduct) =>
normalizeProduct(product)
)
return {
products,
found,
}
},
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,31 @@
import { SWELL_CHECKOUT_URL_COOKIE, STORE_DOMAIN } from './const'
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 const swellProvider = {
locale: 'en-us',
cartCookie: SWELL_CHECKOUT_URL_COOKIE,
storeDomain: STORE_DOMAIN,
fetcher,
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
customer: { useCustomer },
products: { useSearch },
auth: { useLogin, useLogout, useSignup },
features: {
wishlist: false,
},
}
export type SwellProvider = typeof swellProvider

5002
framework/swell/schema.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1
framework/swell/swell-js.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module 'swell-js'

123
framework/swell/types.ts Normal file
View File

@ -0,0 +1,123 @@
import * as Core from '@commerce/types'
import { CheckoutLineItem } from './schema'
export type SwellImage = {
file: {
url: String
height: Number
width: Number
}
id: string
}
export type CartLineItem = {
id: string
product: SwellProduct
price: number
variant: {
name: string | null
sku: string | null
id: string
}
quantity: number
}
export type SwellCart = {
id: string
account_id: number
currency: string
tax_included_total: number
sub_total: number
grand_total: number
discount_total: number
quantity: number
items: CartLineItem[]
date_created: string
discounts?: { id: number; amount: number }[] | null
// TODO: add missing fields
}
export type SwellVariant = {
id: string
option_value_ids: string[]
name: string
price?: number
stock_status?: string
}
export interface ProductOptionValue {
label: string
hexColors?: string[]
id: string
}
export type ProductOptions = {
id: string
name: string
variant: boolean
values: ProductOptionValue[]
required: boolean
active: boolean
attribute_id: string
}
export interface SwellProduct {
id: string
description: string
name: string
slug: string
currency: string
price: number
images: any[]
options: any[]
variants: any[]
}
export interface SwellCustomer extends Core.Customer {
first_name: string
last_name: string
}
export type SwellCheckout = {
id: string
webUrl: string
lineItems: CheckoutLineItem[]
}
export interface Cart extends Core.Cart {
id: string
lineItems: LineItem[]
}
export interface LineItem extends Core.LineItem {
options?: any[]
}
/**
* Cart mutations
*/
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 GetCartHandlerBody = Core.GetCartHandlerBody
export type AddCartItemBody = Core.AddCartItemBody<CartItemBody>
export type AddCartItemHandlerBody = Core.AddCartItemHandlerBody<CartItemBody>
export type UpdateCartItemBody = Core.UpdateCartItemBody<CartItemBody>
export type UpdateCartItemHandlerBody =
Core.UpdateCartItemHandlerBody<CartItemBody>
export type RemoveCartItemBody = Core.RemoveCartItemBody
export type RemoveCartItemHandlerBody = Core.RemoveCartItemHandlerBody

View File

@ -0,0 +1,21 @@
import Cookies, { CookieAttributes } from 'js-cookie'
import { SWELL_COOKIE_EXPIRE, SWELL_CUSTOMER_TOKEN_COOKIE } from '../const'
export const getCustomerToken = () => Cookies.get(SWELL_CUSTOMER_TOKEN_COOKIE)
export const setCustomerToken = (
token: string | null,
options?: CookieAttributes
) => {
if (!token) {
Cookies.remove(SWELL_CUSTOMER_TOKEN_COOKIE)
} else {
Cookies.set(
SWELL_CUSTOMER_TOKEN_COOKIE,
token,
options ?? {
expires: SWELL_COOKIE_EXPIRE,
}
)
}
}

View File

@ -0,0 +1,20 @@
import { SwellConfig } from '../api'
export type Category = {
entityId: string
name: string
path: string
}
const getCategories = async (config: SwellConfig): Promise<Category[]> => {
const data = await config.fetch('categories', 'get')
return (
data.results.map(({ id: entityId, name, slug }: any) => ({
entityId,
name,
path: `/${slug}`,
})) ?? []
)
}
export default getCategories

View File

@ -0,0 +1,8 @@
import Cookies from 'js-cookie'
import { SWELL_CHECKOUT_ID_COOKIE } from '../const'
const getCheckoutId = (id?: string) => {
return id ?? Cookies.get(SWELL_CHECKOUT_ID_COOKIE)
}
export default getCheckoutId

View File

@ -0,0 +1,27 @@
import getSortVariables from './get-sort-variables'
import type { SearchProductsInput } from '../product/use-search'
export const getSearchVariables = ({
brandId,
search,
categoryId,
sort,
}: SearchProductsInput) => {
let query = ''
if (search) {
query += `product_type:${search} OR title:${search} OR tag:${search}`
}
if (brandId) {
query += `${search ? ' AND ' : ''}vendor:${brandId}`
}
return {
categoryId,
query,
...getSortVariables(sort, !!categoryId),
}
}
export default getSearchVariables

View File

@ -0,0 +1,32 @@
const getSortVariables = (sort?: string, isCategory = false) => {
let output = {}
switch (sort) {
case 'price-asc':
output = {
sortKey: 'PRICE',
reverse: false,
}
break
case 'price-desc':
output = {
sortKey: 'PRICE',
reverse: true,
}
break
case 'trending-desc':
output = {
sortKey: 'BEST_SELLING',
reverse: false,
}
break
case 'latest-desc':
output = {
sortKey: isCategory ? 'CREATED' : 'CREATED_AT',
reverse: true,
}
break
}
return output
}
export default getSortVariables

View File

@ -0,0 +1,27 @@
import { SwellConfig } from '../api'
export type BrandNode = {
name: string
path: string
}
export type BrandEdge = {
node: BrandNode
}
export type Brands = BrandEdge[]
const getVendors = async (config: SwellConfig) => {
const vendors: [string] =
(await config.fetch('attributes', 'get', ['brand']))?.values ?? []
return [...new Set(vendors)].map((v) => ({
node: {
entityId: v,
name: v,
path: `brands/${v}`,
},
}))
}
export default getVendors

View File

@ -0,0 +1,19 @@
import { CommerceError } from '@commerce/utils/errors'
type SwellFetchResponse = {
error: {
message: string
code?: string
}
}
const handleFetchResponse = async (res: SwellFetchResponse) => {
if (res) {
if (res.error) {
throw new CommerceError(res.error)
}
return res
}
}
export default handleFetchResponse

View File

@ -0,0 +1,39 @@
import { ValidationError } from '@commerce/utils/errors'
import { setCustomerToken } from './customer-token'
const getErrorMessage = ({
code,
message,
}: {
code: string
message: string
}) => {
switch (code) {
case 'UNIDENTIFIED_CUSTOMER':
message = 'Cannot find an account that matches the provided credentials'
break
}
return message
}
const handleLogin = (data: any) => {
const response = data.customerAccessTokenCreate
const errors = response?.customerUserErrors
if (errors && errors.length) {
throw new ValidationError({
message: getErrorMessage(errors[0]),
})
}
const customerAccessToken = response?.customerAccessToken
const accessToken = customerAccessToken?.accessToken
if (accessToken) {
setCustomerToken(accessToken)
}
return customerAccessToken
}
export default handleLogin

View File

@ -0,0 +1,9 @@
export { default as handleFetchResponse } from './handle-fetch-response'
export { default as getSearchVariables } from './get-search-variables'
export { default as getSortVariables } from './get-sort-variables'
export { default as getVendors } from './get-vendors'
export { default as getCategories } from './get-categories'
export { default as getCheckoutId } from './get-checkout-id'
export * from './normalize'
export * from './customer-token'

View File

@ -0,0 +1,219 @@
import { Product, Customer } from '@commerce/types'
import { MoneyV2, ProductOption } from '../schema'
import type {
Cart,
CartLineItem,
SwellCustomer,
SwellProduct,
SwellImage,
SwellVariant,
ProductOptionValue,
SwellCart,
LineItem,
} from '../types'
const money = ({ amount, currencyCode }: MoneyV2) => {
return {
value: +amount,
currencyCode,
}
}
type normalizedProductOption = {
__typename?: string
id: string
displayName: string
values: ProductOptionValue[]
}
const normalizeProductOption = ({
id,
name: displayName = '',
values = [],
}: ProductOption) => {
let returnValues = values.map((value) => {
let output: any = {
label: value.name,
id: value?.id || id,
}
if (displayName.match(/colou?r/gi)) {
output = {
...output,
hexColors: [value.name],
}
}
return output
})
return {
__typename: 'MultipleChoiceOption',
id,
displayName,
values: returnValues,
}
}
const normalizeProductImages = (images: SwellImage[]) => {
if (!images || images.length < 1) {
return [{ url: '/' }]
}
return images?.map(({ file, ...rest }: SwellImage) => ({
url: file?.url + '',
height: Number(file?.height),
width: Number(file?.width),
...rest,
}))
}
const normalizeProductVariants = (
variants: SwellVariant[],
productOptions: normalizedProductOption[]
) => {
return variants?.map(
({ id, name, price, option_value_ids: optionValueIds = [] }) => {
const values = name
.split(',')
.map((i) => ({ name: i.trim(), label: i.trim() }))
const options = optionValueIds.map((id) => {
const matchingOption = productOptions.find((option) => {
return option.values.find(
(value: ProductOptionValue) => value.id == id
)
})
return normalizeProductOption({
id,
name: matchingOption?.displayName ?? '',
values,
})
})
return {
id,
name,
// sku: sku ?? id,
price: price ?? null,
listPrice: price ?? null,
// requiresShipping: true,
options,
}
}
)
}
export function normalizeProduct(swellProduct: SwellProduct): Product {
const {
id,
name,
description,
images,
options,
slug,
variants,
price: value,
currency: currencyCode,
} = swellProduct
// ProductView accesses variants for each product
const emptyVariants = [{ options: [], id, name }]
const productOptions = options
? options.map((o) => normalizeProductOption(o))
: []
const productVariants = variants
? normalizeProductVariants(variants, productOptions)
: []
const productImages = normalizeProductImages(images)
const product = {
...swellProduct,
description,
id,
vendor: '',
path: `/${slug}`,
images: productImages,
variants:
productVariants && productVariants.length
? productVariants
: emptyVariants,
options: productOptions,
price: {
value,
currencyCode,
},
}
return product
}
export function normalizeCart({
id,
account_id,
date_created,
currency,
tax_included_total,
items,
sub_total,
grand_total,
discounts,
}: SwellCart) {
const cart: Cart = {
id: id,
customerId: account_id + '',
email: '',
createdAt: date_created,
currency: { code: currency },
taxesIncluded: tax_included_total > 0,
lineItems: items?.map(normalizeLineItem) ?? [],
lineItemsSubtotalPrice: +sub_total,
subtotalPrice: +sub_total,
totalPrice: grand_total,
discounts: discounts?.map((discount) => ({ value: discount.amount })),
}
return cart
}
export function normalizeCustomer(customer: SwellCustomer): Customer {
const { first_name: firstName, last_name: lastName } = customer
return {
...customer,
firstName,
lastName,
}
}
function normalizeLineItem({
id,
product,
price,
variant,
quantity,
}: CartLineItem): LineItem {
const item = {
id,
variantId: variant?.id,
productId: product.id ?? '',
name: product?.name ?? '',
quantity,
variant: {
id: variant?.id ?? '',
sku: variant?.sku ?? '',
name: variant?.name!,
image: {
url:
product?.images && product.images.length > 0
? product?.images[0].file.url
: '/',
},
requiresShipping: false,
price: price,
listPrice: price,
},
path: '',
discounts: [],
options: [
{
value: variant?.name,
},
],
}
return item
}

View File

@ -0,0 +1,13 @@
export const getCheckoutIdFromStorage = (token: string) => {
if (window && window.sessionStorage) {
return window.sessionStorage.getItem(token)
}
return null
}
export const setCheckoutIdInStorage = (token: string, id: string | number) => {
if (window && window.sessionStorage) {
return window.sessionStorage.setItem(token, id + '')
}
}

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,46 @@
// TODO: replace this hook and other wishlist hooks with a handler, or remove them if
// Swell doesn't have a wishlist
import { HookFetcher } from '@commerce/utils/types'
import { Product } from '../schema'
const defaultOpts = {}
export type Wishlist = {
items: [
{
product_id: number
variant_id: number
id: number
product: Product
}
]
}
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)

View File

@ -7,7 +7,7 @@ const {
const provider = commerce.provider || getProviderName()
const isBC = provider === 'bigcommerce'
const isShopify = provider === 'shopify'
const isRC = provider === 'reactioncommerce'
const isSwell = provider === 'swell'
module.exports = withCommerceConfig({
env: {
@ -20,7 +20,7 @@ module.exports = withCommerceConfig({
},
rewrites() {
return [
(isBC || isShopify) && {
(isBC || isShopify || isSwell) && {
source: '/checkout',
destination: '/api/bigcommerce/checkout',
},

View File

@ -36,16 +36,17 @@
"next": "^10.0.9-canary.5",
"next-seo": "^4.11.0",
"next-themes": "^0.0.4",
"postcss": "^8.2.6",
"postcss": "^8.2.8",
"postcss-nesting": "^7.0.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-merge-refs": "^1.1.0",
"react-ticker": "^1.2.2",
"shopify-buy": "^2.11.0",
"swell-js": "^4.0.0-next.0",
"swr": "^0.4.0",
"tabbable": "^5.1.5",
"tailwindcss": "^2.0.3"
"tailwindcss": "^2.0.4"
},
"devDependencies": {
"@graphql-codegen/cli": "^1.20.0",
@ -54,6 +55,7 @@
"@graphql-codegen/typescript-operations": "^1.17.13",
"@manifoldco/swagger-to-ts": "^2.1.0",
"@next/bundle-analyzer": "^10.0.1",
"@tailwindcss/jit": "^0.1.3",
"@types/body-scroll-lock": "^2.6.1",
"@types/classnames": "^2.2.10",
"@types/cookie": "^0.4.0",

View File

@ -1,97 +0,0 @@
import type { GetStaticPropsContext } from 'next'
import { getConfig } from '@framework/api'
import getAllPages from '@framework/common/get-all-pages'
import { Layout } from '@components/common'
import { Container } from '@components/ui'
export async function getStaticProps({
preview,
locale,
}: GetStaticPropsContext) {
const config = getConfig({ locale })
const { pages } = await getAllPages({ config, preview })
return {
props: { pages },
}
}
export default function Blog() {
return (
<div className="pb-20">
<div className="text-center pt-40 pb-56 bg-violet">
<Container>
<h2 className="text-4xl tracking-tight leading-10 font-extrabold text-white sm:text-5xl sm:leading-none md:text-6xl">
Welcome to Acme, the simplest way to start publishing with Next.js
</h2>
<p className="mt-3 max-w-md mx-auto text-gray-100 sm:text-lg md:mt-5 md:text-xl md:max-w-3xl">
The Yeezy BOOST 350 V2 lineup continues to grow. We recently had the
Carbon iteration, and now release details have been locked in for
this Natural joint. Revealed by Yeezy Mafia earlier this year, the
shoe was originally called Abez, which translated to Tin in
Hebrew. Its now undergone a name change, and will be referred to as
Natura`
</p>
<div className="mt-5 max-w-md mx-auto sm:flex sm:justify-center md:mt-12">
<div className="flex">
<div className="flex-shrink-0 inline-flex rounded-full border-2 border-white">
<img
className="h-12 w-12 rounded-full"
src="https://vercel.com/api/www/avatar/61182a9f6bda512b4d9263c9c8a60aabe0402f4c?s=204"
alt="Avatar"
/>
</div>
<div className="ml-4">
<div className="leading-6 font-medium text-white">
José Rodriguez
</div>
<div className="leading-6 font-medium text-gray-200">
CEO, Acme
</div>
</div>
</div>
</div>
</Container>
</div>
<Container>
<div className="-mt-96 mx-auto">
<img src="/jacket.png" alt="Jacket" />
</div>
{/** Replace by HTML Content */}
<div className="text-lg leading-7 font-medium py-6 text-justify max-w-6xl mx-auto">
<p className="py-6">
Biscuit oat cake wafer icing ice cream tiramisu pudding cupcake.
Candy canes bonbon dragée jujubes chocolate bar. Cotton candy gummi
bears toffee cake muffin caramels. Gummi bears danish liquorice ice
cream pie chocolate cake lemon drops tootsie roll tart. Biscuit
gingerbread fruitcake cake powder pudding cotton candy chocolate
bar. Sweet donut marshmallow powder gummies jelly tart powder.
Cheesecake bonbon caramels cupcake jujubes halvah donut dessert
chocolate bar. Jelly gummies liquorice lollipop chocolate bar
chocolate cake sugar plum. Lollipop toffee dragée chocolate bar
jelly beans biscuit. Halvah danish cheesecake. Tiramisu donut
lollipop pie donut caramels tiramisu. Jujubes candy canes pudding
danish fruitcake chupa chups jujubes carrot cake bonbon. Halvah
donut jelly halvah bonbon.
</p>
<p className="py-6">
Biscuit sugar plum sweet chocolate cake sesame snaps soufflé
topping. Gummies topping bonbon chocolate pudding cookie. Wafer
icing cake pastry. Gummies candy dessert chupa chups lemon drops.
Soufflé marshmallow oat cake chocolate jelly-o caramels pie marzipan
jelly beans. Cheesecake liquorice donut jujubes halvah ice cream
cotton candy cupcake sugar plum. Ice cream ice cream sweet roll
fruitcake icing. Muffin candy canes bonbon croissant gummies lemon
drops pie danish. Oat cake chocolate toffee cake jelly tart
caramels. Sweet donut cheesecake pastry pie sweet. Bonbon lollipop
brownie. Soufflé pudding macaroon cotton candy gingerbread. Biscuit
macaroon gummi bears candy canes chocolate cake lemon drops
marshmallow. Chocolate cake cotton candy marshmallow cake sweet
tootsie roll bonbon carrot cake sugar plum.
</p>
</div>
</Container>
</div>
)
}
Blog.Layout = Layout

View File

@ -78,7 +78,7 @@ export default function Search({
const { data } = useSearch({
search: typeof q === 'string' ? q : '',
categoryId: activeCategory?.entityId,
brandId: activeBrand?.entityId,
brandId: (activeBrand as any)?.entityId,
sort: typeof sort === 'string' ? sort : '',
shopId,
})

1269
yarn.lock

File diff suppressed because it is too large Load Diff