mirror of
https://github.com/vercel/commerce.git
synced 2025-05-17 15:06:59 +00:00
feat: add olist-provider
This commit is contained in:
parent
87134e2990
commit
72db684864
3
.gitignore
vendored
3
.gitignore
vendored
@ -38,3 +38,6 @@ yarn-error.log*
|
||||
|
||||
# Turborepo
|
||||
.turbo
|
||||
|
||||
#typescript
|
||||
tsconfig.tsbuildinfo
|
||||
|
@ -17,6 +17,7 @@ Demo live at: [demo.vercel.store](https://demo.vercel.store/)
|
||||
- Kibo Commerce Demo: https://kibocommerce.vercel.store/
|
||||
- Commerce.js Demo: https://commercejs.vercel.store/
|
||||
- SalesForce Cloud Commerce Demo: https://salesforce-cloud-commerce.vercel.store/
|
||||
- Olist Demo: https://olist.vercel.store/
|
||||
|
||||
## Run minimal version locally
|
||||
|
||||
|
@ -69,7 +69,7 @@
|
||||
"@types/node": "^17.0.8",
|
||||
"@types/react": "^17.0.38",
|
||||
"lint-staged": "^12.1.7",
|
||||
"next": "^12.0.8",
|
||||
"next": "^12",
|
||||
"prettier": "^2.5.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
@ -15,6 +15,7 @@ A commerce provider is a headless e-commerce platform that integrates with the [
|
||||
- Kibo Commerce ([packages/kibocommerce](../kibocommerce))
|
||||
- Commerce.js ([packages/commercejs](../commercejs))
|
||||
- SFCC - SalesForce Cloud Commerce ([packages/sfcc](../sfcc))
|
||||
- Olist ([packages/olist](../olist))
|
||||
|
||||
Adding a commerce provider means adding a new folder in `packages` with a folder structure like the next one:
|
||||
|
||||
@ -69,7 +70,10 @@ Then, open [/site/.env.template](/site/.env.template) and add the provider name
|
||||
Using BigCommerce as an example. The first thing to do is export a `CommerceProvider` component that includes a `provider` object with all the handlers that can be used for hooks:
|
||||
|
||||
```tsx
|
||||
import { getCommerceProvider, useCommerce as useCoreCommerce } from '@vercel/commerce'
|
||||
import {
|
||||
getCommerceProvider,
|
||||
useCommerce as useCoreCommerce,
|
||||
} from '@vercel/commerce'
|
||||
import { bigcommerceProvider, BigcommerceProvider } from './provider'
|
||||
|
||||
export { bigcommerceProvider }
|
||||
@ -213,25 +217,26 @@ export const handler: MutationHook<AddItemHook> = {
|
||||
```
|
||||
|
||||
## Showing progress and features
|
||||
|
||||
When creating a PR for a new provider, include this list in the PR description and mark the progress as you push so we can organize the code review. Not all points are required (but advised) so make sure to keep the list up to date.
|
||||
|
||||
**Status**
|
||||
|
||||
* [ ] CommerceProvider
|
||||
* [ ] Schema & TS types
|
||||
* [ ] API Operations - Get all collections
|
||||
* [ ] API Operations - Get all pages
|
||||
* [ ] API Operations - Get all products
|
||||
* [ ] API Operations - Get page
|
||||
* [ ] API Operations - Get product
|
||||
* [ ] API Operations - Get Shop Info (categories and vendors working — `vendors` query still a WIP PR on Reaction)
|
||||
* [ ] Hook - Add Item
|
||||
* [ ] Hook - Remove Item
|
||||
* [ ] Hook - Update Item
|
||||
* [ ] Hook - Get Cart (account-tied carts working, anonymous carts working, cart reconciliation working)
|
||||
* [ ] Auth (based on a WIP PR on Reaction - still need to implement refresh tokens)
|
||||
* [ ] Customer information
|
||||
* [ ] Product attributes - Size, Colors
|
||||
* [ ] Custom checkout
|
||||
* [ ] Typing (in progress)
|
||||
* [ ] Tests
|
||||
- [ ] CommerceProvider
|
||||
- [ ] Schema & TS types
|
||||
- [ ] API Operations - Get all collections
|
||||
- [ ] API Operations - Get all pages
|
||||
- [ ] API Operations - Get all products
|
||||
- [ ] API Operations - Get page
|
||||
- [ ] API Operations - Get product
|
||||
- [ ] API Operations - Get Shop Info (categories and vendors working — `vendors` query still a WIP PR on Reaction)
|
||||
- [ ] Hook - Add Item
|
||||
- [ ] Hook - Remove Item
|
||||
- [ ] Hook - Update Item
|
||||
- [ ] Hook - Get Cart (account-tied carts working, anonymous carts working, cart reconciliation working)
|
||||
- [ ] Auth (based on a WIP PR on Reaction - still need to implement refresh tokens)
|
||||
- [ ] Customer information
|
||||
- [ ] Product attributes - Size, Colors
|
||||
- [ ] Custom checkout
|
||||
- [ ] Typing (in progress)
|
||||
- [ ] Tests
|
||||
|
@ -66,7 +66,7 @@
|
||||
"@types/node": "^17.0.8",
|
||||
"@types/react": "^17.0.38",
|
||||
"lint-staged": "^12.1.7",
|
||||
"next": "^12.0.8",
|
||||
"next": "^12",
|
||||
"prettier": "^2.5.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
@ -1,5 +1,6 @@
|
||||
import type { NextApiHandler } from 'next'
|
||||
import type { FetchOptions, Response } from '@vercel/fetch'
|
||||
|
||||
import type { APIEndpoint, APIHandler } from './utils/types'
|
||||
import type { CartSchema } from '../types/cart'
|
||||
import type { CustomerSchema } from '../types/customer'
|
||||
|
@ -65,7 +65,7 @@
|
||||
"@types/node": "^17.0.8",
|
||||
"@types/react": "^17.0.38",
|
||||
"lint-staged": "^12.1.7",
|
||||
"next": "^12.0.8",
|
||||
"next": "^12",
|
||||
"prettier": "^2.5.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
@ -70,7 +70,7 @@
|
||||
"@types/node": "^17.0.8",
|
||||
"@types/react": "^17.0.38",
|
||||
"lint-staged": "^12.1.7",
|
||||
"next": "^12.0.8",
|
||||
"next": "^12",
|
||||
"prettier": "^2.5.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
@ -62,7 +62,7 @@
|
||||
"@types/node": "^17.0.8",
|
||||
"@types/react": "^17.0.38",
|
||||
"lint-staged": "^12.1.7",
|
||||
"next": "^12.0.8",
|
||||
"next": "^12",
|
||||
"prettier": "^2.5.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
4
packages/olist/.env.template
Normal file
4
packages/olist/.env.template
Normal file
@ -0,0 +1,4 @@
|
||||
COMMERCE_PROVIDER=@vercel/commerce-olist
|
||||
|
||||
NEXT_PUBLIC_OLIST_STOREFRONT_DOMAIN=
|
||||
NEXT_PUBLIC_OLIST_STOREFRONT_ACCESS_TOKEN=
|
2
packages/olist/.prettierignore
Normal file
2
packages/olist/.prettierignore
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
6
packages/olist/.prettierrc
Normal file
6
packages/olist/.prettierrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
1
packages/olist/README.md
Normal file
1
packages/olist/README.md
Normal file
@ -0,0 +1 @@
|
||||
# Next.js Olist Provider
|
80
packages/olist/package.json
Normal file
80
packages/olist/package.json
Normal file
@ -0,0 +1,80 @@
|
||||
{
|
||||
"name": "@vercel/commerce-olist",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"release": "taskr release",
|
||||
"build": "taskr build",
|
||||
"dev": "taskr",
|
||||
"types": "tsc --emitDeclarationOnly",
|
||||
"prettier-fix": "prettier --write ."
|
||||
},
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./*": [
|
||||
"./dist/*.js",
|
||||
"./dist/*/index.js"
|
||||
],
|
||||
"./next.config": "./dist/next.config.cjs"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"src/*",
|
||||
"src/*/index"
|
||||
],
|
||||
"next.config": [
|
||||
"dist/next.config.d.cts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"publishConfig": {
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"dist/*.d.ts",
|
||||
"dist/*/index.d.ts"
|
||||
],
|
||||
"next.config": [
|
||||
"dist/next.config.d.cts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/commerce": "^0.0.1",
|
||||
"@vercel/fetch": "^6.1.1",
|
||||
"@vnda/headless-framework": "^0.0.33"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": "^12",
|
||||
"react": "^17",
|
||||
"react-dom": "^17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@taskr/clear": "^1.1.0",
|
||||
"@taskr/esnext": "^1.1.0",
|
||||
"@taskr/watch": "^1.1.0",
|
||||
"@types/node": "^17.0.8",
|
||||
"@types/react": "^17.0.38",
|
||||
"lint-staged": "^12.1.7",
|
||||
"next": "^12",
|
||||
"prettier": "^2.5.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"taskr": "^1.1.0",
|
||||
"taskr-swc": "^0.0.1",
|
||||
"typescript": "^4.5.4"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.{js,jsx,ts,tsx,json}": [
|
||||
"prettier --write",
|
||||
"git add"
|
||||
]
|
||||
}
|
||||
}
|
84
packages/olist/src/api/endpoints/cart/add-item.ts
Normal file
84
packages/olist/src/api/endpoints/cart/add-item.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { serialize } from 'cookie'
|
||||
import type { Cart } from '@vnda/headless-framework'
|
||||
|
||||
import type { CartEndpoint, Handler } from '.'
|
||||
|
||||
import {
|
||||
mapCommerceToRawRequest,
|
||||
mapRawToCommerceResponse,
|
||||
} from '../../../utils/cart'
|
||||
|
||||
const addItem: CartEndpoint['handlers']['addItem'] = async ({
|
||||
res: response,
|
||||
body: { cartId, item },
|
||||
config: { service, cartCookie, cartTokenCookie },
|
||||
}: Handler) => {
|
||||
try {
|
||||
if (!item) {
|
||||
return response.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Missing item' }],
|
||||
})
|
||||
}
|
||||
|
||||
if (!item.quantity) item.quantity = 1
|
||||
|
||||
let cart: Cart
|
||||
|
||||
if (!cartId) {
|
||||
cart = await service.cart.create()
|
||||
|
||||
response.setHeader('Set-Cookie', [
|
||||
serialize(cartCookie, cart.id.toString(), {
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
expires: new Date(Date.now() + 60 * 60 * 24 * 30 * 1000),
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
}),
|
||||
serialize(cartTokenCookie, cart.token, {
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
expires: new Date(Date.now() + 60 * 60 * 24 * 30 * 1000),
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
}),
|
||||
])
|
||||
} else {
|
||||
cart = await service.cart.getById(Number(cartId))
|
||||
}
|
||||
|
||||
const itemExistQuantity =
|
||||
(
|
||||
cart.items.find(({ variantSku }) => variantSku === item.variantId)! ||
|
||||
[]
|
||||
).quantity || 0
|
||||
|
||||
const cartItem = await service.cart.addItem(
|
||||
cartId ? Number(cartId) : cart!.id,
|
||||
mapCommerceToRawRequest({
|
||||
...item,
|
||||
quantity: Number(itemExistQuantity) + 1 || 1,
|
||||
})
|
||||
)
|
||||
|
||||
cart.items = [
|
||||
...(cart?.items.map((value) =>
|
||||
value.variantSku === item.variantId ? cartItem : value
|
||||
) || []),
|
||||
...(itemExistQuantity ? [] : [cartItem]),
|
||||
]
|
||||
|
||||
response.status(200).json({
|
||||
data: mapRawToCommerceResponse(cart),
|
||||
errors: [],
|
||||
})
|
||||
} catch (error) {
|
||||
response.status(500).json({
|
||||
data: {},
|
||||
errors: error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default addItem
|
41
packages/olist/src/api/endpoints/cart/get-cart.ts
Normal file
41
packages/olist/src/api/endpoints/cart/get-cart.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { serialize } from 'cookie'
|
||||
|
||||
import type { CartEndpoint, Handler } from '.'
|
||||
|
||||
import { mapRawToCommerceResponse } from '../../../utils/cart'
|
||||
|
||||
const getCart: CartEndpoint['handlers']['getCart'] = async ({
|
||||
res: response,
|
||||
body: { cartId },
|
||||
config: { service, cartCookie, cartTokenCookie },
|
||||
}: Handler) => {
|
||||
if (!cartId) {
|
||||
return response.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Invalid request' }],
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const cart = await service.cart.getById(Number(cartId))
|
||||
|
||||
response
|
||||
.status(200)
|
||||
.json({ data: mapRawToCommerceResponse(cart), errors: [] })
|
||||
} catch (error) {
|
||||
response.setHeader('Set-Cookie', [
|
||||
serialize(cartCookie, cartId, {
|
||||
maxAge: -1,
|
||||
path: '/',
|
||||
}),
|
||||
serialize(cartTokenCookie, cartId, {
|
||||
maxAge: -1,
|
||||
path: '/',
|
||||
}),
|
||||
])
|
||||
|
||||
response.status(200).json({ data: null, errors: [] })
|
||||
}
|
||||
}
|
||||
|
||||
export default getCart
|
47
packages/olist/src/api/endpoints/cart/index.ts
Normal file
47
packages/olist/src/api/endpoints/cart/index.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import cartEndpoint from '@vercel/commerce/api/endpoints/cart'
|
||||
import { createEndpoint } from '@vercel/commerce/api'
|
||||
|
||||
import type { GetAPISchema } from '@vercel/commerce/api'
|
||||
import type { CartItemBody, CartSchema } from '@vercel/commerce/types/cart'
|
||||
|
||||
import getCart from './get-cart'
|
||||
import addItem from './add-item'
|
||||
import updateItem from './update-item'
|
||||
import removeItem from './remove-item'
|
||||
|
||||
import type { OlistAPI } from '../../../api'
|
||||
import type { Handler as HandlerAPI } from '../../../types/api'
|
||||
|
||||
export type CartAPI = GetAPISchema<OlistAPI, CartSchema>
|
||||
|
||||
export type CartEndpoint = CartAPI['endpoint']
|
||||
|
||||
export const handlers: CartEndpoint['handlers'] = {
|
||||
getCart,
|
||||
addItem,
|
||||
updateItem,
|
||||
removeItem,
|
||||
}
|
||||
|
||||
type GetCartBody = {
|
||||
cartId?: string
|
||||
}
|
||||
|
||||
type AddItemBody = {
|
||||
item?: CartItemBody
|
||||
}
|
||||
|
||||
type RemoveItemBody = {
|
||||
itemId?: string
|
||||
}
|
||||
|
||||
export type Handler = {
|
||||
body: GetCartBody & AddItemBody & RemoveItemBody
|
||||
} & HandlerAPI
|
||||
|
||||
const cartApi = createEndpoint<CartAPI>({
|
||||
handler: cartEndpoint,
|
||||
handlers,
|
||||
})
|
||||
|
||||
export default cartApi
|
26
packages/olist/src/api/endpoints/cart/remove-item.ts
Normal file
26
packages/olist/src/api/endpoints/cart/remove-item.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import type { CartEndpoint, Handler } from '.'
|
||||
|
||||
import { mapRawToCommerceResponse } from '../../../utils/cart'
|
||||
|
||||
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
|
||||
res: response,
|
||||
body: { cartId, itemId },
|
||||
config: { service },
|
||||
}: Handler) => {
|
||||
if (!cartId || !itemId) {
|
||||
return response.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Invalid request' }],
|
||||
})
|
||||
}
|
||||
|
||||
await service.cart.removeItem(Number(cartId), Number(itemId))
|
||||
|
||||
const cart = await service.cart.getById(Number(cartId))
|
||||
|
||||
response
|
||||
.status(200)
|
||||
.json({ data: mapRawToCommerceResponse(cart), errors: [] })
|
||||
}
|
||||
|
||||
export default removeItem
|
28
packages/olist/src/api/endpoints/cart/update-item.ts
Normal file
28
packages/olist/src/api/endpoints/cart/update-item.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import type { CartEndpoint, Handler } from '.'
|
||||
|
||||
import { mapRawToCommerceResponse } from '../../../utils/cart'
|
||||
|
||||
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
|
||||
res: response,
|
||||
body: { cartId, itemId, item },
|
||||
config: { service },
|
||||
}: Handler) => {
|
||||
if (!cartId || !itemId || !item) {
|
||||
return response.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Invalid request' }],
|
||||
})
|
||||
}
|
||||
|
||||
await service.cart.updateItem(Number(cartId), Number(itemId), {
|
||||
quantity: item.quantity,
|
||||
})
|
||||
|
||||
const cart = await service.cart.getById(Number(cartId))
|
||||
|
||||
response
|
||||
.status(200)
|
||||
.json({ data: mapRawToCommerceResponse(cart), errors: [] })
|
||||
}
|
||||
|
||||
export default updateItem
|
@ -0,0 +1,34 @@
|
||||
import type { Product } from '@vnda/headless-framework'
|
||||
|
||||
import { ProductsEndpoint, Handler } from '.'
|
||||
|
||||
import { mapItemRawToCommerceResponse } from '../../../../utils/product'
|
||||
|
||||
// Get products for the product list page. Search and category filter implemented. Sort and brand filter not implemented.
|
||||
const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
|
||||
res,
|
||||
body: { search, sort, categoryId },
|
||||
config: { service },
|
||||
}: Handler) => {
|
||||
try {
|
||||
let result: Product[] = []
|
||||
|
||||
result = await (search
|
||||
? service.product.search({ term: search })
|
||||
: service.product.list({
|
||||
sort,
|
||||
limit: 20,
|
||||
...(categoryId && { tag: [categoryId.toString()] }),
|
||||
}))
|
||||
|
||||
const found = result?.length > 0
|
||||
|
||||
res.status(200).json({
|
||||
data: { products: result?.map(mapItemRawToCommerceResponse), found },
|
||||
})
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export default getProducts
|
30
packages/olist/src/api/endpoints/catalog/products/index.ts
Normal file
30
packages/olist/src/api/endpoints/catalog/products/index.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { createEndpoint } from '@vercel/commerce/api'
|
||||
import productsEndpoint from '@vercel/commerce/api/endpoints/catalog/products'
|
||||
|
||||
import type { GetAPISchema } from '@vercel/commerce/api'
|
||||
import type {
|
||||
ProductsSchema,
|
||||
SearchProductsBody,
|
||||
} from '@vercel/commerce/types/product'
|
||||
|
||||
import getProducts from './get-products'
|
||||
|
||||
import type { OlistAPI } from '../../../../api'
|
||||
import type { Handler as HandlerAPI } from '../../../../types/api'
|
||||
|
||||
export type ProductsAPI = GetAPISchema<OlistAPI, ProductsSchema>
|
||||
|
||||
export type ProductsEndpoint = ProductsAPI['endpoint']
|
||||
|
||||
export const handlers: ProductsEndpoint['handlers'] = { getProducts }
|
||||
|
||||
export type Handler = {
|
||||
body: SearchProductsBody
|
||||
} & HandlerAPI
|
||||
|
||||
const productsApi = createEndpoint<ProductsAPI>({
|
||||
handler: productsEndpoint,
|
||||
handlers,
|
||||
})
|
||||
|
||||
export default productsApi
|
20
packages/olist/src/api/endpoints/checkout/get-checkout.ts
Normal file
20
packages/olist/src/api/endpoints/checkout/get-checkout.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import type { CheckoutEndpoint } from '.'
|
||||
import { Handler } from '../cart'
|
||||
|
||||
const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
|
||||
req: request,
|
||||
res: response,
|
||||
body: { cartId },
|
||||
config: { storeDomain, cartTokenCookie },
|
||||
}: Handler) => {
|
||||
const cartToken = request.cookies[cartTokenCookie]
|
||||
|
||||
if (!cartId || !cartToken) {
|
||||
response.redirect('/')
|
||||
return
|
||||
}
|
||||
|
||||
response.redirect(`https://${storeDomain}/checkout/${cartToken}`)
|
||||
}
|
||||
|
||||
export default getCheckout
|
25
packages/olist/src/api/endpoints/checkout/index.ts
Normal file
25
packages/olist/src/api/endpoints/checkout/index.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { createEndpoint } from '@vercel/commerce/api'
|
||||
import checkoutEndpoint from '@vercel/commerce/api/endpoints/checkout'
|
||||
|
||||
import type { GetAPISchema } from '@vercel/commerce/api'
|
||||
import type { CheckoutSchema } from '@vercel/commerce/types/checkout'
|
||||
|
||||
import getCheckout from './get-checkout'
|
||||
import submitCheckout from './submit-checkout'
|
||||
|
||||
import type { OlistAPI } from '../..'
|
||||
|
||||
export type CheckoutAPI = GetAPISchema<OlistAPI, CheckoutSchema>
|
||||
export type CheckoutEndpoint = CheckoutAPI['endpoint']
|
||||
|
||||
export const handlers: CheckoutEndpoint['handlers'] = {
|
||||
getCheckout,
|
||||
submitCheckout,
|
||||
}
|
||||
|
||||
const checkoutApi = createEndpoint<CheckoutAPI>({
|
||||
handler: checkoutEndpoint,
|
||||
handlers,
|
||||
})
|
||||
|
||||
export default checkoutApi
|
10
packages/olist/src/api/endpoints/checkout/submit-checkout.ts
Normal file
10
packages/olist/src/api/endpoints/checkout/submit-checkout.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import type { CheckoutEndpoint } from '.'
|
||||
|
||||
const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({
|
||||
res,
|
||||
}) => {
|
||||
// Return cart and errors
|
||||
res.status(200).json({ data: null, errors: [] })
|
||||
}
|
||||
|
||||
export default submitCheckout
|
1
packages/olist/src/api/endpoints/customer/address.ts
Normal file
1
packages/olist/src/api/endpoints/customer/address.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
packages/olist/src/api/endpoints/customer/card.ts
Normal file
1
packages/olist/src/api/endpoints/customer/card.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
@ -0,0 +1,51 @@
|
||||
import { verify, TokenExpiredError } from 'jsonwebtoken'
|
||||
|
||||
import type { CustomerEndpoint, Handler } from '.'
|
||||
|
||||
const getLoggedInCustomer: CustomerEndpoint['handlers']['getLoggedInCustomer'] =
|
||||
async ({
|
||||
req: request,
|
||||
res: response,
|
||||
config: { apiToken, customerTokenCookie, service },
|
||||
}: Handler) => {
|
||||
const token = request.cookies[customerTokenCookie]
|
||||
|
||||
if (token) {
|
||||
const decoded = verify(token, apiToken)
|
||||
|
||||
try {
|
||||
const customer = await service.client.getUserById((decoded as any).id)
|
||||
|
||||
if (!customer) {
|
||||
return response.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Customer not found', code: 'not_found' }],
|
||||
})
|
||||
}
|
||||
|
||||
return response.status(200).json({
|
||||
data: {
|
||||
customer,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof TokenExpiredError) {
|
||||
response.status(401).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
message: 'Jwt expired',
|
||||
code: 'token_expired_error',
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
response.status(200).json({ data: null })
|
||||
}
|
||||
|
||||
export default getLoggedInCustomer
|
24
packages/olist/src/api/endpoints/customer/index.ts
Normal file
24
packages/olist/src/api/endpoints/customer/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
||||
import customerEndpoint from '@vercel/commerce/api/endpoints/customer'
|
||||
|
||||
import type { CustomerSchema } from '@vercel/commerce/types/customer'
|
||||
|
||||
import { OlistAPI } from '../../../api'
|
||||
import getLoggedInCustomer from './get-logged-in-customer'
|
||||
|
||||
import type { Handler as HandlerAPI } from '../../../types/api'
|
||||
|
||||
export type CustomerAPI = GetAPISchema<OlistAPI, CustomerSchema>
|
||||
|
||||
export type CustomerEndpoint = CustomerAPI['endpoint']
|
||||
|
||||
export const handlers: CustomerEndpoint['handlers'] = { getLoggedInCustomer }
|
||||
|
||||
export type Handler = { body: any } & HandlerAPI
|
||||
|
||||
const customerApi = createEndpoint<CustomerAPI>({
|
||||
handler: customerEndpoint,
|
||||
handlers,
|
||||
})
|
||||
|
||||
export default customerApi
|
25
packages/olist/src/api/endpoints/login/index.ts
Normal file
25
packages/olist/src/api/endpoints/login/index.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { createEndpoint } from '@vercel/commerce/api'
|
||||
import loginEndpoint from '@vercel/commerce/api/endpoints/login'
|
||||
|
||||
import type { GetAPISchema } from '@vercel/commerce/api'
|
||||
import type { LoginBody, LoginSchema } from '@vercel/commerce/types/login'
|
||||
|
||||
import login from './login'
|
||||
|
||||
import type { OlistAPI } from '../../../api'
|
||||
import type { Handler as HandlerAPI } from '../../../types/api'
|
||||
|
||||
export type LoginAPI = GetAPISchema<OlistAPI, LoginSchema>
|
||||
|
||||
export type LoginEndpoint = LoginAPI['endpoint']
|
||||
|
||||
export type Handler = { body: LoginBody } & HandlerAPI
|
||||
|
||||
export const handlers: LoginEndpoint['handlers'] = { login }
|
||||
|
||||
const loginApi = createEndpoint<LoginAPI>({
|
||||
handler: loginEndpoint,
|
||||
handlers,
|
||||
})
|
||||
|
||||
export default loginApi
|
63
packages/olist/src/api/endpoints/login/login.ts
Normal file
63
packages/olist/src/api/endpoints/login/login.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { sign } from 'jsonwebtoken'
|
||||
import { serialize } from 'cookie'
|
||||
|
||||
import { FetcherError } from '@vercel/commerce/utils/errors'
|
||||
|
||||
import type { Handler, LoginEndpoint } from '.'
|
||||
|
||||
export const invalidCredentials = /email and\/or password invalid/i
|
||||
|
||||
const login: LoginEndpoint['handlers']['login'] = async ({
|
||||
res: response,
|
||||
body: { email, password },
|
||||
config: { apiToken, customerTokenCookie, service },
|
||||
}: Handler) => {
|
||||
if (!email || !password) {
|
||||
return response.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Invalid request' }],
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const { id } = await service.client.login({ email, password })
|
||||
|
||||
if (id) {
|
||||
response.setHeader('Set-Cookie', [
|
||||
serialize(
|
||||
customerTokenCookie,
|
||||
sign({ id: id.toString() }, apiToken, { expiresIn: 60 * 60 }),
|
||||
{
|
||||
maxAge: 60 * 60,
|
||||
expires: new Date(Date.now() + 60 * 60 * 1000),
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
}
|
||||
),
|
||||
])
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof FetcherError &&
|
||||
invalidCredentials.test(error.message)
|
||||
) {
|
||||
return response.status(401).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Cannot find an account that matches the provided credentials',
|
||||
code: 'invalid_credentials',
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
response.status(200).json({ data: null })
|
||||
}
|
||||
|
||||
export default login
|
25
packages/olist/src/api/endpoints/logout/index.ts
Normal file
25
packages/olist/src/api/endpoints/logout/index.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { createEndpoint } from '@vercel/commerce/api'
|
||||
import logoutEndpoint from '@vercel/commerce/api/endpoints/logout'
|
||||
import { LogoutSchema, LogoutTypes } from '@vercel/commerce/types/logout'
|
||||
|
||||
import type { GetAPISchema } from '@vercel/commerce/api'
|
||||
|
||||
import logout from './logout'
|
||||
|
||||
import type { OlistAPI } from '../../../api'
|
||||
import type { Handler as HandlerAPI } from '../../../types/api'
|
||||
|
||||
export type LogoutAPI = GetAPISchema<OlistAPI, LogoutSchema>
|
||||
|
||||
export type LogoutEndpoint = LogoutAPI['endpoint']
|
||||
|
||||
export const handlers: LogoutEndpoint['handlers'] = { logout }
|
||||
|
||||
export type Handler = { body: LogoutTypes['body'] } & HandlerAPI
|
||||
|
||||
const logoutApi = createEndpoint<LogoutAPI>({
|
||||
handler: logoutEndpoint,
|
||||
handlers,
|
||||
})
|
||||
|
||||
export default logoutApi
|
21
packages/olist/src/api/endpoints/logout/logout.ts
Normal file
21
packages/olist/src/api/endpoints/logout/logout.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { serialize } from 'cookie'
|
||||
import type { Handler, LogoutEndpoint } from '.'
|
||||
|
||||
const logout: LogoutEndpoint['handlers']['logout'] = async ({
|
||||
res: response,
|
||||
body: { redirectTo },
|
||||
config: { customerTokenCookie },
|
||||
}: Handler) => {
|
||||
response.setHeader(
|
||||
'Set-Cookie',
|
||||
serialize(customerTokenCookie, '', { maxAge: -1, path: '/' })
|
||||
)
|
||||
|
||||
if (redirectTo?.startsWith('/')) {
|
||||
response.redirect(redirectTo)
|
||||
} else {
|
||||
response.status(200).json({ data: null })
|
||||
}
|
||||
}
|
||||
|
||||
export default logout
|
25
packages/olist/src/api/endpoints/signup/index.ts
Normal file
25
packages/olist/src/api/endpoints/signup/index.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { createEndpoint } from '@vercel/commerce/api'
|
||||
import signupEndpoint from '@vercel/commerce/api/endpoints/signup'
|
||||
|
||||
import type { GetAPISchema } from '@vercel/commerce/api'
|
||||
import type { SignupBody, SignupSchema } from '@vercel/commerce/types/signup'
|
||||
|
||||
import signup from './signup'
|
||||
|
||||
import type { OlistAPI } from '../../../api'
|
||||
import type { Handler as HandlerAPI } from '../../../types/api'
|
||||
|
||||
export type SignupAPI = GetAPISchema<OlistAPI, SignupSchema>
|
||||
|
||||
export type SignupEndpoint = SignupAPI['endpoint']
|
||||
|
||||
export const handlers: SignupEndpoint['handlers'] = { signup }
|
||||
|
||||
export type Handler = { body: SignupBody } & HandlerAPI
|
||||
|
||||
const singupApi = createEndpoint<SignupAPI>({
|
||||
handler: signupEndpoint,
|
||||
handlers,
|
||||
})
|
||||
|
||||
export default singupApi
|
48
packages/olist/src/api/endpoints/signup/signup.ts
Normal file
48
packages/olist/src/api/endpoints/signup/signup.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { sign } from 'jsonwebtoken'
|
||||
import { serialize } from 'cookie'
|
||||
|
||||
import type { Handler, SignupEndpoint } from '.'
|
||||
|
||||
const signup: SignupEndpoint['handlers']['signup'] = async ({
|
||||
res: response,
|
||||
body: { firstName, lastName, email, password },
|
||||
config: { apiToken, customerTokenCookie, service },
|
||||
}: Handler) => {
|
||||
if (!(firstName && lastName && email && password)) {
|
||||
return response.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Invalid request' }],
|
||||
})
|
||||
}
|
||||
try {
|
||||
const { id } = await service.client.signup({
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
password,
|
||||
passwordConfirmation: password,
|
||||
})
|
||||
|
||||
if (id) {
|
||||
response.setHeader('Set-Cookie', [
|
||||
serialize(
|
||||
customerTokenCookie,
|
||||
sign({ id: id.toString() }, apiToken, { expiresIn: 60 * 60 }),
|
||||
{
|
||||
maxAge: 60 * 60,
|
||||
expires: new Date(Date.now() + 60 * 60 * 1000),
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
}
|
||||
),
|
||||
])
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
response.status(200).json({ data: null })
|
||||
}
|
||||
|
||||
export default signup
|
1
packages/olist/src/api/endpoints/wishlist/index.tsx
Normal file
1
packages/olist/src/api/endpoints/wishlist/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
86
packages/olist/src/api/index.ts
Normal file
86
packages/olist/src/api/index.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { getCommerceApi as commerceApi } from '@vercel/commerce/api'
|
||||
|
||||
import type { CommerceAPI, CommerceAPIConfig } from '@vercel/commerce/api'
|
||||
import type { VndaServiceSingleton } from '@vnda/headless-framework'
|
||||
|
||||
import createGraphqlFetcher from './utils/fetch-graphql'
|
||||
|
||||
import { client } from '../api/utils/fetch'
|
||||
|
||||
import getPage from '../api/operations/get-page'
|
||||
import getProduct from '../api/operations/get-product'
|
||||
import getSiteInfo from '../api/operations/get-site-info'
|
||||
import getAllPages from '../api/operations/get-all-pages'
|
||||
import getAllProducts from '../api/operations/get-all-products'
|
||||
import getAllProductPaths from '../api/operations/get-all-product-paths'
|
||||
|
||||
import {
|
||||
API_URL,
|
||||
API_VERSION,
|
||||
CART_COOKIE,
|
||||
CUSTOMER_COOKIE,
|
||||
TOKEN_COOKIE,
|
||||
API_TOKEN,
|
||||
STORE_DOMAIN,
|
||||
CART_TOKEN_COOKIE,
|
||||
CUSTOMER_TOKEN_COOKIE,
|
||||
} from '../constants'
|
||||
|
||||
if (!API_TOKEN) {
|
||||
throw new Error(
|
||||
`The environment variable NEXT_PUBLIC_OLIST_STOREFRONT_ACCESS_TOKEN is missing and it's required to access your store`
|
||||
)
|
||||
}
|
||||
|
||||
if (!STORE_DOMAIN) {
|
||||
throw new Error(
|
||||
`The environment variable NEXT_PUBLIC_OLIST_STOREFRONT_DOMAIN is missing and it's required to access your store`
|
||||
)
|
||||
}
|
||||
|
||||
export interface OlistConfig extends CommerceAPIConfig {
|
||||
commerceUrl: string
|
||||
service: VndaServiceSingleton
|
||||
apiVersion: string
|
||||
tokenCookie: string
|
||||
storeDomain: string
|
||||
cartTokenCookie: string
|
||||
customerTokenCookie: string
|
||||
}
|
||||
|
||||
const ONE_DAY = 60 * 60 * 24
|
||||
|
||||
const config: OlistConfig = {
|
||||
commerceUrl: API_URL,
|
||||
apiToken: API_TOKEN,
|
||||
storeDomain: STORE_DOMAIN,
|
||||
apiVersion: API_VERSION,
|
||||
cartCookie: CART_COOKIE,
|
||||
customerCookie: CUSTOMER_COOKIE,
|
||||
tokenCookie: TOKEN_COOKIE,
|
||||
cartTokenCookie: CART_TOKEN_COOKIE,
|
||||
customerTokenCookie: CUSTOMER_TOKEN_COOKIE,
|
||||
cartCookieMaxAge: ONE_DAY * 30,
|
||||
fetch: createGraphqlFetcher(() => getCommerceApi().getConfig()),
|
||||
service: client(API_TOKEN, STORE_DOMAIN),
|
||||
}
|
||||
|
||||
const operations = {
|
||||
getAllPages,
|
||||
getPage,
|
||||
getSiteInfo,
|
||||
getAllProductPaths,
|
||||
getAllProducts,
|
||||
getProduct,
|
||||
}
|
||||
|
||||
export const provider = { config, operations }
|
||||
|
||||
export type Provider = typeof provider
|
||||
export type OlistAPI<P extends Provider = Provider> = CommerceAPI<P | any>
|
||||
|
||||
export function getCommerceApi<P extends Provider>(
|
||||
customProvider: P = provider as any
|
||||
): OlistAPI<P> {
|
||||
return commerceApi(customProvider as any)
|
||||
}
|
22
packages/olist/src/api/operations/get-all-pages.ts
Normal file
22
packages/olist/src/api/operations/get-all-pages.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { GetAllPagesOperation } from '@vercel/commerce/types/page'
|
||||
|
||||
import type { OlistConfig } from '../'
|
||||
|
||||
export type Page = { url: string }
|
||||
export type GetAllPagesResult = { pages: Page[] }
|
||||
|
||||
export default function getAllPagesOperation() {
|
||||
async function getAllPages<T extends GetAllPagesOperation>({
|
||||
config,
|
||||
preview,
|
||||
}: {
|
||||
url?: string
|
||||
config?: Partial<OlistConfig>
|
||||
preview?: boolean
|
||||
} = {}): Promise<T['data']> {
|
||||
return Promise.resolve({
|
||||
pages: [],
|
||||
})
|
||||
}
|
||||
return getAllPages
|
||||
}
|
30
packages/olist/src/api/operations/get-all-product-paths.ts
Normal file
30
packages/olist/src/api/operations/get-all-product-paths.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import type { OperationContext } from '@vercel/commerce/api/operations'
|
||||
import type { GetAllProductPathsOperation } from '@vercel/commerce/types/product'
|
||||
|
||||
import type { OlistConfig, Provider } from '../'
|
||||
|
||||
export type GetAllProductPathsResult = {
|
||||
products: Array<{ path: string }>
|
||||
}
|
||||
|
||||
export default function getAllProductPathsOperation({
|
||||
commerce,
|
||||
}: OperationContext<Provider>) {
|
||||
async function getAllProductPaths<T extends GetAllProductPathsOperation>({
|
||||
config,
|
||||
}: {
|
||||
config?: Partial<OlistConfig>
|
||||
} = {}): Promise<T['data']> {
|
||||
const { service } = commerce.getConfig(config)
|
||||
|
||||
const products = await service.product.list({ limit: 20 })
|
||||
|
||||
return {
|
||||
products: products.map((product) => ({
|
||||
path: `/${product.slug}-${product.id}`,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
return getAllProductPaths
|
||||
}
|
28
packages/olist/src/api/operations/get-all-products.ts
Normal file
28
packages/olist/src/api/operations/get-all-products.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import type { OperationContext } from '@vercel/commerce/api/operations'
|
||||
import type { GetAllProductsOperation } from '@vercel/commerce/types/product'
|
||||
|
||||
import type { OlistConfig, Provider } from '..'
|
||||
import { mapItemRawToCommerceResponse } from '../../utils/product'
|
||||
|
||||
export default function getAllProductsOperation({
|
||||
commerce,
|
||||
}: OperationContext<Provider>) {
|
||||
async function getAllProducts<T extends GetAllProductsOperation>({
|
||||
config,
|
||||
}: {
|
||||
query?: string
|
||||
variables?: T['variables']
|
||||
config?: Partial<OlistConfig>
|
||||
preview?: boolean
|
||||
} = {}): Promise<T['data']> {
|
||||
const { service } = commerce.getConfig(config)
|
||||
|
||||
const products = await service.product.list({ limit: 20 })
|
||||
|
||||
return {
|
||||
products: products.map(mapItemRawToCommerceResponse),
|
||||
}
|
||||
}
|
||||
|
||||
return getAllProducts
|
||||
}
|
15
packages/olist/src/api/operations/get-page.ts
Normal file
15
packages/olist/src/api/operations/get-page.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { GetPageOperation } from '@vercel/commerce/types/page'
|
||||
|
||||
export type Page = any
|
||||
export type GetPageResult = { page?: Page }
|
||||
|
||||
export type PageVariables = {
|
||||
id: number
|
||||
}
|
||||
|
||||
export default function getPageOperation() {
|
||||
async function getPage<T extends GetPageOperation>(): Promise<T['data']> {
|
||||
return Promise.resolve({})
|
||||
}
|
||||
return getPage
|
||||
}
|
51
packages/olist/src/api/operations/get-product.ts
Normal file
51
packages/olist/src/api/operations/get-product.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import type { OperationContext } from '@vercel/commerce/api/operations'
|
||||
import type { GetProductOperation } from '@vercel/commerce/types/product'
|
||||
|
||||
import type { OlistConfig, Provider } from '..'
|
||||
|
||||
import {
|
||||
mapItemRawToCommerceResponse,
|
||||
extractProductId,
|
||||
mapImagesRawToCommerceResponse,
|
||||
mapVariantsRawToCommerceResponse,
|
||||
mapVariantsOptionsToCommerceCommerce,
|
||||
} from '../../utils/product'
|
||||
|
||||
export default function getProductOperation({
|
||||
commerce,
|
||||
}: OperationContext<Provider>) {
|
||||
async function getProduct<T extends GetProductOperation>({
|
||||
config,
|
||||
variables,
|
||||
}: {
|
||||
query?: string
|
||||
variables?: T['variables']
|
||||
config?: Partial<OlistConfig>
|
||||
preview?: boolean
|
||||
} = {}): Promise<T['data']> {
|
||||
const { service } = commerce.getConfig(config)
|
||||
|
||||
const productId = extractProductId(variables?.slug || variables?.path)
|
||||
|
||||
const productPromise = service.product.getById(Number(productId))
|
||||
const variantsPromise = service.variant.list(Number(productId))
|
||||
const imagesPromise = service.product.image.list(Number(productId))
|
||||
|
||||
const [product, images, variants] = await Promise.all([
|
||||
productPromise,
|
||||
imagesPromise,
|
||||
variantsPromise,
|
||||
])
|
||||
|
||||
return {
|
||||
product: {
|
||||
...mapItemRawToCommerceResponse(product),
|
||||
images: mapImagesRawToCommerceResponse(images),
|
||||
variants: mapVariantsRawToCommerceResponse(variants),
|
||||
options: mapVariantsOptionsToCommerceCommerce(variants),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return getProduct
|
||||
}
|
44
packages/olist/src/api/operations/get-site-info.ts
Normal file
44
packages/olist/src/api/operations/get-site-info.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import type { OperationContext } from '@vercel/commerce/api/operations'
|
||||
import type {
|
||||
Category,
|
||||
GetSiteInfoOperation,
|
||||
} from '@vercel/commerce/types/site'
|
||||
|
||||
import type { OlistConfig, Provider } from '..'
|
||||
|
||||
export type GetSiteInfoResult<
|
||||
T extends { categories: any[]; brands: any[] } = {
|
||||
categories: Category[]
|
||||
brands: any[]
|
||||
}
|
||||
> = T
|
||||
|
||||
export default function getSiteInfoOperation({
|
||||
commerce,
|
||||
}: OperationContext<Provider>) {
|
||||
async function getSiteInfo<T extends GetSiteInfoOperation>({
|
||||
config,
|
||||
}: {
|
||||
query?: string
|
||||
variables?: any
|
||||
config?: Partial<OlistConfig>
|
||||
preview?: boolean
|
||||
} = {}): Promise<T['data']> {
|
||||
const { service } = commerce.getConfig(config)
|
||||
|
||||
const tags = await service.tag.list({ paginate: { perPage: 10 } })
|
||||
|
||||
return {
|
||||
categories:
|
||||
tags?.map((category, idx) => ({
|
||||
id: category.name,
|
||||
name: category.name || `name-${idx}`,
|
||||
slug: category.name || `slug-${idx}`,
|
||||
path: `/${category.name}`,
|
||||
})) || [],
|
||||
brands: [],
|
||||
}
|
||||
}
|
||||
|
||||
return getSiteInfo
|
||||
}
|
6
packages/olist/src/api/operations/index.ts
Normal file
6
packages/olist/src/api/operations/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export { default as getAllPages } from './get-all-pages'
|
||||
export { default as getPage } from './get-page'
|
||||
export { default as getSiteInfo } from './get-site-info'
|
||||
export { default as getProduct } from './get-product'
|
||||
export { default as getAllProducts } from './get-all-products'
|
||||
export { default as getAllProductPaths } from './get-all-product-paths'
|
14
packages/olist/src/api/utils/fetch-graphql.ts
Normal file
14
packages/olist/src/api/utils/fetch-graphql.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import type { GraphQLFetcher } from '@vercel/commerce/api'
|
||||
import type { OlistConfig } from '../'
|
||||
|
||||
import { FetcherError } from '@vercel/commerce/utils/errors'
|
||||
|
||||
const fetchGraphqlApi: (getConfig: () => OlistConfig) => GraphQLFetcher =
|
||||
() => async () => {
|
||||
throw new FetcherError({
|
||||
errors: [{ message: 'GraphQL fetch is not implemented' }],
|
||||
status: 500,
|
||||
})
|
||||
}
|
||||
|
||||
export default fetchGraphqlApi
|
127
packages/olist/src/api/utils/fetch-rest.ts
Normal file
127
packages/olist/src/api/utils/fetch-rest.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import vercelFetch from '@vercel/fetch'
|
||||
import { FetcherError } from '@vercel/commerce/utils/errors'
|
||||
|
||||
import { OlistConfig } from '..'
|
||||
|
||||
export type FetchRest = <T>(
|
||||
method: string,
|
||||
resource: string,
|
||||
body?: Record<string, unknown>,
|
||||
fetchOptions?: Record<string, any>
|
||||
) => Promise<T>
|
||||
|
||||
// Get an instance to vercel fetch
|
||||
const fetch = vercelFetch()
|
||||
|
||||
export async function fetchData<T>(opts: {
|
||||
token: string
|
||||
path: string
|
||||
method: string
|
||||
config: OlistConfig
|
||||
fetchOptions?: Record<string, any>
|
||||
body?: Record<string, unknown>
|
||||
}): Promise<T> {
|
||||
// Destructure opts
|
||||
const { path, body, fetchOptions, config, token, method = 'GET' } = opts
|
||||
|
||||
// Do the request with the correct headers
|
||||
const dataResponse = await fetch(
|
||||
`${config.commerceUrl}/${config.apiVersion}${path}`,
|
||||
{
|
||||
...fetchOptions,
|
||||
method,
|
||||
headers: {
|
||||
...fetchOptions?.headers,
|
||||
'Content-Type': 'application/json',
|
||||
accept: 'application/json, text/plain, */*',
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
}
|
||||
)
|
||||
|
||||
// If something failed getting the data response
|
||||
if (!dataResponse.ok) {
|
||||
// Get the body of it
|
||||
const error = await dataResponse.textConverted()
|
||||
|
||||
// And return an error
|
||||
throw new FetcherError({
|
||||
errors: [{ message: error || dataResponse.statusText }],
|
||||
status: dataResponse.status,
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const result = { data: await dataResponse.json() }
|
||||
// Return data response as json
|
||||
return result as unknown as Promise<T>
|
||||
} catch (error) {
|
||||
// If response is empty return it as text
|
||||
return null as unknown as Promise<T>
|
||||
}
|
||||
}
|
||||
|
||||
export const createMiddlewareFetcher: (
|
||||
getConfig: () => OlistConfig
|
||||
) => FetchRest =
|
||||
(getConfig) =>
|
||||
async <T>(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: Record<string, unknown>,
|
||||
fetchOptions?: Record<string, any>
|
||||
) => {
|
||||
// Get provider config
|
||||
const config = getConfig()
|
||||
|
||||
// Get a token
|
||||
const token = config.apiToken
|
||||
|
||||
// Return the data and specify the expected type
|
||||
return fetchData<T>({
|
||||
token,
|
||||
fetchOptions,
|
||||
method,
|
||||
config,
|
||||
path,
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
export const createBuyerFetcher: (getConfig: () => OlistConfig) => FetchRest =
|
||||
(getConfig) =>
|
||||
async <T>(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: Record<string, unknown>,
|
||||
fetchOptions?: Record<string, any>
|
||||
) => {
|
||||
// Get provider config
|
||||
const config = getConfig()
|
||||
|
||||
// If a token was passed, set it on global
|
||||
if (fetchOptions?.token) {
|
||||
;(global as any).token = fetchOptions.token
|
||||
}
|
||||
|
||||
// Get a token
|
||||
if (!(global as any).token) {
|
||||
;(global as any).token = config.apiToken
|
||||
}
|
||||
|
||||
// Return the data and specify the expected type
|
||||
const data = await fetchData<T>({
|
||||
token: (global as any).token as string,
|
||||
fetchOptions,
|
||||
config,
|
||||
method,
|
||||
path,
|
||||
body,
|
||||
})
|
||||
|
||||
return {
|
||||
...data,
|
||||
meta: { token: (global as any).token as string },
|
||||
}
|
||||
}
|
9
packages/olist/src/api/utils/fetch.ts
Normal file
9
packages/olist/src/api/utils/fetch.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import {
|
||||
VndaServiceInstance,
|
||||
VndaServiceSingleton,
|
||||
} from '@vnda/headless-framework'
|
||||
|
||||
export const client = (
|
||||
apiToken: string,
|
||||
shopHost: string
|
||||
): VndaServiceSingleton => VndaServiceInstance(apiToken, { shopHost })
|
3
packages/olist/src/auth/index.ts
Normal file
3
packages/olist/src/auth/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { default as useLogin } from './use-login'
|
||||
export { default as useLogout } from './use-logout'
|
||||
export { default as useSignup } from './use-signup'
|
43
packages/olist/src/auth/use-login.tsx
Normal file
43
packages/olist/src/auth/use-login.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import { CommerceError } from '@vercel/commerce/utils/errors'
|
||||
import useLogin, { UseLogin } from '@vercel/commerce/auth/use-login'
|
||||
import type { LoginHook } from '@vercel/commerce/types/login'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
export default useLogin as UseLogin<typeof handler>
|
||||
|
||||
export const handler: MutationHook<LoginHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/login',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({ input: { email, password }, options, fetch }) {
|
||||
if (!(email && password)) {
|
||||
throw new CommerceError({
|
||||
message: 'An email and password are required to login',
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...options,
|
||||
body: { email, password },
|
||||
})
|
||||
},
|
||||
useHook: ({ fetch }) =>
|
||||
function useHook() {
|
||||
const { mutate } = useCustomer()
|
||||
|
||||
return useCallback(
|
||||
async function login(input) {
|
||||
const data = await fetch({ input })
|
||||
|
||||
await mutate()
|
||||
|
||||
return data
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[fetch, mutate]
|
||||
)
|
||||
},
|
||||
}
|
30
packages/olist/src/auth/use-logout.tsx
Normal file
30
packages/olist/src/auth/use-logout.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import useLogout, { UseLogout } from '@vercel/commerce/auth/use-logout'
|
||||
import type { LogoutHook } from '@vercel/commerce/types/logout'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
export default useLogout as UseLogout<typeof handler>
|
||||
|
||||
export const handler: MutationHook<LogoutHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/logout',
|
||||
method: 'GET',
|
||||
},
|
||||
useHook: ({ fetch }) =>
|
||||
function useHook() {
|
||||
const { mutate } = useCustomer()
|
||||
|
||||
return useCallback(
|
||||
async function logout() {
|
||||
const data = await fetch()
|
||||
|
||||
await mutate(null, false)
|
||||
|
||||
return data
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[fetch, mutate]
|
||||
)
|
||||
},
|
||||
}
|
48
packages/olist/src/auth/use-signup.tsx
Normal file
48
packages/olist/src/auth/use-signup.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import { CommerceError } from '@vercel/commerce/utils/errors'
|
||||
import useSignup, { UseSignup } from '@vercel/commerce/auth/use-signup'
|
||||
import type { SignupHook } from '@vercel/commerce/types/signup'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
export default useSignup as UseSignup<typeof handler>
|
||||
|
||||
export const handler: MutationHook<SignupHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/signup',
|
||||
method: 'POST',
|
||||
},
|
||||
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',
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...options,
|
||||
body: { firstName, lastName, email, password },
|
||||
})
|
||||
},
|
||||
useHook: ({ fetch }) =>
|
||||
function useHook() {
|
||||
const { mutate } = useCustomer()
|
||||
|
||||
return useCallback(
|
||||
async function signup(input) {
|
||||
const data = await fetch({ input })
|
||||
|
||||
await mutate()
|
||||
|
||||
return data
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[fetch, mutate]
|
||||
)
|
||||
},
|
||||
}
|
4
packages/olist/src/cart/index.ts
Normal file
4
packages/olist/src/cart/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export { default as useCart } from './use-cart'
|
||||
export { default as useAddItem } from './use-add-item'
|
||||
export { default as useRemoveItem } from './use-remove-item'
|
||||
export { default as useUpdateItem } from './use-update-item'
|
49
packages/olist/src/cart/use-add-item.tsx
Normal file
49
packages/olist/src/cart/use-add-item.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { useCallback } from 'react'
|
||||
import { CommerceError } from '@vercel/commerce/utils/errors'
|
||||
import useAddItem, { UseAddItem } from '@vercel/commerce/cart/use-add-item'
|
||||
|
||||
import type { AddItemHook } from '@vercel/commerce/types/cart'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
|
||||
import useCart from './use-cart'
|
||||
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export const handler: MutationHook<AddItemHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/cart',
|
||||
method: 'POST',
|
||||
},
|
||||
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 data = await fetch({
|
||||
...options,
|
||||
body: { item },
|
||||
})
|
||||
|
||||
return data
|
||||
},
|
||||
useHook: ({ fetch }) =>
|
||||
function useHook() {
|
||||
const { mutate } = useCart()
|
||||
|
||||
return useCallback(
|
||||
async function addItem(input) {
|
||||
const data = await fetch({ input })
|
||||
|
||||
await mutate(data, false)
|
||||
|
||||
return data
|
||||
},
|
||||
[mutate]
|
||||
)
|
||||
},
|
||||
}
|
33
packages/olist/src/cart/use-cart.tsx
Normal file
33
packages/olist/src/cart/use-cart.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { useMemo } from 'react'
|
||||
import { SWRHook } from '@vercel/commerce/utils/types'
|
||||
import useCart, { UseCart } from '@vercel/commerce/cart/use-cart'
|
||||
|
||||
import type { GetCartHook } from '@vercel/commerce/types/cart'
|
||||
|
||||
export default useCart as UseCart<typeof handler>
|
||||
|
||||
export const handler: SWRHook<GetCartHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/cart',
|
||||
method: 'GET',
|
||||
},
|
||||
useHook: ({ useData }) =>
|
||||
function useHook(input) {
|
||||
const response = useData({
|
||||
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
|
||||
})
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.create(response, {
|
||||
isEmpty: {
|
||||
get() {
|
||||
return (response.data?.lineItems?.length ?? 0) <= 0
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
}),
|
||||
[response]
|
||||
)
|
||||
},
|
||||
}
|
66
packages/olist/src/cart/use-remove-item.tsx
Normal file
66
packages/olist/src/cart/use-remove-item.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { useCallback } from 'react'
|
||||
import { ValidationError } from '@vercel/commerce/utils/errors'
|
||||
import useRemoveItem, {
|
||||
UseRemoveItem,
|
||||
} from '@vercel/commerce/cart/use-remove-item'
|
||||
|
||||
import type {
|
||||
Cart,
|
||||
LineItem,
|
||||
RemoveItemHook,
|
||||
} from '@vercel/commerce/types/cart'
|
||||
import type {
|
||||
MutationHookContext,
|
||||
HookFetcherContext,
|
||||
} from '@vercel/commerce/utils/types'
|
||||
|
||||
import useCart from './use-cart'
|
||||
|
||||
export type RemoveItemFn<T = any> = T extends LineItem
|
||||
? (input?: RemoveItemActionInput<T>) => Promise<Cart | null | undefined>
|
||||
: (input: RemoveItemActionInput<T>) => Promise<Cart | null>
|
||||
|
||||
export type RemoveItemActionInput<T = any> = T extends LineItem
|
||||
? Partial<RemoveItemHook['actionInput']>
|
||||
: RemoveItemHook['actionInput']
|
||||
|
||||
export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||
|
||||
export const handler = {
|
||||
fetchOptions: {
|
||||
url: '/api/cart',
|
||||
method: 'DELETE',
|
||||
},
|
||||
async fetcher({
|
||||
input: { itemId },
|
||||
options,
|
||||
fetch,
|
||||
}: HookFetcherContext<RemoveItemHook>) {
|
||||
return await fetch({ ...options, body: { itemId } })
|
||||
},
|
||||
useHook: ({ fetch }: MutationHookContext<RemoveItemHook>) =>
|
||||
function useHook<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
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
return useCallback(removeItem as RemoveItemFn<T>, [fetch, mutate])
|
||||
},
|
||||
}
|
94
packages/olist/src/cart/use-update-item.tsx
Normal file
94
packages/olist/src/cart/use-update-item.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import { useCallback } from 'react'
|
||||
import debounce from 'lodash.debounce'
|
||||
import { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import { ValidationError } from '@vercel/commerce/utils/errors'
|
||||
import useUpdateItem, {
|
||||
UseUpdateItem,
|
||||
} from '@vercel/commerce/cart/use-update-item'
|
||||
|
||||
import type { UpdateItemHook, LineItem } from '@vercel/commerce/types/cart'
|
||||
import type {
|
||||
HookFetcherContext,
|
||||
MutationHookContext,
|
||||
} from '@vercel/commerce/utils/types'
|
||||
|
||||
import useCart from './use-cart'
|
||||
import { handler as removeItemHandler } from './use-remove-item'
|
||||
|
||||
export type UpdateItemActionInput<T = any> = T extends LineItem
|
||||
? Partial<UpdateItemHook['actionInput']>
|
||||
: UpdateItemHook['actionInput']
|
||||
|
||||
export default useUpdateItem as UseUpdateItem<any>
|
||||
|
||||
export const handler: MutationHook<any> = {
|
||||
fetchOptions: {
|
||||
url: '/api/cart',
|
||||
method: 'PUT',
|
||||
},
|
||||
async fetcher({
|
||||
input: { itemId, item },
|
||||
options,
|
||||
fetch,
|
||||
}: HookFetcherContext<UpdateItemHook>) {
|
||||
if (Number.isInteger(item.quantity)) {
|
||||
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',
|
||||
})
|
||||
}
|
||||
|
||||
return await fetch({
|
||||
...options,
|
||||
body: { itemId, item },
|
||||
})
|
||||
},
|
||||
useHook: ({ fetch }: MutationHookContext<UpdateItemHook>) =>
|
||||
function useHook<T extends LineItem | undefined = undefined>(
|
||||
ctx: {
|
||||
item?: T
|
||||
wait?: number
|
||||
} = {}
|
||||
) {
|
||||
const { item } = ctx
|
||||
const { mutate } = useCart() as any
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
return useCallback(
|
||||
debounce(async (input: UpdateItemActionInput<T>) => {
|
||||
const itemId = input.id ?? item?.id
|
||||
const productId = input.productId ?? item?.productId
|
||||
const variantId = input.productId ?? item?.variantId
|
||||
|
||||
if (!itemId || !productId) {
|
||||
throw new ValidationError({
|
||||
message: 'Invalid input used for this operation',
|
||||
})
|
||||
}
|
||||
|
||||
const data = await fetch({
|
||||
input: {
|
||||
itemId,
|
||||
item: {
|
||||
productId,
|
||||
variantId: variantId || '',
|
||||
quantity: input.quantity,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await mutate(data, false)
|
||||
|
||||
return data
|
||||
}, ctx.wait ?? 500),
|
||||
[fetch, mutate]
|
||||
)
|
||||
},
|
||||
}
|
2
packages/olist/src/checkout/index.ts
Normal file
2
packages/olist/src/checkout/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as useSubmitCheckout } from './use-submit-checkout'
|
||||
export { default as useCheckout } from './use-checkout'
|
43
packages/olist/src/checkout/use-checkout.tsx
Normal file
43
packages/olist/src/checkout/use-checkout.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import type { GetCheckoutHook } from '@vercel/commerce/types/checkout'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { SWRHook } from '@vercel/commerce/utils/types'
|
||||
import useCheckout, {
|
||||
UseCheckout,
|
||||
} from '@vercel/commerce/checkout/use-checkout'
|
||||
import useSubmitCheckout from './use-submit-checkout'
|
||||
|
||||
export default useCheckout as UseCheckout<typeof handler>
|
||||
|
||||
export const handler: SWRHook<GetCheckoutHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/checkout',
|
||||
method: 'GET',
|
||||
},
|
||||
useHook: ({ useData }) =>
|
||||
function useHook(input) {
|
||||
const submit = useSubmitCheckout()
|
||||
const response = useData({
|
||||
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
|
||||
})
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.create(response, {
|
||||
isEmpty: {
|
||||
get() {
|
||||
return (response.data?.lineItems?.length ?? 0) <= 0
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
submit: {
|
||||
get() {
|
||||
return submit
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
}),
|
||||
[response, submit]
|
||||
)
|
||||
},
|
||||
}
|
38
packages/olist/src/checkout/use-submit-checkout.tsx
Normal file
38
packages/olist/src/checkout/use-submit-checkout.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import type { SubmitCheckoutHook } from '@vercel/commerce/types/checkout'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import useSubmitCheckout, {
|
||||
UseSubmitCheckout,
|
||||
} from '@vercel/commerce/checkout/use-submit-checkout'
|
||||
|
||||
export default useSubmitCheckout as UseSubmitCheckout<typeof handler>
|
||||
|
||||
export const handler: MutationHook<SubmitCheckoutHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/checkout',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({ input: item, options, fetch }) {
|
||||
// @TODO: Make form validations in here, import generic error like import { CommerceError } from '@vercel/commerce/utils/errors'
|
||||
// Get payment and delivery information in here
|
||||
|
||||
const data = await fetch({
|
||||
...options,
|
||||
body: { item },
|
||||
})
|
||||
|
||||
return data
|
||||
},
|
||||
useHook: ({ fetch }) =>
|
||||
function useHook() {
|
||||
return useCallback(
|
||||
async function onSubmitCheckout(input) {
|
||||
const data = await fetch({ input })
|
||||
|
||||
return data
|
||||
},
|
||||
[fetch]
|
||||
)
|
||||
},
|
||||
}
|
10
packages/olist/src/commerce.config.json
Normal file
10
packages/olist/src/commerce.config.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"provider": "olist",
|
||||
"features": {
|
||||
"wishlist": false,
|
||||
"cart": true,
|
||||
"search": true,
|
||||
"customerAuth": false,
|
||||
"customCheckout": false
|
||||
}
|
||||
}
|
10
packages/olist/src/constants.ts
Normal file
10
packages/olist/src/constants.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export const CART_COOKIE = 'olist.vnda.cart'
|
||||
export const CART_TOKEN_COOKIE = 'olist.vnda.cart.token'
|
||||
export const TOKEN_COOKIE = 'olist.vnda.token'
|
||||
export const CUSTOMER_COOKIE = 'olist.vnda.customer'
|
||||
export const API_URL = `https://${process.env.NEXT_PUBLIC_OLIST_STOREFRONT_DOMAIN}/api`
|
||||
export const API_VERSION = 'v2'
|
||||
export const LOCALE = 'en-us'
|
||||
export const STORE_DOMAIN = process.env.NEXT_PUBLIC_OLIST_STOREFRONT_DOMAIN
|
||||
export const API_TOKEN = process.env.NEXT_PUBLIC_OLIST_STOREFRONT_ACCESS_TOKEN
|
||||
export const CUSTOMER_TOKEN_COOKIE = 'olist.vnda.customer.token'
|
18
packages/olist/src/customer/address/use-add-item.tsx
Normal file
18
packages/olist/src/customer/address/use-add-item.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import useAddItem, {
|
||||
UseAddItem,
|
||||
} from '@vercel/commerce/customer/address/use-add-item'
|
||||
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export const handler: MutationHook<any> = {
|
||||
fetchOptions: {
|
||||
query: '',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {},
|
||||
useHook:
|
||||
({ fetch }) =>
|
||||
() =>
|
||||
async () => ({}),
|
||||
}
|
18
packages/olist/src/customer/card/use-add-item.tsx
Normal file
18
packages/olist/src/customer/card/use-add-item.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import useAddItem, {
|
||||
UseAddItem,
|
||||
} from '@vercel/commerce/customer/card/use-add-item'
|
||||
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export const handler: MutationHook<any> = {
|
||||
fetchOptions: {
|
||||
query: '',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {},
|
||||
useHook:
|
||||
({ fetch }) =>
|
||||
() =>
|
||||
async () => ({}),
|
||||
}
|
1
packages/olist/src/customer/index.ts
Normal file
1
packages/olist/src/customer/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as useCustomer } from './use-customer'
|
29
packages/olist/src/customer/use-customer.tsx
Normal file
29
packages/olist/src/customer/use-customer.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { SWRHook } from '@vercel/commerce/utils/types'
|
||||
import useCustomer, {
|
||||
UseCustomer,
|
||||
} from '@vercel/commerce/customer/use-customer'
|
||||
import type { CustomerHook } from '@vercel/commerce/types/customer'
|
||||
|
||||
export default useCustomer as UseCustomer<typeof handler>
|
||||
|
||||
export const handler: SWRHook<CustomerHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/customer',
|
||||
method: 'GET',
|
||||
},
|
||||
async fetcher({ options, fetch }) {
|
||||
const data = await fetch(options)
|
||||
return data?.customer ?? null
|
||||
},
|
||||
useHook:
|
||||
({ useData }) =>
|
||||
(input) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
return useData({
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...input?.swrOptions,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
17
packages/olist/src/fetcher.ts
Normal file
17
packages/olist/src/fetcher.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Fetcher } from '@vercel/commerce/utils/types'
|
||||
|
||||
const clientFetcher: Fetcher = async ({ method, url, body }) => {
|
||||
const response = await fetch(url!, {
|
||||
method,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((response) => response.data)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export default clientFetcher
|
12
packages/olist/src/index.tsx
Normal file
12
packages/olist/src/index.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { olistProvider, OlistProvider } from './provider'
|
||||
import {
|
||||
getCommerceProvider,
|
||||
useCommerce as useCoreCommerce,
|
||||
} from '@vercel/commerce'
|
||||
|
||||
export { olistProvider }
|
||||
export type { OlistProvider }
|
||||
|
||||
export const CommerceProvider = getCommerceProvider(olistProvider)
|
||||
|
||||
export const useCommerce = () => useCoreCommerce()
|
16
packages/olist/src/next.config.cjs
Normal file
16
packages/olist/src/next.config.cjs
Normal file
@ -0,0 +1,16 @@
|
||||
const commerce = require('./commerce.config.json')
|
||||
|
||||
module.exports = {
|
||||
commerce,
|
||||
images: {
|
||||
domains: [
|
||||
'localhost',
|
||||
'b0.vnda.com.br',
|
||||
'b1.vnda.com.br',
|
||||
'b2.vnda.com.br',
|
||||
'b3.vnda.com.br',
|
||||
'b4.vnda.com.br',
|
||||
'cdn.vnda.dev',
|
||||
],
|
||||
},
|
||||
}
|
2
packages/olist/src/product/index.ts
Normal file
2
packages/olist/src/product/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as usePrice } from './use-price'
|
||||
export { default as useSearch } from './use-search'
|
2
packages/olist/src/product/use-price.tsx
Normal file
2
packages/olist/src/product/use-price.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
export * from '@vercel/commerce/product/use-price'
|
||||
export { default } from '@vercel/commerce/product/use-price'
|
39
packages/olist/src/product/use-search.tsx
Normal file
39
packages/olist/src/product/use-search.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { SWRHook } from '@vercel/commerce/utils/types'
|
||||
import useSearch, { UseSearch } from '@vercel/commerce/product/use-search'
|
||||
import { SearchProductsHook } from '@vercel/commerce/types/product'
|
||||
export default useSearch as UseSearch<typeof handler>
|
||||
|
||||
export const handler: SWRHook<SearchProductsHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/catalog/products',
|
||||
method: 'GET',
|
||||
},
|
||||
fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) {
|
||||
const url = new URLSearchParams()
|
||||
|
||||
if (search) url.set('search', String(search))
|
||||
if (categoryId) url.set('categoryId', String(categoryId))
|
||||
if (brandId) url.set('brandId', String(brandId))
|
||||
if (sort) url.set('sort', String(sort))
|
||||
|
||||
return fetch({
|
||||
url: `${options.url!}?${url}`,
|
||||
method: options.method,
|
||||
})
|
||||
},
|
||||
useHook: ({ useData }) =>
|
||||
function useHook(input = {}) {
|
||||
return useData({
|
||||
input: [
|
||||
['search', input.search],
|
||||
['categoryId', input.categoryId],
|
||||
['brandId', input.brandId],
|
||||
['sort', input.sort],
|
||||
],
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...input.swrOptions,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
51
packages/olist/src/provider.ts
Normal file
51
packages/olist/src/provider.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { handler as useCart } from './cart/use-cart'
|
||||
import { handler as useAddCartItem } from './cart/use-add-item'
|
||||
import { handler as useUpdateCartItem } from './cart/use-update-item'
|
||||
import { handler as useRemoveCartItem } from './cart/use-remove-item'
|
||||
|
||||
import { handler as useCustomer } from './customer/use-customer'
|
||||
import { handler as useSearch } from './product/use-search'
|
||||
|
||||
import { handler as useLogin } from './auth/use-login'
|
||||
import { handler as useLogout } from './auth/use-logout'
|
||||
import { handler as useSignup } from './auth/use-signup'
|
||||
|
||||
import { handler as useCheckout } from './checkout/use-checkout'
|
||||
import { handler as useSubmitCheckout } from './checkout/use-submit-checkout'
|
||||
|
||||
import { handler as useAddCardItem } from './customer/card/use-add-item'
|
||||
|
||||
import { handler as useAddAddressItem } from './customer/address/use-add-item'
|
||||
|
||||
import { CART_COOKIE, CART_TOKEN_COOKIE, LOCALE } from './constants'
|
||||
import { default as fetcher } from './fetcher'
|
||||
|
||||
export const olistProvider = {
|
||||
locale: LOCALE,
|
||||
cartCookie: CART_COOKIE,
|
||||
cartTokenCookie: CART_TOKEN_COOKIE,
|
||||
fetcher,
|
||||
cart: {
|
||||
useCart,
|
||||
useAddItem: useAddCartItem,
|
||||
useUpdateItem: useUpdateCartItem,
|
||||
useRemoveItem: useRemoveCartItem,
|
||||
},
|
||||
checkout: {
|
||||
useCheckout,
|
||||
useSubmitCheckout,
|
||||
},
|
||||
customer: {
|
||||
useCustomer,
|
||||
card: {
|
||||
useAddItem: useAddCardItem,
|
||||
},
|
||||
address: {
|
||||
useAddItem: useAddAddressItem,
|
||||
},
|
||||
},
|
||||
products: { useSearch },
|
||||
auth: { useLogin, useLogout, useSignup },
|
||||
}
|
||||
|
||||
export type OlistProvider = typeof olistProvider
|
12
packages/olist/src/types/api.ts
Normal file
12
packages/olist/src/types/api.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import type { OlistConfig } from '../api'
|
||||
|
||||
export type Handler = {
|
||||
req: NextApiRequest
|
||||
res: NextApiResponse
|
||||
config: OlistConfig
|
||||
}
|
||||
|
||||
export type FetcherResponse<T> = {
|
||||
data: T
|
||||
}
|
95
packages/olist/src/types/cart.ts
Normal file
95
packages/olist/src/types/cart.ts
Normal file
@ -0,0 +1,95 @@
|
||||
export * from '@vercel/commerce/types/cart'
|
||||
|
||||
export type RawCartItemRequest = {
|
||||
sku: string
|
||||
quantity: number
|
||||
extra?: {}
|
||||
place_id?: number
|
||||
}
|
||||
|
||||
export type RawCartResponse = {
|
||||
agent: string
|
||||
billing_address_id: number
|
||||
channel: string
|
||||
client_id: number
|
||||
code: string
|
||||
coupon_code: string
|
||||
discount: {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
facebook: false
|
||||
valid_to: string
|
||||
seal_uid: string
|
||||
seal_url: string
|
||||
start_at: string
|
||||
end_at: string
|
||||
email: string
|
||||
cpf: string
|
||||
tags: string
|
||||
}
|
||||
discount_price: number
|
||||
extra: Record<string, any>
|
||||
id: number
|
||||
items: RawCartItemResponse[]
|
||||
items_count: number
|
||||
shipping_address_id: number
|
||||
shipping_method: string
|
||||
shipping_methods: [
|
||||
{
|
||||
package: string
|
||||
name: string
|
||||
label: string
|
||||
price: string
|
||||
delivery_days: string
|
||||
delivery_type: string
|
||||
description: string
|
||||
short_description: string
|
||||
}
|
||||
]
|
||||
shipping_price: number
|
||||
subtotal: number
|
||||
token: string
|
||||
total: number
|
||||
total_for_deposit: number
|
||||
total_for_slip: number
|
||||
total_for_pix: number
|
||||
updated_at: string
|
||||
rebate_token: string
|
||||
rebate_discount: number
|
||||
handling_days: number
|
||||
subtotal_discount: number
|
||||
total_discount: number
|
||||
}
|
||||
|
||||
export type RawCartItemResponse = {
|
||||
id: string
|
||||
available_quantity: number
|
||||
delivery_days: number
|
||||
extra: Record<string, any>
|
||||
place_id: number
|
||||
price: number
|
||||
product_id: number
|
||||
product_name: string
|
||||
product_reference: string
|
||||
product_url: string
|
||||
product_type: string
|
||||
quantity: number
|
||||
seller: string
|
||||
seller_name: string
|
||||
subtotal: number
|
||||
total: number
|
||||
updated_at: string
|
||||
has_customizations: boolean
|
||||
image_url: string
|
||||
variant_attributes: Record<string, any>
|
||||
variant_min_quantity: number
|
||||
variant_name: string
|
||||
variant_price: number
|
||||
variant_intl_price: number
|
||||
variant_properties: Record<
|
||||
string,
|
||||
{ name: string; value: string; defining: boolean }
|
||||
>
|
||||
variant_sku: string
|
||||
}
|
8
packages/olist/src/types/category.ts
Normal file
8
packages/olist/src/types/category.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface RawCategory {
|
||||
id: string
|
||||
name: string
|
||||
title: string
|
||||
subtitle: null | string
|
||||
description: null | string
|
||||
image_url: null | string
|
||||
}
|
4
packages/olist/src/types/checkout.ts
Normal file
4
packages/olist/src/types/checkout.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import * as Core from '@vercel/commerce/types/checkout'
|
||||
|
||||
export type CheckoutTypes = Core.CheckoutTypes
|
||||
export type CheckoutSchema = Core.CheckoutSchema<CheckoutTypes>
|
51
packages/olist/src/types/customer.ts
Normal file
51
packages/olist/src/types/customer.ts
Normal file
@ -0,0 +1,51 @@
|
||||
export * from '@vercel/commerce/types/customer'
|
||||
|
||||
export type RawCustomerResponse = {
|
||||
id: number
|
||||
first_name: string
|
||||
last_name: string
|
||||
email: string
|
||||
gender: string
|
||||
phone_area: string
|
||||
phone: string
|
||||
cpf: string
|
||||
cnpj: string
|
||||
ie: string
|
||||
tags: string
|
||||
lists: string[]
|
||||
facebook_uid: string
|
||||
liked_facebook_page: boolean
|
||||
updated_at: string
|
||||
birthdate: string
|
||||
recent_address: [
|
||||
{
|
||||
id: string
|
||||
first_name: string
|
||||
last_name: string
|
||||
company_name: string
|
||||
street_name: string
|
||||
street_number: string
|
||||
neighborhood: string
|
||||
complement: string
|
||||
reference: string
|
||||
city: string
|
||||
state: string
|
||||
zip: string
|
||||
first_phone_area: string
|
||||
first_phone: string
|
||||
second_phone_area: string
|
||||
second_phone: string
|
||||
email: string
|
||||
documents: {
|
||||
cpf: string
|
||||
cnpj: string
|
||||
}
|
||||
}
|
||||
]
|
||||
auth_token: string
|
||||
last_confirmed_order_at: string
|
||||
received_orders_count: number
|
||||
confirmed_orders_count: number
|
||||
canceled_orders_count: number
|
||||
renew_password: boolean
|
||||
}
|
7
packages/olist/src/types/global.d.ts
vendored
Normal file
7
packages/olist/src/types/global.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
token: string | undefined | null
|
||||
}
|
||||
}
|
||||
}
|
3
packages/olist/src/types/index.d.ts
vendored
Normal file
3
packages/olist/src/types/index.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
declare module globalThis {
|
||||
var token: string | null | undefined
|
||||
}
|
6
packages/olist/src/types/login.ts
Normal file
6
packages/olist/src/types/login.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from '@vercel/commerce/types/login'
|
||||
|
||||
export type RawLoginResponse = {
|
||||
id: number
|
||||
auth_token: string
|
||||
}
|
1
packages/olist/src/types/logout.ts
Normal file
1
packages/olist/src/types/logout.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from '@vercel/commerce/types/logout'
|
63
packages/olist/src/types/product.ts
Normal file
63
packages/olist/src/types/product.ts
Normal file
@ -0,0 +1,63 @@
|
||||
export interface RawVariantProperty {
|
||||
name: string
|
||||
value: string
|
||||
defining?: boolean
|
||||
}
|
||||
|
||||
export interface RawVariantProperties {
|
||||
[key: string]: RawVariantProperty
|
||||
}
|
||||
|
||||
export interface RawVariant {
|
||||
id: number
|
||||
main: boolean
|
||||
available: boolean
|
||||
sku: string
|
||||
name: string
|
||||
slug: string
|
||||
min_quantity: number
|
||||
quantity: number
|
||||
quantity_sold: number
|
||||
stock: number
|
||||
custom_attributes: object
|
||||
properties?: RawVariantProperties
|
||||
updated_at: string
|
||||
price: number
|
||||
installments: number[]
|
||||
available_quantity: number
|
||||
weight: number
|
||||
width: number
|
||||
height: number
|
||||
length: number
|
||||
handling_days: number
|
||||
inventories: any[]
|
||||
sale_price: number
|
||||
intl_price: number
|
||||
image_url: string
|
||||
product_id: number
|
||||
barcode: string
|
||||
norder: number
|
||||
}
|
||||
|
||||
export interface RawProduct {
|
||||
id: number
|
||||
name: string
|
||||
active: boolean
|
||||
available: boolean
|
||||
description: string | null
|
||||
html_description?: string | null
|
||||
image_url?: string | null
|
||||
price: number
|
||||
sale_price: number
|
||||
reference: string
|
||||
slug: string
|
||||
url: string
|
||||
variants?: RawVariant[]
|
||||
}
|
||||
|
||||
export interface RawProductImages {
|
||||
id: number
|
||||
url: string
|
||||
updated_at: string
|
||||
variant_ids?: string[] | number[]
|
||||
}
|
58
packages/olist/src/types/signup.ts
Normal file
58
packages/olist/src/types/signup.ts
Normal file
@ -0,0 +1,58 @@
|
||||
export * from '@vercel/commerce/types/signup'
|
||||
|
||||
export type RawSignUpRequest = {
|
||||
email: string
|
||||
first_name: string
|
||||
last_name: string
|
||||
birthdate: string
|
||||
gender: string
|
||||
tags: string
|
||||
lists: string[]
|
||||
password: string
|
||||
password_confirmation: string
|
||||
terms: boolean
|
||||
}
|
||||
|
||||
export type RawSignUpResponse = {
|
||||
id: number
|
||||
first_name: string
|
||||
last_name: string
|
||||
email: string
|
||||
gender: string
|
||||
phone_area: string
|
||||
phone: string
|
||||
cpf: string
|
||||
cnpj: string
|
||||
ie: string
|
||||
tags: string
|
||||
lists: string[]
|
||||
facebook_uid: string
|
||||
liked_facebook_page: boolean
|
||||
updated_at: string
|
||||
birthdate: string
|
||||
recent_address: [
|
||||
{
|
||||
id: string
|
||||
first_name: string
|
||||
last_name: string
|
||||
company_name: string
|
||||
street_name: string
|
||||
street_number: string
|
||||
neighborhood: string
|
||||
complement: string
|
||||
reference: string
|
||||
city: string
|
||||
state: string
|
||||
zip: string
|
||||
first_phone_area: string
|
||||
first_phone: string
|
||||
second_phone_area: string
|
||||
second_phone: string
|
||||
email: string
|
||||
documents: {
|
||||
cpf: string
|
||||
cnpj: string
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
77
packages/olist/src/utils/cart.ts
Normal file
77
packages/olist/src/utils/cart.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import type { Cart, CartItemBody, LineItem } from '@vercel/commerce/types/cart'
|
||||
import type {
|
||||
Cart as CartRequest,
|
||||
CartItem,
|
||||
CartAddItemRequest,
|
||||
} from '@vnda/headless-framework'
|
||||
|
||||
import { getLastItem } from './product'
|
||||
|
||||
export const mapCommerceToRawRequest = ({
|
||||
quantity,
|
||||
variantId,
|
||||
}: CartItemBody): CartAddItemRequest => ({
|
||||
quantity: quantity || 1,
|
||||
sku: variantId,
|
||||
})
|
||||
|
||||
export const mapItemRawToCommerceResponse = ({
|
||||
id,
|
||||
quantity,
|
||||
productId,
|
||||
productName,
|
||||
productUrl,
|
||||
variantName,
|
||||
variantSku,
|
||||
variantPrice,
|
||||
imageUrl,
|
||||
availableQuantity,
|
||||
variantProperties,
|
||||
}: CartItem): LineItem => ({
|
||||
id: id.toString(),
|
||||
variantId: variantSku,
|
||||
productId: productId.toString(),
|
||||
name: productName,
|
||||
quantity: quantity,
|
||||
discounts: [],
|
||||
path: getLastItem(productUrl),
|
||||
variant: {
|
||||
id: variantSku,
|
||||
sku: variantSku,
|
||||
name: variantName,
|
||||
requiresShipping: false,
|
||||
price: variantPrice,
|
||||
listPrice: variantPrice,
|
||||
isInStock: availableQuantity > 0,
|
||||
availableForSale: availableQuantity > 0,
|
||||
image: {
|
||||
url: imageUrl || 'http://localhost:3000/',
|
||||
},
|
||||
},
|
||||
options: Object.values(variantProperties).map(({ name, value }) => ({
|
||||
name: name === 'Cor' ? 'Color' : name,
|
||||
value,
|
||||
})),
|
||||
})
|
||||
|
||||
export const mapRawToCommerceResponse = ({
|
||||
id,
|
||||
clientId,
|
||||
discountPrice,
|
||||
subtotal,
|
||||
total,
|
||||
items,
|
||||
}: CartRequest): Cart => ({
|
||||
id: id?.toString(),
|
||||
customerId: clientId?.toString(),
|
||||
createdAt: '',
|
||||
currency: {
|
||||
code: 'BRL',
|
||||
},
|
||||
taxesIncluded: false,
|
||||
lineItems: items?.map(mapItemRawToCommerceResponse),
|
||||
lineItemsSubtotalPrice: 0,
|
||||
subtotalPrice: subtotal,
|
||||
totalPrice: total,
|
||||
discounts: [{ value: discountPrice }],
|
||||
})
|
122
packages/olist/src/utils/product.ts
Normal file
122
packages/olist/src/utils/product.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import type {
|
||||
Product,
|
||||
ProductImage,
|
||||
ProductOption,
|
||||
ProductVariant,
|
||||
} from '@vercel/commerce/types/product'
|
||||
import type {
|
||||
Variant,
|
||||
Product as ProductRequest,
|
||||
ProductImage as ProductImageRequest,
|
||||
} from '@vnda/headless-framework'
|
||||
|
||||
export const getLastItem = (path: string) =>
|
||||
path.substring(path.lastIndexOf('/') + 1)
|
||||
|
||||
const getRelativePaths = (url: string) =>
|
||||
!['http:', 'https:'].includes(url) ? `https:${url}` : url
|
||||
|
||||
export const getVariantOptions = (variant: Variant): ProductOption[] =>
|
||||
Object.entries(variant.properties || [])
|
||||
.filter(([_, { value }]) => !!value && !!value)
|
||||
.map(([_, property]) => ({
|
||||
__typename: 'MultipleChoiceOption',
|
||||
id: `option-${property.name.toLocaleLowerCase()}`,
|
||||
displayName: property.name,
|
||||
values: [{ label: property.value }],
|
||||
}))
|
||||
|
||||
export const mapImagesRawToCommerceResponse = (
|
||||
images: ProductImageRequest[]
|
||||
): ProductImage[] =>
|
||||
(images || []).map((item) => ({
|
||||
url: getRelativePaths(item.url),
|
||||
alt: `image-for-item-id-${item.id}`,
|
||||
}))
|
||||
|
||||
export const mapVariantsOptionsToCommerceCommerce = (
|
||||
variants: Variant[]
|
||||
): ProductOption[] => {
|
||||
if (!variants || !variants.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
let options: ProductOption[] = []
|
||||
|
||||
variants.forEach((variant) => {
|
||||
const opts = getVariantOptions(variant)
|
||||
|
||||
opts.forEach((opt) => {
|
||||
if (options.filter((option) => option.id !== opt.id)) {
|
||||
options.push({
|
||||
id: opt.id,
|
||||
displayName: opt.displayName,
|
||||
values: opt.values,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
var ids = Array.from(new Set(options.map((d) => d.id)))
|
||||
|
||||
options = ids.map((id) => {
|
||||
let values: any[] = []
|
||||
|
||||
options
|
||||
.filter((option) => option.id === id)
|
||||
.map(({ values }) =>
|
||||
values?.forEach(({ label, hexColors }) => {
|
||||
if (label && !values.find((value) => value?.label === label)) {
|
||||
values.push({ label, hexColors })
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
id,
|
||||
displayName: options.find((option) => option.id === id)?.displayName!,
|
||||
values: Array.from(new Set(values)),
|
||||
}
|
||||
})
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
export const mapVariantsRawToCommerceResponse = (
|
||||
variants: Variant[]
|
||||
): ProductVariant[] =>
|
||||
(variants || []).map((variant) => ({
|
||||
id: variant.sku,
|
||||
options: getVariantOptions(variant),
|
||||
availableForSale: variant.stock ? variant.stock > 0 : false,
|
||||
}))
|
||||
|
||||
export const mapItemRawToCommerceResponse = (
|
||||
product: ProductRequest
|
||||
): Product => ({
|
||||
id: product.id.toString(),
|
||||
description: product.description || '',
|
||||
images: product.imageUrl ? [{ url: getRelativePaths(product.imageUrl) }] : [],
|
||||
name: product.name,
|
||||
price: {
|
||||
value: product.price,
|
||||
currencyCode: 'BRL',
|
||||
salePrice: product.salePrice,
|
||||
},
|
||||
...(product.htmlDescription && {
|
||||
descriptionHtml: product.htmlDescription,
|
||||
}),
|
||||
sku: product.reference,
|
||||
slug: `${product.slug}-${product.id.toString()}`,
|
||||
path: product.url ? getLastItem(product.url) : undefined,
|
||||
options: [],
|
||||
variants: [],
|
||||
})
|
||||
|
||||
export const extractProductId = (value?: string) => {
|
||||
if (!value || typeof value !== 'string') {
|
||||
return value
|
||||
}
|
||||
|
||||
return value.substring(value.lastIndexOf('-') + 1)
|
||||
}
|
15
packages/olist/src/utils/signup.ts
Normal file
15
packages/olist/src/utils/signup.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import type { RawSignUpRequest, SignupBody } from '../types/signup'
|
||||
|
||||
export const mapCommerceToRawRequest = ({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
password,
|
||||
}: SignupBody): Partial<RawSignUpRequest> => ({
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
email,
|
||||
password,
|
||||
password_confirmation: password,
|
||||
terms: true,
|
||||
})
|
13
packages/olist/src/wishlist/use-add-item.tsx
Normal file
13
packages/olist/src/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
packages/olist/src/wishlist/use-remove-item.tsx
Normal file
17
packages/olist/src/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
|
43
packages/olist/src/wishlist/use-wishlist.tsx
Normal file
43
packages/olist/src/wishlist/use-wishlist.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { HookFetcher } from '@vercel/commerce/utils/types'
|
||||
import type { Product } from '@vercel/commerce/types/product'
|
||||
|
||||
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)
|
20
packages/olist/taskfile.js
Normal file
20
packages/olist/taskfile.js
Normal file
@ -0,0 +1,20 @@
|
||||
export async function build(task, opts) {
|
||||
await task
|
||||
.source('src/**/*.+(ts|tsx|js)')
|
||||
.swc({ dev: opts.dev, outDir: 'dist', baseUrl: 'src' })
|
||||
.target('dist')
|
||||
.source('src/**/*.+(cjs|json)')
|
||||
.target('dist')
|
||||
task.$.log('Compiled src files')
|
||||
}
|
||||
|
||||
export async function release(task) {
|
||||
await task.clear('dist').start('build')
|
||||
}
|
||||
|
||||
export default async function dev(task) {
|
||||
const opts = { dev: true }
|
||||
await task.clear('dist')
|
||||
await task.start('build', opts)
|
||||
await task.watch('src/**/*.+(ts|tsx|js|cjs|json)', 'build', opts)
|
||||
}
|
21
packages/olist/tsconfig.json
Normal file
21
packages/olist/tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"outDir": "dist",
|
||||
"baseUrl": "src",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"declaration": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"incremental": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
@ -65,7 +65,7 @@
|
||||
"@types/node": "^17.0.8",
|
||||
"@types/react": "^17.0.38",
|
||||
"lint-staged": "^12.1.7",
|
||||
"next": "^12.0.8",
|
||||
"next": "^12",
|
||||
"prettier": "^2.5.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
@ -70,7 +70,7 @@
|
||||
"@types/node": "^17.0.8",
|
||||
"@types/react": "^17.0.38",
|
||||
"lint-staged": "^12.1.7",
|
||||
"next": "^12.0.8",
|
||||
"next": "^12",
|
||||
"prettier": "^2.5.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
@ -63,7 +63,7 @@
|
||||
"@types/node": "^17.0.8",
|
||||
"@types/react": "^17.0.38",
|
||||
"lint-staged": "^12.1.7",
|
||||
"next": "^12.0.8",
|
||||
"next": "^12",
|
||||
"prettier": "^2.5.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
@ -71,7 +71,7 @@
|
||||
"@types/react": "^17.0.38",
|
||||
"dotenv": "^12.0.3",
|
||||
"lint-staged": "^12.1.7",
|
||||
"next": "^12.0.8",
|
||||
"next": "^12",
|
||||
"prettier": "^2.5.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
@ -66,7 +66,7 @@
|
||||
"@types/node": "^17.0.8",
|
||||
"@types/react": "^17.0.38",
|
||||
"lint-staged": "^12.1.7",
|
||||
"next": "^12.0.8",
|
||||
"next": "^12",
|
||||
"prettier": "^2.5.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
@ -6,7 +6,7 @@ const requireConfig = <T>(isomorphicConfig: T, key: keyof T) => {
|
||||
|
||||
if (typeof valueUnderKey === 'undefined') {
|
||||
throw new MissingConfigurationValueError(
|
||||
`Value for configuration key ${key} was undefined.`
|
||||
`Value for configuration key ${key as string} was undefined.`
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -66,7 +66,7 @@
|
||||
"@types/node": "^17.0.8",
|
||||
"@types/react": "^17.0.38",
|
||||
"lint-staged": "^12.1.7",
|
||||
"next": "^12.0.8",
|
||||
"next": "^12",
|
||||
"prettier": "^2.5.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
@ -68,7 +68,7 @@
|
||||
"@types/node": "^17.0.8",
|
||||
"@types/react": "^17.0.38",
|
||||
"lint-staged": "^12.1.7",
|
||||
"next": "^12.0.8",
|
||||
"next": "^12",
|
||||
"prettier": "^2.5.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
@ -10,6 +10,7 @@
|
||||
# @vercel/commerce-kibocommerce
|
||||
# @vercel/commerce-commercejs
|
||||
# @vercel/commerce-sfcc
|
||||
# @vercel/commerce-olist
|
||||
COMMERCE_PROVIDER=
|
||||
|
||||
BIGCOMMERCE_STOREFRONT_API_URL=
|
||||
@ -35,6 +36,9 @@ NEXT_PUBLIC_SALEOR_CHANNEL=
|
||||
NEXT_PUBLIC_VENDURE_SHOP_API_URL=
|
||||
NEXT_PUBLIC_VENDURE_LOCAL_URL=
|
||||
|
||||
NEXT_PUBLIC_OLIST_STOREFRONT_DOMAIN=
|
||||
NEXT_PUBLIC_OLIST_STOREFRONT_ACCESS_TOKEN=
|
||||
|
||||
ORDERCLOUD_CLIENT_ID=
|
||||
ORDERCLOUD_CLIENT_SECRET=
|
||||
STRIPE_SECRET=
|
||||
|
@ -20,6 +20,7 @@ const PROVIDERS = [
|
||||
'@vercel/commerce-spree',
|
||||
'@vercel/commerce-commercejs',
|
||||
'@vercel/commerce-sfcc',
|
||||
'@vercel/commerce-olist',
|
||||
]
|
||||
|
||||
function getProviderName() {
|
||||
@ -31,6 +32,8 @@ function getProviderName() {
|
||||
? '@vercel/commerce-shopify'
|
||||
: process.env.NEXT_PUBLIC_SWELL_STORE_ID
|
||||
? '@vercel/commerce-swell'
|
||||
: process.env.NEXT_PUBLIC_OLIST_STOREFRONT_DOMAIN
|
||||
? '@vercel/commerce-olist'
|
||||
: '@vercel/commerce-local')
|
||||
)
|
||||
}
|
||||
@ -40,6 +43,7 @@ function withCommerceConfig(nextConfig = {}) {
|
||||
{ commerce: { provider: getProviderName() } },
|
||||
nextConfig
|
||||
)
|
||||
|
||||
const { commerce } = config
|
||||
const { provider } = commerce
|
||||
|
||||
|
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