mirror of
https://github.com/vercel/commerce.git
synced 2025-07-04 04:01: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