mirror of
https://github.com/vercel/commerce.git
synced 2025-07-04 20:21:21 +00:00
Add basic Spree framework structure
This commit is contained in:
parent
dd6ad7556e
commit
a3ef27f5e7
8
framework/spree/.env.template
Normal file
8
framework/spree/.env.template
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Template to be used for creating .env* files (.env, .env.local etc.) in the project's root directory.
|
||||||
|
|
||||||
|
COMMERCE_PROVIDER=spree
|
||||||
|
|
||||||
|
SPREE_API_HOST = 'http://localhost:3000'
|
||||||
|
|
||||||
|
# TODO:
|
||||||
|
# COMMERCE_IMAGE_HOST
|
2
framework/spree/README.md
Normal file
2
framework/spree/README.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
TODO: Base README on other Framework READMEs.
|
||||||
|
TODO: Link to demo site running NextJS Commerce communicating with Spree.
|
9
framework/spree/commerce.config.json
Normal file
9
framework/spree/commerce.config.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"provider": "spree",
|
||||||
|
"features": {
|
||||||
|
"wishlist": false,
|
||||||
|
"cart": false,
|
||||||
|
"search": false,
|
||||||
|
"customerAuth": false
|
||||||
|
}
|
||||||
|
}
|
88
framework/spree/createFetcher.ts
Normal file
88
framework/spree/createFetcher.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import type { Fetcher } from '@commerce/utils/types'
|
||||||
|
import convertSpreeErrorToGraphQlError from './utils/convertSpreeErrorToGraphQlError'
|
||||||
|
import { makeClient } from '@spree/storefront-api-v2-sdk'
|
||||||
|
import type { ResultResponse } from '@spree/storefront-api-v2-sdk/types/interfaces/ResultResponse'
|
||||||
|
import type {
|
||||||
|
JsonApiDocument,
|
||||||
|
JsonApiListResponse,
|
||||||
|
} from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi'
|
||||||
|
import { errors } from '@spree/storefront-api-v2-sdk'
|
||||||
|
// import { API_TOKEN, API_URL } from './const'
|
||||||
|
// import { handleFetchResponse } from './utils'
|
||||||
|
|
||||||
|
const createFetcher = (fetcherOptions: any): Fetcher => {
|
||||||
|
const { host } = fetcherOptions
|
||||||
|
const client = makeClient({ host })
|
||||||
|
|
||||||
|
//TODO: Add types to fetcherOptions
|
||||||
|
return async (requestOptions) => {
|
||||||
|
console.log('FETCHER')
|
||||||
|
// url?: string
|
||||||
|
// query?: string
|
||||||
|
// method?: string
|
||||||
|
// variables?: any
|
||||||
|
// body?: Body
|
||||||
|
const { url, method, variables, query } = requestOptions
|
||||||
|
const { locale, ...vars } = variables ?? {}
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
// TODO: Create a custom type for this error.
|
||||||
|
throw new Error('Url not provider for fetcher.')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Fetching products using options: ${JSON.stringify(requestOptions)}.`
|
||||||
|
)
|
||||||
|
|
||||||
|
// const storeResponse = await fetch(url, {
|
||||||
|
// method,
|
||||||
|
// body: JSON.stringify({ query, variables: vars }),
|
||||||
|
// headers: {
|
||||||
|
// 'X-Shopify-Storefront-Access-Token': API_TOKEN,
|
||||||
|
// 'Content-Type': 'application/json', TODO: Probably not needed. Check!
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
|
||||||
|
// const storeResponse.json()
|
||||||
|
|
||||||
|
// if (storeResponse.ok) {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// TODO: Not best to use url for finding the method, but should be good enough for now.
|
||||||
|
|
||||||
|
const clientEndpointMethod = url
|
||||||
|
.split('.')
|
||||||
|
.reduce((clientNode: any, pathPart) => {
|
||||||
|
// TODO: Fix clientNode type
|
||||||
|
return clientNode[pathPart]
|
||||||
|
}, client)
|
||||||
|
|
||||||
|
const storeResponse: ResultResponse<JsonApiDocument | JsonApiListResponse> =
|
||||||
|
await clientEndpointMethod(...variables.args) // TODO: Not the best to use variables here as it's type is any.
|
||||||
|
|
||||||
|
if (storeResponse.success()) {
|
||||||
|
return storeResponse.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storeResponse.fail() instanceof errors.SpreeError) {
|
||||||
|
throw convertSpreeErrorToGraphQlError(storeResponse.fail())
|
||||||
|
}
|
||||||
|
|
||||||
|
throw storeResponse.fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// import { Fetcher } from '@commerce/utils/types'
|
||||||
|
|
||||||
|
// export const fetcher: Fetcher = async () => {
|
||||||
|
// console.log('FETCHER')
|
||||||
|
// const res = await fetch('./data.json')
|
||||||
|
// if (res.ok) {
|
||||||
|
// const { data } = await res.json()
|
||||||
|
// return data
|
||||||
|
// }
|
||||||
|
// throw res
|
||||||
|
// }
|
||||||
|
|
||||||
|
export default createFetcher
|
43
framework/spree/createProvider.ts
Normal file
43
framework/spree/createProvider.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import type { Provider } from '@commerce'
|
||||||
|
import { Config } from '.'
|
||||||
|
import fetcher from './fetcher'
|
||||||
|
|
||||||
|
// 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'
|
||||||
|
|
||||||
|
// export const saleorProvider = {
|
||||||
|
// locale: 'en-us',
|
||||||
|
// cartCookie: '',
|
||||||
|
// cartCookieToken: '',
|
||||||
|
// fetcher,
|
||||||
|
// cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
|
||||||
|
// customer: { useCustomer },
|
||||||
|
// products: { useSearch },
|
||||||
|
// auth: { useLogin, useLogout, useSignup },
|
||||||
|
// }
|
||||||
|
|
||||||
|
export const createProvider = (options: { config: Config }): Provider => {
|
||||||
|
const { config } = options
|
||||||
|
|
||||||
|
return {
|
||||||
|
locale: '', // Not an optional key in TypeScript, but already set in config. So, just make it an empty string.
|
||||||
|
cartCookie: '', // Not an optional key in TypeScript, but already set in config. So, just make it an empty string.
|
||||||
|
fetcher: createFetcher({ host: config.store.host }),
|
||||||
|
// FIXME: Add dummy hooks for below based on framework/local EXCEPT use-product
|
||||||
|
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
|
||||||
|
customer: { useCustomer },
|
||||||
|
products: { useSearch },
|
||||||
|
auth: { useLogin, useLogout, useSignup },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { Provider }
|
46
framework/spree/index.tsx
Normal file
46
framework/spree/index.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
CommerceConfig,
|
||||||
|
CommerceProvider as CoreCommerceProvider,
|
||||||
|
useCommerce as useCoreCommerce,
|
||||||
|
} from '@commerce'
|
||||||
|
|
||||||
|
// import { provider, Provider } from './provider'
|
||||||
|
import { createProvider, Provider } from './createProvider'
|
||||||
|
|
||||||
|
// export { provider }
|
||||||
|
|
||||||
|
// TODO: Below is probably not needed. Expect default values to be set by NextJS Commerce and be ok for now.
|
||||||
|
// export const saleorConfig: CommerceConfig = {
|
||||||
|
// locale: 'en-us',
|
||||||
|
// cartCookie: Const.CHECKOUT_ID_COOKIE,
|
||||||
|
// }
|
||||||
|
|
||||||
|
export type Config = {
|
||||||
|
store: {
|
||||||
|
host: string
|
||||||
|
}
|
||||||
|
} & CommerceConfig // This is the type that holds any custom values specifically for the Spree Framework.
|
||||||
|
|
||||||
|
export type SpreeProps = {
|
||||||
|
children: ReactNode
|
||||||
|
provider: Provider
|
||||||
|
config: Config
|
||||||
|
} & Config
|
||||||
|
|
||||||
|
export function CommerceProvider({ children, ...config }: SpreeProps) {
|
||||||
|
console.log('CommerceProvider called')
|
||||||
|
|
||||||
|
// TODO: Make sure this doesn't get called all the time. If it does, useMemo.
|
||||||
|
const provider = createProvider({ config })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CoreCommerceProvider provider={provider} config={config}>
|
||||||
|
{children}
|
||||||
|
</CoreCommerceProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCommerce = () => useCoreCommerce<Provider>()
|
13
framework/spree/next.config.js
Normal file
13
framework/spree/next.config.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const commerce = require('./commerce.config.json')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
commerce,
|
||||||
|
store: {
|
||||||
|
host: process.env.SPREE_API_HOST,
|
||||||
|
},
|
||||||
|
// images: {
|
||||||
|
// domains: [process.env.COMMERCE_IMAGE_HOST],
|
||||||
|
// },
|
||||||
|
// locale: 'en-us',
|
||||||
|
// cartCookie: Const.CHECKOUT_ID_COOKIE,
|
||||||
|
}
|
62
framework/spree/product/use-search.tsx
Normal file
62
framework/spree/product/use-search.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import type { SWRHook, Fetcher } from '@commerce/utils/types'
|
||||||
|
import useSearch from '@commerce/product/use-search'
|
||||||
|
import type { UseSearch } from '@commerce/product/use-search'
|
||||||
|
|
||||||
|
export const handler: SWRHook<any> = {
|
||||||
|
fetchOptions: {
|
||||||
|
url: 'client.products.list', // Add custom option for method name later
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
async fetcher({ input, options, fetch }) {
|
||||||
|
// This method is only needed if the options need to be modified before calling the generic fetcher (created in createFetcher).
|
||||||
|
// TODO: Actually filter by input and query.
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Calling useSearch fetcher with input: ${JSON.stringify(
|
||||||
|
input
|
||||||
|
)} and options: ${JSON.stringify(options)}.`
|
||||||
|
)
|
||||||
|
|
||||||
|
// FIXME: IMPLEMENT
|
||||||
|
|
||||||
|
return fetch({
|
||||||
|
url: options.url,
|
||||||
|
variables: { args: [] }, // TODO: Actually provide args later.
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// useHook is used for both, SWR and mutation requests to the store.
|
||||||
|
// useHook is called in React components. For example, after clicking `Add to cart`.
|
||||||
|
useHook:
|
||||||
|
({ useData }) =>
|
||||||
|
(input = {}) => {
|
||||||
|
console.log('useHook called')
|
||||||
|
|
||||||
|
// useData calls the fetcher method (above).
|
||||||
|
// The difference between useHook and calling fetcher directly is
|
||||||
|
// useHook accepts swrOptions.
|
||||||
|
return useData({
|
||||||
|
input: [
|
||||||
|
['search', input.search],
|
||||||
|
['categoryId', input.categoryId],
|
||||||
|
['brandId', input.brandId],
|
||||||
|
['sort', input.sort],
|
||||||
|
],
|
||||||
|
swrOptions: {
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
// revalidateOnFocus: false means do not fetch products again when website is refocused in the web browser.
|
||||||
|
...input.swrOptions,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// (input = {}) => {
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// data: {
|
||||||
|
// // FIXME: Use actual fetcher
|
||||||
|
// products: [],
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useSearch as UseSearch<typeof handler>
|
46
framework/spree/utils/convertSpreeErrorToGraphQlError.ts
Normal file
46
framework/spree/utils/convertSpreeErrorToGraphQlError.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { FetcherError } from '@commerce/utils/errors'
|
||||||
|
import { errors } from '@spree/storefront-api-v2-sdk'
|
||||||
|
|
||||||
|
const convertSpreeErrorToGraphQlError = (
|
||||||
|
error: errors.SpreeError
|
||||||
|
): FetcherError => {
|
||||||
|
if (error instanceof errors.ExpandedSpreeError) {
|
||||||
|
// Assuming error.errors[key] is a list of strings.
|
||||||
|
|
||||||
|
if ('base' in error.errors) {
|
||||||
|
const baseErrorMessage = error.errors.base as unknown as string
|
||||||
|
|
||||||
|
return new FetcherError({
|
||||||
|
status: error.serverResponse.status,
|
||||||
|
message: baseErrorMessage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetcherErrors = Object.keys(error.errors).map((sdkErrorKey) => {
|
||||||
|
const errors = error.errors[sdkErrorKey] as string[]
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: `${sdkErrorKey} ${errors.join(', ')}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return new FetcherError({
|
||||||
|
status: error.serverResponse.status,
|
||||||
|
errors: fetcherErrors,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof errors.BasicSpreeError) {
|
||||||
|
return new FetcherError({
|
||||||
|
status: error.serverResponse.status,
|
||||||
|
message: error.summary,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FetcherError({
|
||||||
|
status: error.serverResponse.status,
|
||||||
|
message: error.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default convertSpreeErrorToGraphQlError
|
Loading…
x
Reference in New Issue
Block a user