mirror of
https://github.com/vercel/commerce.git
synced 2025-06-19 05:31:22 +00:00
Merge branch 'master' of https://github.com/vercel/commerce into fixSoldOutProduct
This commit is contained in:
commit
05ee6d2bee
@ -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=
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -18,6 +18,7 @@ out/
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
.idea
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
|
23
README.md
23
README.md
@ -7,8 +7,10 @@ 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/
|
||||
- Vendure Demo: https://vendure.vercel.store
|
||||
|
||||
## Features
|
||||
|
||||
@ -40,6 +42,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`
|
||||
|
@ -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 {
|
||||
@screen md {
|
||||
.root {
|
||||
@apply flex text-left;
|
||||
}
|
||||
}
|
||||
|
@ -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!}>
|
||||
|
@ -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 {
|
||||
@screen lg {
|
||||
.dropdownMenu {
|
||||
@apply absolute border border-accents-1 shadow-lg w-56 h-auto;
|
||||
}
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
@screen md {
|
||||
@screen md {
|
||||
.closeButton {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
@screen lg {
|
||||
.dropdownMenu {
|
||||
@apply absolute top-10 border border-accents-1 shadow-lg w-56 h-auto;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -1,6 +1,9 @@
|
||||
.root {
|
||||
@apply mx-auto grid grid-cols-1 py-32 gap-4;
|
||||
@screen md {
|
||||
}
|
||||
|
||||
@screen md {
|
||||
.root {
|
||||
@apply grid-cols-2;
|
||||
}
|
||||
}
|
||||
|
@ -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" />
|
||||
|
@ -7,7 +7,7 @@ const fs = require('fs')
|
||||
const merge = require('deepmerge')
|
||||
const prettier = require('prettier')
|
||||
|
||||
const PROVIDERS = ['bigcommerce', 'shopify']
|
||||
const PROVIDERS = ['bigcommerce', 'shopify', 'swell', 'vendure']
|
||||
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
5
framework/swell/.env.template
Normal file
5
framework/swell/.env.template
Normal file
@ -0,0 +1,5 @@
|
||||
SWELL_STORE_DOMAIN=
|
||||
SWELL_STOREFRONT_ACCESS_TOKEN=
|
||||
|
||||
NEXT_PUBLIC_SWELL_STORE_ID=
|
||||
NEXT_PUBLIC_SWELL_PUBLIC_KEY=
|
1
framework/swell/api/cart/index.ts
Normal file
1
framework/swell/api/cart/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/swell/api/catalog/index.ts
Normal file
1
framework/swell/api/catalog/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/swell/api/catalog/products.ts
Normal file
1
framework/swell/api/catalog/products.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
20
framework/swell/api/checkout/index.ts
Normal file
20
framework/swell/api/checkout/index.ts
Normal 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, {}, {})
|
1
framework/swell/api/customer.ts
Normal file
1
framework/swell/api/customer.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/swell/api/customers/index.ts
Normal file
1
framework/swell/api/customers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/swell/api/customers/login.ts
Normal file
1
framework/swell/api/customers/login.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/swell/api/customers/logout.ts
Normal file
1
framework/swell/api/customers/logout.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/swell/api/customers/signup.ts
Normal file
1
framework/swell/api/customers/signup.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
50
framework/swell/api/index.ts
Normal file
50
framework/swell/api/index.ts
Normal 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)
|
||||
}
|
25
framework/swell/api/operations/get-page.ts
Normal file
25
framework/swell/api/operations/get-page.ts
Normal 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
|
58
framework/swell/api/utils/create-api-handler.ts
Normal file
58
framework/swell/api/utils/create-api-handler.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
7
framework/swell/api/utils/fetch-swell-api.ts
Normal file
7
framework/swell/api/utils/fetch-swell-api.ts
Normal 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
|
2
framework/swell/api/utils/fetch.ts
Normal file
2
framework/swell/api/utils/fetch.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import zeitFetch from '@vercel/fetch'
|
||||
export default zeitFetch()
|
28
framework/swell/api/utils/is-allowed-method.ts
Normal file
28
framework/swell/api/utils/is-allowed-method.ts
Normal 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
|
||||
}
|
2
framework/swell/api/wishlist/index.tsx
Normal file
2
framework/swell/api/wishlist/index.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
export type WishlistItem = { product: any; id: number }
|
||||
export default function () {}
|
74
framework/swell/auth/use-login.tsx
Normal file
74
framework/swell/auth/use-login.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
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]
|
||||
)
|
||||
},
|
||||
}
|
36
framework/swell/auth/use-logout.tsx
Normal file
36
framework/swell/auth/use-logout.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
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]
|
||||
)
|
||||
},
|
||||
}
|
65
framework/swell/auth/use-signup.tsx
Normal file
65
framework/swell/auth/use-signup.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
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]
|
||||
)
|
||||
},
|
||||
}
|
3
framework/swell/cart/index.ts
Normal file
3
framework/swell/cart/index.ts
Normal 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'
|
59
framework/swell/cart/use-add-item.tsx
Normal file
59
framework/swell/cart/use-add-item.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
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]
|
||||
)
|
||||
},
|
||||
}
|
37
framework/swell/cart/use-cart.tsx
Normal file
37
framework/swell/cart/use-cart.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
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]
|
||||
)
|
||||
},
|
||||
}
|
71
framework/swell/cart/use-remove-item.tsx
Normal file
71
framework/swell/cart/use-remove-item.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
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])
|
||||
},
|
||||
}
|
95
framework/swell/cart/use-update-item.tsx
Normal file
95
framework/swell/cart/use-update-item.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
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]
|
||||
)
|
||||
},
|
||||
}
|
28
framework/swell/cart/utils/checkout-create.ts
Normal file
28
framework/swell/cart/utils/checkout-create.ts
Normal 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
|
26
framework/swell/cart/utils/checkout-to-cart.ts
Normal file
26
framework/swell/cart/utils/checkout-to-cart.ts
Normal 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
|
33
framework/swell/cart/utils/fetcher.ts
Normal file
33
framework/swell/cart/utils/fetcher.ts
Normal 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
|
2
framework/swell/cart/utils/index.ts
Normal file
2
framework/swell/cart/utils/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as checkoutToCart } from './checkout-to-cart'
|
||||
export { default as checkoutCreate } from './checkout-create'
|
6
framework/swell/commerce.config.json
Normal file
6
framework/swell/commerce.config.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"provider": "swell",
|
||||
"features": {
|
||||
"wishlist": false
|
||||
}
|
||||
}
|
37
framework/swell/common/get-all-pages.ts
Normal file
37
framework/swell/common/get-all-pages.ts
Normal 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
|
33
framework/swell/common/get-page.ts
Normal file
33
framework/swell/common/get-page.ts
Normal 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
|
31
framework/swell/common/get-site-info.ts
Normal file
31
framework/swell/common/get-site-info.ts
Normal 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
13
framework/swell/const.ts
Normal 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
|
1
framework/swell/customer/index.ts
Normal file
1
framework/swell/customer/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as useCustomer } from './use-customer'
|
50
framework/swell/customer/use-customer.tsx
Normal file
50
framework/swell/customer/use-customer.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
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;
|
28
framework/swell/fetcher.ts
Normal file
28
framework/swell/fetcher.ts
Normal 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
47
framework/swell/index.tsx
Normal 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()
|
8
framework/swell/next.config.js
Normal file
8
framework/swell/next.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
const commerce = require('./commerce.config.json')
|
||||
|
||||
module.exports = {
|
||||
commerce,
|
||||
images: {
|
||||
domains: ['cdn.schema.io'],
|
||||
},
|
||||
}
|
28
framework/swell/product/get-all-collections.ts
Normal file
28
framework/swell/product/get-all-collections.ts
Normal 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
|
39
framework/swell/product/get-all-product-paths.ts
Normal file
39
framework/swell/product/get-all-product-paths.ts
Normal 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
|
36
framework/swell/product/get-all-products.ts
Normal file
36
framework/swell/product/get-all-products.ts
Normal 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
|
32
framework/swell/product/get-product.ts
Normal file
32
framework/swell/product/get-product.ts
Normal 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
|
2
framework/swell/product/use-price.tsx
Normal file
2
framework/swell/product/use-price.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
export * from '@commerce/product/use-price'
|
||||
export { default } from '@commerce/product/use-price'
|
71
framework/swell/product/use-search.tsx
Normal file
71
framework/swell/product/use-search.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
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,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
31
framework/swell/provider.ts
Normal file
31
framework/swell/provider.ts
Normal 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
5002
framework/swell/schema.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
9631
framework/swell/schema.graphql
Normal file
9631
framework/swell/schema.graphql
Normal file
File diff suppressed because it is too large
Load Diff
1
framework/swell/swell-js.d.ts
vendored
Normal file
1
framework/swell/swell-js.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module 'swell-js'
|
122
framework/swell/types.ts
Normal file
122
framework/swell/types.ts
Normal file
@ -0,0 +1,122 @@
|
||||
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
|
21
framework/swell/utils/customer-token.ts
Normal file
21
framework/swell/utils/customer-token.ts
Normal 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,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
20
framework/swell/utils/get-categories.ts
Normal file
20
framework/swell/utils/get-categories.ts
Normal 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
|
8
framework/swell/utils/get-checkout-id.ts
Normal file
8
framework/swell/utils/get-checkout-id.ts
Normal 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
|
27
framework/swell/utils/get-search-variables.ts
Normal file
27
framework/swell/utils/get-search-variables.ts
Normal 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
|
32
framework/swell/utils/get-sort-variables.ts
Normal file
32
framework/swell/utils/get-sort-variables.ts
Normal 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
|
27
framework/swell/utils/get-vendors.ts
Normal file
27
framework/swell/utils/get-vendors.ts
Normal 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
|
19
framework/swell/utils/handle-fetch-response.ts
Normal file
19
framework/swell/utils/handle-fetch-response.ts
Normal 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
|
39
framework/swell/utils/handle-login.ts
Normal file
39
framework/swell/utils/handle-login.ts
Normal 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
|
9
framework/swell/utils/index.ts
Normal file
9
framework/swell/utils/index.ts
Normal 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'
|
219
framework/swell/utils/normalize.ts
Normal file
219
framework/swell/utils/normalize.ts
Normal 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
|
||||
}
|
13
framework/swell/utils/storage.ts
Normal file
13
framework/swell/utils/storage.ts
Normal 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 + '')
|
||||
}
|
||||
}
|
13
framework/swell/wishlist/use-add-item.tsx
Normal file
13
framework/swell/wishlist/use-add-item.tsx
Normal 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
|
17
framework/swell/wishlist/use-remove-item.tsx
Normal file
17
framework/swell/wishlist/use-remove-item.tsx
Normal 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
|
46
framework/swell/wishlist/use-wishlist.tsx
Normal file
46
framework/swell/wishlist/use-wishlist.tsx
Normal 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)
|
1
framework/vendure/.env.template
Normal file
1
framework/vendure/.env.template
Normal file
@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_VENDURE_SHOP_API_URL=http://localhost:3001/shop-api
|
33
framework/vendure/README.md
Normal file
33
framework/vendure/README.md
Normal file
@ -0,0 +1,33 @@
|
||||
# Vendure Storefront Data Hooks
|
||||
|
||||
UI hooks and data fetching methods built from the ground up for e-commerce applications written in React, that use [Vendure](http://vendure.io/) as a headless e-commerce platform.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Clone this repo and install its dependencies with `yarn install` or `npm install`
|
||||
2. Set the Vendure provider and API URL in your `.env.local` file:
|
||||
```
|
||||
COMMERCE_PROVIDER=vendure
|
||||
NEXT_PUBLIC_VENDURE_SHOP_API_URL=https://demo.vendure.io/shop-api
|
||||
NEXT_PUBLIC_VENDURE_LOCAL_URL=/vendure-shop-api
|
||||
```
|
||||
3. With the Vendure server running, start this project using `yarn dev` or `npm run dev`.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. Vendure does not ship with built-in wishlist functionality.
|
||||
2. Nor does it come with any kind of blog/page-building feature. Both of these can be created as Vendure plugins, however.
|
||||
3. The entire Vendure customer flow is carried out via its GraphQL API. This means that there is no external, pre-existing checkout flow. The checkout flow must be created as part of the Next.js app. See https://github.com/vercel/commerce/issues/64 for further discusion.
|
||||
4. By default, the sign-up flow in Vendure uses email verification. This means that using the existing "sign up" flow from this project will not grant a new user the ability to authenticate, since the new account must first be verified. Again, the necessary parts to support this flow can be created as part of the Next.js app.
|
||||
|
||||
## Code generation
|
||||
|
||||
This provider makes use of GraphQL code generation. The [schema.graphql](./schema.graphql) and [schema.d.ts](./schema.d.ts) files contain the generated types & schema introspection results.
|
||||
|
||||
When developing the provider, changes to any GraphQL operations should be followed by re-generation of the types and schema files:
|
||||
|
||||
From the project root dir, run
|
||||
|
||||
```sh
|
||||
graphql-codegen --config ./framework/vendure/codegen.json
|
||||
```
|
1
framework/vendure/api/cart/index.ts
Normal file
1
framework/vendure/api/cart/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/vendure/api/catalog/index.ts
Normal file
1
framework/vendure/api/catalog/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/vendure/api/catalog/products.ts
Normal file
1
framework/vendure/api/catalog/products.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
60
framework/vendure/api/checkout/index.ts
Normal file
60
framework/vendure/api/checkout/index.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { NextApiHandler } from 'next'
|
||||
|
||||
const checkoutApi = async (req: any, res: any, config: any) => {
|
||||
try {
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Checkout</title>
|
||||
</head>
|
||||
<body>
|
||||
<div style='margin: 10rem auto; text-align: center; font-family: SansSerif, "Segoe UI", Helvetica'>
|
||||
<h1>Checkout not implemented :(</h1>
|
||||
<p>
|
||||
See <a href='https://github.com/vercel/commerce/issues/64' target='_blank'>#64</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
res.status(200)
|
||||
res.setHeader('Content-Type', 'text/html')
|
||||
res.write(html)
|
||||
res.end()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
const message = 'An unexpected error ocurred'
|
||||
|
||||
res.status(500).json({ data: null, errors: [{ message }] })
|
||||
}
|
||||
}
|
||||
|
||||
export function createApiHandler<T = any, H = {}, Options extends {} = {}>(
|
||||
handler: any,
|
||||
handlers: H,
|
||||
defaultOptions: Options
|
||||
) {
|
||||
return function getApiHandler({
|
||||
config,
|
||||
operations,
|
||||
options,
|
||||
}: {
|
||||
config?: any
|
||||
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, config, ops, opts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default createApiHandler(checkoutApi, {}, {})
|
1
framework/vendure/api/customers/index.ts
Normal file
1
framework/vendure/api/customers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/vendure/api/customers/login.ts
Normal file
1
framework/vendure/api/customers/login.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/vendure/api/customers/logout.ts
Normal file
1
framework/vendure/api/customers/logout.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/vendure/api/customers/signup.ts
Normal file
1
framework/vendure/api/customers/signup.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
51
framework/vendure/api/index.ts
Normal file
51
framework/vendure/api/index.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import type { CommerceAPIConfig } from '@commerce/api'
|
||||
import fetchGraphqlApi from './utils/fetch-graphql-api'
|
||||
|
||||
export interface VendureConfig extends CommerceAPIConfig {}
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL
|
||||
|
||||
if (!API_URL) {
|
||||
throw new Error(
|
||||
`The environment variable NEXT_PUBLIC_VENDURE_SHOP_API_URL is missing and it's required to access your store`
|
||||
)
|
||||
}
|
||||
|
||||
export class Config {
|
||||
private config: VendureConfig
|
||||
|
||||
constructor(config: VendureConfig) {
|
||||
this.config = {
|
||||
...config,
|
||||
}
|
||||
}
|
||||
|
||||
getConfig(userConfig: Partial<VendureConfig> = {}) {
|
||||
return Object.entries(userConfig).reduce<VendureConfig>(
|
||||
(cfg, [key, value]) => Object.assign(cfg, { [key]: value }),
|
||||
{ ...this.config }
|
||||
)
|
||||
}
|
||||
|
||||
setConfig(newConfig: Partial<VendureConfig>) {
|
||||
Object.assign(this.config, newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
const ONE_DAY = 60 * 60 * 24
|
||||
const config = new Config({
|
||||
commerceUrl: API_URL,
|
||||
apiToken: '',
|
||||
cartCookie: '',
|
||||
customerCookie: '',
|
||||
cartCookieMaxAge: ONE_DAY * 30,
|
||||
fetch: fetchGraphqlApi,
|
||||
})
|
||||
|
||||
export function getConfig(userConfig?: Partial<VendureConfig>) {
|
||||
return config.getConfig(userConfig)
|
||||
}
|
||||
|
||||
export function setConfig(newConfig: Partial<VendureConfig>) {
|
||||
return config.setConfig(newConfig)
|
||||
}
|
37
framework/vendure/api/utils/fetch-graphql-api.ts
Normal file
37
framework/vendure/api/utils/fetch-graphql-api.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { FetcherError } from '@commerce/utils/errors'
|
||||
import type { GraphQLFetcher } from '@commerce/api'
|
||||
import { getConfig } from '..'
|
||||
import fetch from './fetch'
|
||||
|
||||
const fetchGraphqlApi: GraphQLFetcher = async (
|
||||
query: string,
|
||||
{ variables, preview } = {},
|
||||
fetchOptions
|
||||
) => {
|
||||
const config = getConfig()
|
||||
const res = await fetch(config.commerceUrl, {
|
||||
...fetchOptions,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.apiToken}`,
|
||||
...fetchOptions?.headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
variables,
|
||||
}),
|
||||
})
|
||||
|
||||
const json = await res.json()
|
||||
if (json.errors) {
|
||||
throw new FetcherError({
|
||||
errors: json.errors ?? [{ message: 'Failed to fetch Vendure API' }],
|
||||
status: res.status,
|
||||
})
|
||||
}
|
||||
|
||||
return { data: json.data, res }
|
||||
}
|
||||
|
||||
export default fetchGraphqlApi
|
3
framework/vendure/api/utils/fetch.ts
Normal file
3
framework/vendure/api/utils/fetch.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import zeitFetch from '@vercel/fetch'
|
||||
|
||||
export default zeitFetch()
|
2
framework/vendure/api/wishlist/index.tsx
Normal file
2
framework/vendure/api/wishlist/index.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
export type WishlistItem = { product: any; id: number }
|
||||
export default function () {}
|
50
framework/vendure/auth/use-login.tsx
Normal file
50
framework/vendure/auth/use-login.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { useCallback } from 'react'
|
||||
import { MutationHook } from '@commerce/utils/types'
|
||||
import useLogin, { UseLogin } from '@commerce/auth/use-login'
|
||||
import { CommerceError, ValidationError } from '@commerce/utils/errors'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import { LoginMutation, LoginMutationVariables } from '../schema'
|
||||
import { loginMutation } from '../lib/mutations/log-in-mutation'
|
||||
|
||||
export default useLogin as UseLogin<typeof handler>
|
||||
|
||||
export const handler: MutationHook<null, {}, any> = {
|
||||
fetchOptions: {
|
||||
query: loginMutation,
|
||||
},
|
||||
async fetcher({ input: { email, password }, options, fetch }) {
|
||||
if (!(email && password)) {
|
||||
throw new CommerceError({
|
||||
message: 'A email and password are required to login',
|
||||
})
|
||||
}
|
||||
|
||||
const variables: LoginMutationVariables = {
|
||||
username: email,
|
||||
password,
|
||||
}
|
||||
|
||||
const { login } = await fetch<LoginMutation>({
|
||||
...options,
|
||||
variables,
|
||||
})
|
||||
|
||||
if (login.__typename !== 'CurrentUser') {
|
||||
throw new ValidationError(login)
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { revalidate } = useCustomer()
|
||||
|
||||
return useCallback(
|
||||
async function login(input) {
|
||||
const data = await fetch({ input })
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fetch, revalidate]
|
||||
)
|
||||
},
|
||||
}
|
32
framework/vendure/auth/use-logout.tsx
Normal file
32
framework/vendure/auth/use-logout.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { useCallback } from 'react'
|
||||
import { MutationHook } from '@commerce/utils/types'
|
||||
import useLogout, { UseLogout } from '@commerce/auth/use-logout'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import { LogoutMutation } from '../schema'
|
||||
import { logoutMutation } from '../lib/mutations/log-out-mutation'
|
||||
|
||||
export default useLogout as UseLogout<typeof handler>
|
||||
|
||||
export const handler: MutationHook<null> = {
|
||||
fetchOptions: {
|
||||
query: logoutMutation,
|
||||
},
|
||||
async fetcher({ options, fetch }) {
|
||||
await fetch<LogoutMutation>({
|
||||
...options,
|
||||
})
|
||||
return null
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { mutate } = useCustomer()
|
||||
|
||||
return useCallback(
|
||||
async function logout() {
|
||||
const data = await fetch()
|
||||
await mutate(null, false)
|
||||
return data
|
||||
},
|
||||
[fetch, mutate]
|
||||
)
|
||||
},
|
||||
}
|
68
framework/vendure/auth/use-signup.tsx
Normal file
68
framework/vendure/auth/use-signup.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import { useCallback } from 'react'
|
||||
import { MutationHook } from '@commerce/utils/types'
|
||||
import { CommerceError, ValidationError } from '@commerce/utils/errors'
|
||||
import useSignup, { UseSignup } from '@commerce/auth/use-signup'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import {
|
||||
RegisterCustomerInput,
|
||||
SignupMutation,
|
||||
SignupMutationVariables,
|
||||
} from '../schema'
|
||||
import { signupMutation } from '../lib/mutations/sign-up-mutation'
|
||||
|
||||
export default useSignup as UseSignup<typeof handler>
|
||||
|
||||
export type SignupInput = {
|
||||
email: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export const handler: MutationHook<null, {}, SignupInput, SignupInput> = {
|
||||
fetchOptions: {
|
||||
query: signupMutation,
|
||||
},
|
||||
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 variables: SignupMutationVariables = {
|
||||
input: {
|
||||
firstName,
|
||||
lastName,
|
||||
emailAddress: email,
|
||||
password,
|
||||
},
|
||||
}
|
||||
const { registerCustomerAccount } = await fetch<SignupMutation>({
|
||||
...options,
|
||||
variables,
|
||||
})
|
||||
|
||||
if (registerCustomerAccount.__typename !== 'Success') {
|
||||
throw new ValidationError(registerCustomerAccount)
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { revalidate } = useCustomer()
|
||||
|
||||
return useCallback(
|
||||
async function signup(input) {
|
||||
const data = await fetch({ input })
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fetch, revalidate]
|
||||
)
|
||||
},
|
||||
}
|
5
framework/vendure/cart/index.ts
Normal file
5
framework/vendure/cart/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
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 useWishlistActions } from './use-cart-actions'
|
||||
export { default as useUpdateItem } from './use-cart-actions'
|
52
framework/vendure/cart/use-add-item.tsx
Normal file
52
framework/vendure/cart/use-add-item.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { Cart, CartItemBody } from '@commerce/types'
|
||||
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import { MutationHook } from '@commerce/utils/types'
|
||||
import { useCallback } from 'react'
|
||||
import useCart from './use-cart'
|
||||
import { AddItemToOrderMutation } from '../schema'
|
||||
import { normalizeCart } from '../lib/normalize'
|
||||
import { addItemToOrderMutation } from '../lib/mutations/add-item-to-order-mutation'
|
||||
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export const handler: MutationHook<Cart, {}, CartItemBody> = {
|
||||
fetchOptions: {
|
||||
query: addItemToOrderMutation,
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {
|
||||
if (
|
||||
input.quantity &&
|
||||
(!Number.isInteger(input.quantity) || input.quantity! < 1)
|
||||
) {
|
||||
throw new CommerceError({
|
||||
message: 'The item quantity has to be a valid integer greater than 0',
|
||||
})
|
||||
}
|
||||
|
||||
const { addItemToOrder } = await fetch<AddItemToOrderMutation>({
|
||||
...options,
|
||||
variables: {
|
||||
quantity: input.quantity || 1,
|
||||
variantId: input.variantId,
|
||||
},
|
||||
})
|
||||
|
||||
if (addItemToOrder.__typename === 'Order') {
|
||||
return normalizeCart(addItemToOrder)
|
||||
}
|
||||
throw new CommerceError(addItemToOrder)
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { mutate } = useCart()
|
||||
|
||||
return useCallback(
|
||||
async function addItem(input) {
|
||||
const data = await fetch({ input })
|
||||
await mutate(data, false)
|
||||
return data
|
||||
},
|
||||
[fetch, mutate]
|
||||
)
|
||||
},
|
||||
}
|
13
framework/vendure/cart/use-cart-actions.tsx
Normal file
13
framework/vendure/cart/use-cart-actions.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import useAddItem from './use-add-item'
|
||||
import useRemoveItem from './use-remove-item'
|
||||
import useUpdateItem from './use-update-item'
|
||||
|
||||
// This hook is probably not going to be used, but it's here
|
||||
// to show how a commerce should be structuring it
|
||||
export default function useCartActions() {
|
||||
const addItem = useAddItem()
|
||||
const updateItem = useUpdateItem()
|
||||
const removeItem = useRemoveItem()
|
||||
|
||||
return { addItem, updateItem, removeItem }
|
||||
}
|
49
framework/vendure/cart/use-cart.tsx
Normal file
49
framework/vendure/cart/use-cart.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { Cart } from '@commerce/types'
|
||||
import { SWRHook } from '@commerce/utils/types'
|
||||
import useCart, { FetchCartInput, UseCart } from '@commerce/cart/use-cart'
|
||||
import { ActiveOrderQuery, CartFragment } from '../schema'
|
||||
import { normalizeCart } from '../lib/normalize'
|
||||
import { useMemo } from 'react'
|
||||
import { getCartQuery } from '../lib/queries/get-cart-query'
|
||||
|
||||
export type CartResult = {
|
||||
activeOrder?: CartFragment
|
||||
addItemToOrder?: CartFragment
|
||||
adjustOrderLine?: CartFragment
|
||||
removeOrderLine?: CartFragment
|
||||
}
|
||||
|
||||
export default useCart as UseCart<typeof handler>
|
||||
|
||||
export const handler: SWRHook<
|
||||
Cart | null,
|
||||
{},
|
||||
FetchCartInput,
|
||||
{ isEmpty?: boolean }
|
||||
> = {
|
||||
fetchOptions: {
|
||||
query: getCartQuery,
|
||||
},
|
||||
async fetcher({ input: { cartId }, options, fetch }) {
|
||||
const { activeOrder } = await fetch<ActiveOrderQuery>(options)
|
||||
return activeOrder ? normalizeCart(activeOrder) : 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]
|
||||
)
|
||||
},
|
||||
}
|
48
framework/vendure/cart/use-remove-item.tsx
Normal file
48
framework/vendure/cart/use-remove-item.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { useCallback } from 'react'
|
||||
import { HookFetcherContext, MutationHookContext } from '@commerce/utils/types'
|
||||
import useRemoveItem, { UseRemoveItem } from '@commerce/cart/use-remove-item'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useCart from './use-cart'
|
||||
import {
|
||||
RemoveOrderLineMutation,
|
||||
RemoveOrderLineMutationVariables,
|
||||
} from '../schema'
|
||||
import { Cart, LineItem, RemoveCartItemBody } from '@commerce/types'
|
||||
import { normalizeCart } from '../lib/normalize'
|
||||
import { removeOrderLineMutation } from '../lib/mutations/remove-order-line-mutation'
|
||||
|
||||
export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||
|
||||
export const handler = {
|
||||
fetchOptions: {
|
||||
query: removeOrderLineMutation,
|
||||
},
|
||||
async fetcher({ input, options, fetch }: HookFetcherContext<LineItem>) {
|
||||
const variables: RemoveOrderLineMutationVariables = {
|
||||
orderLineId: input.id,
|
||||
}
|
||||
const { removeOrderLine } = await fetch<RemoveOrderLineMutation>({
|
||||
...options,
|
||||
variables,
|
||||
})
|
||||
|
||||
if (removeOrderLine.__typename === 'Order') {
|
||||
return normalizeCart(removeOrderLine)
|
||||
}
|
||||
throw new CommerceError(removeOrderLine)
|
||||
},
|
||||
useHook: ({
|
||||
fetch,
|
||||
}: MutationHookContext<Cart | null, RemoveCartItemBody>) => (ctx = {}) => {
|
||||
const { mutate } = useCart()
|
||||
|
||||
return useCallback(
|
||||
async function removeItem(input) {
|
||||
const data = await fetch({ input })
|
||||
await mutate(data, false)
|
||||
return data
|
||||
},
|
||||
[fetch, mutate]
|
||||
)
|
||||
},
|
||||
}
|
78
framework/vendure/cart/use-update-item.tsx
Normal file
78
framework/vendure/cart/use-update-item.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import { useCallback } from 'react'
|
||||
import { HookFetcherContext, MutationHookContext } from '@commerce/utils/types'
|
||||
import { CommerceError, ValidationError } from '@commerce/utils/errors'
|
||||
import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item'
|
||||
import {
|
||||
Cart,
|
||||
CartItemBody,
|
||||
LineItem,
|
||||
UpdateCartItemBody,
|
||||
} from '@commerce/types'
|
||||
import useCart from './use-cart'
|
||||
import {
|
||||
AdjustOrderLineMutation,
|
||||
AdjustOrderLineMutationVariables,
|
||||
} from '../schema'
|
||||
import { normalizeCart } from '../lib/normalize'
|
||||
import { adjustOrderLineMutation } from '../lib/mutations/adjust-order-line-mutation'
|
||||
|
||||
export default useUpdateItem as UseUpdateItem<typeof handler>
|
||||
|
||||
export const handler = {
|
||||
fetchOptions: {
|
||||
query: adjustOrderLineMutation,
|
||||
},
|
||||
async fetcher(context: HookFetcherContext<UpdateCartItemBody<CartItemBody>>) {
|
||||
const { input, options, fetch } = context
|
||||
const variables: AdjustOrderLineMutationVariables = {
|
||||
quantity: input.item.quantity || 1,
|
||||
orderLineId: input.itemId,
|
||||
}
|
||||
const { adjustOrderLine } = await fetch<AdjustOrderLineMutation>({
|
||||
...options,
|
||||
variables,
|
||||
})
|
||||
|
||||
if (adjustOrderLine.__typename === 'Order') {
|
||||
return normalizeCart(adjustOrderLine)
|
||||
}
|
||||
throw new CommerceError(adjustOrderLine)
|
||||
},
|
||||
useHook: ({
|
||||
fetch,
|
||||
}: MutationHookContext<Cart | null, UpdateCartItemBody<CartItemBody>>) => (
|
||||
ctx: {
|
||||
item?: LineItem
|
||||
wait?: number
|
||||
} = {}
|
||||
) => {
|
||||
const { item } = ctx
|
||||
const { mutate } = useCart()
|
||||
|
||||
return useCallback(
|
||||
async function addItem(input: Partial<CartItemBody>) {
|
||||
const itemId = item?.id
|
||||
const productId = input.productId ?? item?.productId
|
||||
const variantId = input.productId ?? item?.variantId
|
||||
if (!itemId || !productId || !variantId) {
|
||||
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
|
||||
},
|
||||
[fetch, mutate]
|
||||
)
|
||||
},
|
||||
}
|
28
framework/vendure/codegen.json
Normal file
28
framework/vendure/codegen.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"schema": {
|
||||
"http://localhost:3001/shop-api": {}
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"./framework/vendure/**/*.{ts,tsx}": {
|
||||
"noRequire": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"generates": {
|
||||
"./framework/vendure/schema.d.ts": {
|
||||
"plugins": ["typescript", "typescript-operations"],
|
||||
"config": {
|
||||
"scalars": {
|
||||
"ID": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"./framework/vendure/schema.graphql": {
|
||||
"plugins": ["schema-ast"]
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
"afterAllFileWrite": ["prettier --write"]
|
||||
}
|
||||
}
|
6
framework/vendure/commerce.config.json
Normal file
6
framework/vendure/commerce.config.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"provider": "vendure",
|
||||
"features": {
|
||||
"wishlist": false
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user