mirror of
https://github.com/vercel/commerce.git
synced 2025-07-01 02:41:22 +00:00
Shopify i18n (#9)
This commit is contained in:
parent
05501c6f99
commit
628fbf50bb
@ -1,5 +1,15 @@
|
|||||||
|
# Available providers: bigcommerce, shopify, swell
|
||||||
|
COMMERCE_PROVIDER=
|
||||||
|
|
||||||
BIGCOMMERCE_STOREFRONT_API_URL=
|
BIGCOMMERCE_STOREFRONT_API_URL=
|
||||||
BIGCOMMERCE_STOREFRONT_API_TOKEN=
|
BIGCOMMERCE_STOREFRONT_API_TOKEN=
|
||||||
BIGCOMMERCE_STORE_API_URL=
|
BIGCOMMERCE_STORE_API_URL=
|
||||||
BIGCOMMERCE_STORE_API_TOKEN=
|
BIGCOMMERCE_STORE_API_TOKEN=
|
||||||
BIGCOMMERCE_STORE_API_CLIENT_ID=
|
BIGCOMMERCE_STORE_API_CLIENT_ID=
|
||||||
|
BIGCOMMERCE_CHANNEL_ID=
|
||||||
|
|
||||||
|
NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=
|
||||||
|
NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=
|
||||||
|
|
||||||
|
NEXT_PUBLIC_SWELL_STORE_ID=
|
||||||
|
NEXT_PUBLIC_SWELL_PUBLIC_KEY=
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -18,6 +18,7 @@ out/
|
|||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
|
.idea
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false
|
||||||
|
}
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["esbenp.prettier-vscode"]
|
||||||
|
}
|
@ -1,4 +0,0 @@
|
|||||||
## Changelog
|
|
||||||
|
|
||||||
- Select Variants Working
|
|
||||||
- Click on cart item title, closes the sidebar
|
|
116
README.md
116
README.md
@ -7,7 +7,10 @@ Start right now at [nextjs.org/commerce](https://nextjs.org/commerce)
|
|||||||
|
|
||||||
Demo live at: [demo.vercel.store](https://demo.vercel.store/)
|
Demo live at: [demo.vercel.store](https://demo.vercel.store/)
|
||||||
|
|
||||||
This project is currently <b>under development</b>.
|
- Shopify Demo: https://shopify.vercel.store/
|
||||||
|
- Swell Demo: https://swell.vercel.store/
|
||||||
|
- BigCommerce Demo: https://bigcommerce.vercel.store/
|
||||||
|
- Vendure Demo: https://vendure.vercel.store
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@ -21,82 +24,66 @@ This project is currently <b>under development</b>.
|
|||||||
- Integrations - Integrate seamlessly with the most common ecommerce platforms.
|
- Integrations - Integrate seamlessly with the most common ecommerce platforms.
|
||||||
- Dark Mode Support
|
- Dark Mode Support
|
||||||
|
|
||||||
## Work in progress
|
|
||||||
|
|
||||||
We're using Github Projects to keep track of issues in progress and todo's. Here is our [Board](https://github.com/vercel/commerce/projects/1)
|
|
||||||
|
|
||||||
## Integrations
|
## Integrations
|
||||||
|
|
||||||
Next.js Commerce integrates out-of-the-box with BigCommerce. We plan to support all major ecommerce backends.
|
Next.js Commerce integrates out-of-the-box with BigCommerce and Shopify. We plan to support all major ecommerce backends.
|
||||||
|
|
||||||
## Goals
|
## Considerations
|
||||||
|
|
||||||
- **Next.js Commerce** should have a completely data **agnostic** UI
|
- `framework/commerce` contains all types, helpers and functions to be used as base to build a new **provider**.
|
||||||
- **Aware of schema**: should ship with the right data schemas and types.
|
- **Providers** live under `framework`'s root folder and they will extend Next.js Commerce types and functionality (`framework/commerce`).
|
||||||
- All providers should return the right data types and schemas to blend correctly with Next.js Commerce.
|
- We have a **Features API** to ensure feature parity between the UI and the Provider. The UI should update accordingly and no extra code should be bundled. All extra configuration for features will live under `features` in `commerce.config.json` and if needed it can also be accessed programatically.
|
||||||
- `@framework` will be the alias utilized in commerce and it will map to the ecommerce provider of preference- e.g BigCommerce, Shopify, Swell. All providers should expose the same standardized functions. _Note that the same applies for recipes using a CMS + an ecommerce provider._
|
- Each **provider** should add its corresponding `next.config.js` and `commerce.config.json` adding specific data related to the provider. For example in case of BigCommerce, the images CDN and additional API routes.
|
||||||
|
- **Providers don't depend on anything that's specific to the application they're used in**. They only depend on `framework/commerce`, on their own framework folder and on some dependencies included in `package.json`
|
||||||
|
|
||||||
There is a `framework` folder in the root folder that will contain multiple ecommerce providers.
|
## Configuration
|
||||||
|
|
||||||
Additionally, we need to ensure feature parity (not all providers have e.g. wishlist) we will also have to build a feature API to disable/enable features in the UI.
|
### How to change providers
|
||||||
|
|
||||||
People actively working on this project: @okbel & @lfades.
|
Open `.env.local` and change the value of `COMMERCE_PROVIDER` to the provider you would like to use, then set the environment variables for that provider (use `.env.template` as the base).
|
||||||
|
|
||||||
## Framework
|
The setup for Shopify would look like this for example:
|
||||||
|
|
||||||
Framework is where the data comes from. It contains mostly hooks and functions.
|
```
|
||||||
|
COMMERCE_PROVIDER=shopify
|
||||||
## Structure
|
NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=xxxxxxx.myshopify.com
|
||||||
Main folder and its exposed functions
|
|
||||||
|
|
||||||
- `product`
|
|
||||||
- usePrice
|
|
||||||
- useSearch
|
|
||||||
- getProduct
|
|
||||||
- getAllProducts
|
|
||||||
- `wishlist`
|
|
||||||
- useWishlist
|
|
||||||
- addWishlistItem
|
|
||||||
- removeWishlistItem
|
|
||||||
- `auth`
|
|
||||||
- useLogin
|
|
||||||
- useLogout
|
|
||||||
- useSignup
|
|
||||||
- `cart`
|
|
||||||
|
|
||||||
- useCart
|
|
||||||
- useAddItem
|
|
||||||
- useRemoveItem
|
|
||||||
- useCartActions
|
|
||||||
- useUpdateItem
|
|
||||||
|
|
||||||
- `config.json`
|
|
||||||
- README.md
|
|
||||||
|
|
||||||
#### Example of correct usage of Commece Framework
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { useUI } from '@components/ui'
|
|
||||||
import { useCustomer } from '@framework/customer'
|
|
||||||
import { useAddItem, useWishlist, useRemoveItem } from '@framework/wishlist'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Config
|
And check that the `tsconfig.json` resolves to the chosen provider:
|
||||||
|
|
||||||
|
```
|
||||||
|
"@framework": ["framework/shopify"],
|
||||||
|
"@framework/*": ["framework/shopify/*"]
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it!
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
In order to make the UI entirely functional, we need to specify which features certain providers do not **provide**.
|
Every provider defines the features that it supports under `framework/{provider}/commerce.config.json`
|
||||||
|
|
||||||
**Disabling wishlist:**
|
#### How to turn Features on and off
|
||||||
|
|
||||||
```
|
> NOTE: The selected provider should support the feature that you are toggling. (This means that you can't turn wishlist on if the provider doesn't support this functionality out the box)
|
||||||
{
|
|
||||||
|
- Open `commerce.config.json`
|
||||||
|
- You'll see a config file like this:
|
||||||
|
```json
|
||||||
|
{
|
||||||
"features": {
|
"features": {
|
||||||
"wishlist": false
|
"wishlist": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
- Turn wishlist on by setting wishlist to true.
|
||||||
|
- Run the app and the wishlist functionality should be back on.
|
||||||
|
|
||||||
|
### How to create a new provider
|
||||||
|
|
||||||
|
Follow our docs for [Adding a new Commerce Provider](framework/commerce/new-provider.md).
|
||||||
|
|
||||||
|
If you succeeded building a provider, submit a PR with a valid demo and we'll review it asap.
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
@ -106,11 +93,15 @@ Our commitment to Open Source can be found [here](https://vercel.com/oss).
|
|||||||
2. Create a new branch `git checkout -b MY_BRANCH_NAME`
|
2. Create a new branch `git checkout -b MY_BRANCH_NAME`
|
||||||
3. Install yarn: `npm install -g yarn`
|
3. Install yarn: `npm install -g yarn`
|
||||||
4. Install the dependencies: `yarn`
|
4. Install the dependencies: `yarn`
|
||||||
5. Duplicate `.env.template` and rename it to `.env.local`.
|
5. Duplicate `.env.template` and rename it to `.env.local`
|
||||||
6. Add proper store values to `.env.local`.
|
6. Add proper store values to `.env.local`
|
||||||
7. Run `yarn dev` to build and watch for code changes
|
7. Run `yarn dev` to build and watch for code changes
|
||||||
8. The development branch is `canary` (this is the branch pull requests should be made against).
|
|
||||||
On a release, `canary` branch is rebased into `master`.
|
## Work in progress
|
||||||
|
|
||||||
|
We're using Github Projects to keep track of issues in progress and todo's. Here is our [Board](https://github.com/vercel/commerce/projects/1)
|
||||||
|
|
||||||
|
People actively working on this project: @okbel & @lfades.
|
||||||
|
|
||||||
## Troubleshoot
|
## Troubleshoot
|
||||||
|
|
||||||
@ -128,6 +119,7 @@ BIGCOMMERCE_STOREFRONT_API_TOKEN=<>
|
|||||||
BIGCOMMERCE_STORE_API_URL=<>
|
BIGCOMMERCE_STORE_API_URL=<>
|
||||||
BIGCOMMERCE_STORE_API_TOKEN=<>
|
BIGCOMMERCE_STORE_API_TOKEN=<>
|
||||||
BIGCOMMERCE_STORE_API_CLIENT_ID=<>
|
BIGCOMMERCE_STORE_API_CLIENT_ID=<>
|
||||||
|
BIGCOMMERCE_CHANNEL_ID=<>
|
||||||
```
|
```
|
||||||
|
|
||||||
If your project was started with a "Deploy with Vercel" button, you can use Vercel's CLI to retrieve these credentials.
|
If your project was started with a "Deploy with Vercel" button, you can use Vercel's CLI to retrieve these credentials.
|
||||||
|
@ -33,7 +33,7 @@ const CartItem = ({
|
|||||||
currencyCode,
|
currencyCode,
|
||||||
})
|
})
|
||||||
|
|
||||||
const updateItem = useUpdateItem(item)
|
const updateItem = useUpdateItem({ item })
|
||||||
const removeItem = useRemoveItem()
|
const removeItem = useRemoveItem()
|
||||||
const [quantity, setQuantity] = useState(item.quantity)
|
const [quantity, setQuantity] = useState(item.quantity)
|
||||||
const [removing, setRemoving] = useState(false)
|
const [removing, setRemoving] = useState(false)
|
||||||
@ -92,8 +92,10 @@ const CartItem = ({
|
|||||||
})}
|
})}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<div className="w-16 h-16 bg-violet relative overflow-hidden">
|
<div className="w-16 h-16 bg-violet relative overflow-hidden cursor-pointer">
|
||||||
|
<Link href={`/product/${item.path}`}>
|
||||||
<Image
|
<Image
|
||||||
|
onClick={() => closeSidebarIfPresent()}
|
||||||
className={s.productImage}
|
className={s.productImage}
|
||||||
width={150}
|
width={150}
|
||||||
height={150}
|
height={150}
|
||||||
@ -101,6 +103,7 @@ const CartItem = ({
|
|||||||
alt={item.variant.image!.altText}
|
alt={item.variant.image!.altText}
|
||||||
unoptimized
|
unoptimized
|
||||||
/>
|
/>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex flex-col text-base">
|
<div className="flex-1 flex flex-col text-base">
|
||||||
<Link href={`/product/${item.path}`}>
|
<Link href={`/product/${item.path}`}>
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import { UserNav } from '@components/common'
|
import Link from 'next/link'
|
||||||
import { Button } from '@components/ui'
|
|
||||||
import { Bag, Cross, Check } from '@components/icons'
|
|
||||||
import { useUI } from '@components/ui/context'
|
|
||||||
import useCart from '@framework/cart/use-cart'
|
|
||||||
import usePrice from '@framework/product/use-price'
|
|
||||||
import CartItem from '../CartItem'
|
import CartItem from '../CartItem'
|
||||||
import s from './CartSidebarView.module.css'
|
import s from './CartSidebarView.module.css'
|
||||||
|
import { Button } from '@components/ui'
|
||||||
|
import { UserNav } from '@components/common'
|
||||||
|
import { useUI } from '@components/ui/context'
|
||||||
|
import { Bag, Cross, Check } from '@components/icons'
|
||||||
|
import useCart from '@framework/cart/use-cart'
|
||||||
|
import usePrice from '@framework/product/use-price'
|
||||||
|
|
||||||
const CartSidebarView: FC<{ wishlist?: boolean }> = ({ wishlist }) => {
|
const CartSidebarView: FC = () => {
|
||||||
const { closeSidebar } = useUI()
|
const { closeSidebar } = useUI()
|
||||||
const { data, isLoading, isEmpty } = useCart()
|
const { data, isLoading, isEmpty } = useCart()
|
||||||
|
|
||||||
@ -48,7 +49,7 @@ const CartSidebarView: FC<{ wishlist?: boolean }> = ({ wishlist }) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<UserNav wishlist={wishlist} />
|
<UserNav />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@ -87,11 +88,16 @@ const CartSidebarView: FC<{ wishlist?: boolean }> = ({ wishlist }) => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="px-4 sm:px-6 flex-1">
|
<div className="px-4 sm:px-6 flex-1">
|
||||||
<h2 className="pt-1 pb-4 text-2xl leading-7 font-bold text-base tracking-wide">
|
<Link href="/cart">
|
||||||
|
<h2
|
||||||
|
className="pt-1 pb-4 text-2xl leading-7 font-bold text-base tracking-wide cursor-pointer inline-block"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
My Cart
|
My Cart
|
||||||
</h2>
|
</h2>
|
||||||
|
</Link>
|
||||||
<ul className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-accents-3 border-t border-accents-3">
|
<ul className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-accents-3 border-t border-accents-3">
|
||||||
{data!.lineItems.map((item) => (
|
{data!.lineItems.map((item: any) => (
|
||||||
<CartItem
|
<CartItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply text-center p-6 bg-primary text-sm flex-row justify-center items-center font-medium fixed bottom-0 w-full z-30 transition-all duration-300 ease-out;
|
@apply text-center p-6 bg-primary text-sm flex-row justify-center items-center font-medium fixed bottom-0 w-full z-30 transition-all duration-300 ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
@screen md {
|
@screen md {
|
||||||
|
.root {
|
||||||
@apply flex text-left;
|
@apply flex text-left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,20 +44,6 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
|||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className="py-3 md:py-0 md:pb-4">
|
|
||||||
<Link href="/">
|
|
||||||
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
|
|
||||||
Careers
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li className="py-3 md:py-0 md:pb-4">
|
|
||||||
<Link href="/blog">
|
|
||||||
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
|
|
||||||
Blog
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
{sitePages.map((page) => (
|
{sitePages.map((page) => (
|
||||||
<li key={page.url} className="py-3 md:py-0 md:pb-4">
|
<li key={page.url} className="py-3 md:py-0 md:pb-4">
|
||||||
<Link href={page.url!}>
|
<Link href={page.url!}>
|
||||||
|
@ -5,20 +5,17 @@ import { Grid } from '@components/ui'
|
|||||||
import { ProductCard } from '@components/product'
|
import { ProductCard } from '@components/product'
|
||||||
import s from './HomeAllProductsGrid.module.css'
|
import s from './HomeAllProductsGrid.module.css'
|
||||||
import { getCategoryPath, getDesignerPath } from '@lib/search'
|
import { getCategoryPath, getDesignerPath } from '@lib/search'
|
||||||
import wishlist from '@framework/api/wishlist'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
categories?: any
|
categories?: any
|
||||||
brands?: any
|
brands?: any
|
||||||
products?: Product[]
|
products?: Product[]
|
||||||
wishlist?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const HomeAllProductsGrid: FC<Props> = ({
|
const HomeAllProductsGrid: FC<Props> = ({
|
||||||
categories,
|
categories,
|
||||||
brands,
|
brands,
|
||||||
products = [],
|
products = [],
|
||||||
wishlist = false,
|
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={s.root}>
|
<div className={s.root}>
|
||||||
@ -65,7 +62,6 @@ const HomeAllProductsGrid: FC<Props> = ({
|
|||||||
width: 480,
|
width: 480,
|
||||||
height: 480,
|
height: 480,
|
||||||
}}
|
}}
|
||||||
wishlist={wishlist}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -16,14 +16,16 @@
|
|||||||
|
|
||||||
.dropdownMenu {
|
.dropdownMenu {
|
||||||
@apply fixed right-0 top-12 mt-2 origin-top-right outline-none bg-primary z-40 w-full h-full;
|
@apply fixed right-0 top-12 mt-2 origin-top-right outline-none bg-primary z-40 w-full h-full;
|
||||||
|
}
|
||||||
|
|
||||||
@screen lg {
|
@screen lg {
|
||||||
|
.dropdownMenu {
|
||||||
@apply absolute border border-accents-1 shadow-lg w-56 h-auto;
|
@apply absolute border border-accents-1 shadow-lg w-56 h-auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.closeButton {
|
@screen md {
|
||||||
@screen md {
|
.closeButton {
|
||||||
@apply hidden;
|
@apply hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,11 +58,10 @@ const Layout: FC<Props> = ({
|
|||||||
} = useUI()
|
} = useUI()
|
||||||
const { acceptedCookies, onAcceptCookies } = useAcceptCookies()
|
const { acceptedCookies, onAcceptCookies } = useAcceptCookies()
|
||||||
const { locale = 'en-US' } = useRouter()
|
const { locale = 'en-US' } = useRouter()
|
||||||
const isWishlistEnabled = commerceFeatures.wishlist
|
|
||||||
return (
|
return (
|
||||||
<CommerceProvider locale={locale}>
|
<CommerceProvider locale={locale}>
|
||||||
<div className={cn(s.root)}>
|
<div className={cn(s.root)}>
|
||||||
<Navbar wishlist={isWishlistEnabled} />
|
<Navbar />
|
||||||
<main className="fit">{children}</main>
|
<main className="fit">{children}</main>
|
||||||
<Footer pages={pageProps.pages} />
|
<Footer pages={pageProps.pages} />
|
||||||
|
|
||||||
@ -73,7 +72,7 @@ const Layout: FC<Props> = ({
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Sidebar open={displaySidebar} onClose={closeSidebar}>
|
<Sidebar open={displaySidebar} onClose={closeSidebar}>
|
||||||
<CartSidebarView wishlist={isWishlistEnabled} />
|
<CartSidebarView />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|
||||||
<FeatureBar
|
<FeatureBar
|
||||||
|
@ -5,7 +5,7 @@ import { Searchbar, UserNav } from '@components/common'
|
|||||||
import NavbarRoot from './NavbarRoot'
|
import NavbarRoot from './NavbarRoot'
|
||||||
import s from './Navbar.module.css'
|
import s from './Navbar.module.css'
|
||||||
|
|
||||||
const Navbar: FC<{ wishlist?: boolean }> = ({ wishlist }) => (
|
const Navbar: FC = () => (
|
||||||
<NavbarRoot>
|
<NavbarRoot>
|
||||||
<Container>
|
<Container>
|
||||||
<div className="relative flex flex-row justify-between py-4 align-center md:py-6">
|
<div className="relative flex flex-row justify-between py-4 align-center md:py-6">
|
||||||
@ -25,6 +25,9 @@ const Navbar: FC<{ wishlist?: boolean }> = ({ wishlist }) => (
|
|||||||
<Link href="/search?q=accessories">
|
<Link href="/search?q=accessories">
|
||||||
<a className={s.link}>Accessories</a>
|
<a className={s.link}>Accessories</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link href="/search?q=shoes">
|
||||||
|
<a className={s.link}>Shoes</a>
|
||||||
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -33,7 +36,7 @@ const Navbar: FC<{ wishlist?: boolean }> = ({ wishlist }) => (
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end flex-1 space-x-8">
|
<div className="flex justify-end flex-1 space-x-8">
|
||||||
<UserNav wishlist={wishlist} />
|
<UserNav />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
.dropdownMenu {
|
.dropdownMenu {
|
||||||
@apply fixed right-0 mt-2 origin-top-right outline-none bg-primary z-40 w-full h-full;
|
@apply fixed right-0 mt-2 origin-top-right outline-none bg-primary z-40 w-full h-full;
|
||||||
|
}
|
||||||
|
|
||||||
@screen lg {
|
@screen lg {
|
||||||
|
.dropdownMenu {
|
||||||
@apply absolute top-10 border border-accents-1 shadow-lg w-56 h-auto;
|
@apply absolute top-10 border border-accents-1 shadow-lg w-56 h-auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,20 +4,19 @@ import cn from 'classnames'
|
|||||||
import type { LineItem } from '@framework/types'
|
import type { LineItem } from '@framework/types'
|
||||||
import useCart from '@framework/cart/use-cart'
|
import useCart from '@framework/cart/use-cart'
|
||||||
import useCustomer from '@framework/customer/use-customer'
|
import useCustomer from '@framework/customer/use-customer'
|
||||||
|
import { Avatar } from '@components/common'
|
||||||
import { Heart, Bag } from '@components/icons'
|
import { Heart, Bag } from '@components/icons'
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
import DropdownMenu from './DropdownMenu'
|
import DropdownMenu from './DropdownMenu'
|
||||||
import s from './UserNav.module.css'
|
import s from './UserNav.module.css'
|
||||||
import { Avatar } from '@components/common'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
wishlist?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const countItem = (count: number, item: LineItem) => count + item.quantity
|
const countItem = (count: number, item: LineItem) => count + item.quantity
|
||||||
|
|
||||||
const UserNav: FC<Props> = ({ className, wishlist = false }) => {
|
const UserNav: FC<Props> = ({ className }) => {
|
||||||
const { data } = useCart()
|
const { data } = useCart()
|
||||||
const { data: customer } = useCustomer()
|
const { data: customer } = useCustomer()
|
||||||
const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI()
|
const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI()
|
||||||
@ -31,7 +30,7 @@ const UserNav: FC<Props> = ({ className, wishlist = false }) => {
|
|||||||
<Bag />
|
<Bag />
|
||||||
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
|
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
|
||||||
</li>
|
</li>
|
||||||
{wishlist && (
|
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
||||||
<li className={s.item}>
|
<li className={s.item}>
|
||||||
<Link href="/wishlist">
|
<Link href="/wishlist">
|
||||||
<a onClick={closeSidebarIfPresent} aria-label="Wishlist">
|
<a onClick={closeSidebarIfPresent} aria-label="Wishlist">
|
||||||
|
20
components/icons/CreditCard.tsx
Normal file
20
components/icons/CreditCard.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
const CreditCard = ({ ...props }) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
fill="none"
|
||||||
|
shapeRendering="geometricPrecision"
|
||||||
|
>
|
||||||
|
<rect x="1" y="4" width="22" height="16" rx="2" ry="2" />
|
||||||
|
<path d="M1 10h22" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreditCard
|
20
components/icons/MapPin.tsx
Normal file
20
components/icons/MapPin.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
const MapPin = ({ ...props }) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
fill="none"
|
||||||
|
shapeRendering="geometricPrecision"
|
||||||
|
>
|
||||||
|
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z" />
|
||||||
|
<circle cx="12" cy="10" r="3" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MapPin
|
@ -14,3 +14,5 @@ export { default as RightArrow } from './RightArrow'
|
|||||||
export { default as Info } from './Info'
|
export { default as Info } from './Info'
|
||||||
export { default as ChevronUp } from './ChevronUp'
|
export { default as ChevronUp } from './ChevronUp'
|
||||||
export { default as Vercel } from './Vercel'
|
export { default as Vercel } from './Vercel'
|
||||||
|
export { default as MapPin } from './MapPin'
|
||||||
|
export { default as CreditCard } from './CreditCard'
|
||||||
|
@ -11,7 +11,6 @@ interface Props {
|
|||||||
product: Product
|
product: Product
|
||||||
variant?: 'slim' | 'simple'
|
variant?: 'slim' | 'simple'
|
||||||
imgProps?: Omit<ImageProps, 'src'>
|
imgProps?: Omit<ImageProps, 'src'>
|
||||||
wishlist?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const placeholderImg = '/product-img-placeholder.svg'
|
const placeholderImg = '/product-img-placeholder.svg'
|
||||||
@ -21,7 +20,6 @@ const ProductCard: FC<Props> = ({
|
|||||||
product,
|
product,
|
||||||
variant,
|
variant,
|
||||||
imgProps,
|
imgProps,
|
||||||
wishlist = false,
|
|
||||||
...props
|
...props
|
||||||
}) => (
|
}) => (
|
||||||
<Link href={`/product/${product.slug}`} {...props}>
|
<Link href={`/product/${product.slug}`} {...props}>
|
||||||
@ -59,11 +57,11 @@ const ProductCard: FC<Props> = ({
|
|||||||
{product.price.currencyCode}
|
{product.price.currencyCode}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{wishlist && (
|
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
||||||
<WishlistButton
|
<WishlistButton
|
||||||
className={s.wishlistButton}
|
className={s.wishlistButton}
|
||||||
productId={product.id}
|
productId={product.id}
|
||||||
variant={product.variants[0]}
|
variant={product.variants[0] as any}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.productDisplay {
|
.productDisplay {
|
||||||
@apply relative flex px-0 pb-0 relative box-border col-span-1 bg-violet;
|
@apply relative flex px-0 pb-0 box-border col-span-1 bg-violet;
|
||||||
min-height: 600px;
|
min-height: 600px;
|
||||||
|
|
||||||
@screen md {
|
@screen md {
|
||||||
|
@ -1,28 +1,23 @@
|
|||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { NextSeo } from 'next-seo'
|
import { NextSeo } from 'next-seo'
|
||||||
import { FC, useState } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
import s from './ProductView.module.css'
|
import s from './ProductView.module.css'
|
||||||
|
|
||||||
import { useUI } from '@components/ui'
|
|
||||||
import { Swatch, ProductSlider } from '@components/product'
|
import { Swatch, ProductSlider } from '@components/product'
|
||||||
import { Button, Container, Text } from '@components/ui'
|
import { Button, Container, Text, useUI } from '@components/ui'
|
||||||
|
|
||||||
import type { Product } from '@commerce/types'
|
import type { Product } from '@commerce/types'
|
||||||
import usePrice from '@framework/product/use-price'
|
import usePrice from '@framework/product/use-price'
|
||||||
import { useAddItem } from '@framework/cart'
|
import { useAddItem } from '@framework/cart'
|
||||||
|
|
||||||
import { getVariant, SelectedOptions } from '../helpers'
|
import { getVariant, SelectedOptions } from '../helpers'
|
||||||
import WishlistButton from '@components/wishlist/WishlistButton'
|
import WishlistButton from '@components/wishlist/WishlistButton'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
|
||||||
children?: any
|
children?: any
|
||||||
product: Product
|
product: Product
|
||||||
wishlist?: boolean
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductView: FC<Props> = ({ product, wishlist = false }) => {
|
const ProductView: FC<Props> = ({ product }) => {
|
||||||
const addItem = useAddItem()
|
const addItem = useAddItem()
|
||||||
const { price } = usePrice({
|
const { price } = usePrice({
|
||||||
amount: product.price.value,
|
amount: product.price.value,
|
||||||
@ -31,12 +26,18 @@ const ProductView: FC<Props> = ({ product, wishlist = false }) => {
|
|||||||
})
|
})
|
||||||
const { openSidebar } = useUI()
|
const { openSidebar } = useUI()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [choices, setChoices] = useState<SelectedOptions>({
|
const [choices, setChoices] = useState<SelectedOptions>({})
|
||||||
size: null,
|
|
||||||
color: null,
|
useEffect(() => {
|
||||||
})
|
// Selects the default option
|
||||||
|
product.variants[0].options?.forEach((v) => {
|
||||||
|
setChoices((choices) => ({
|
||||||
|
...choices,
|
||||||
|
[v.displayName.toLowerCase()]: v.values[0].label.toLowerCase(),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Select the correct variant based on choices
|
|
||||||
const variant = getVariant(product, choices)
|
const variant = getVariant(product, choices)
|
||||||
|
|
||||||
const addToCart = async () => {
|
const addToCart = async () => {
|
||||||
@ -101,7 +102,6 @@ const ProductView: FC<Props> = ({ product, wishlist = false }) => {
|
|||||||
</ProductSlider>
|
</ProductSlider>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={s.sidebar}>
|
<div className={s.sidebar}>
|
||||||
<section>
|
<section>
|
||||||
{product.options?.map((opt) => (
|
{product.options?.map((opt) => (
|
||||||
@ -136,7 +136,7 @@ const ProductView: FC<Props> = ({ product, wishlist = false }) => {
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="pb-14 break-words w-full max-w-xl">
|
<div className="pb-14 break-words w-full max-w-xl">
|
||||||
<Text html={product.description} />
|
<Text html={product.descriptionHtml || product.description} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div>
|
<div>
|
||||||
@ -146,17 +146,16 @@ const ProductView: FC<Props> = ({ product, wishlist = false }) => {
|
|||||||
className={s.button}
|
className={s.button}
|
||||||
onClick={addToCart}
|
onClick={addToCart}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={!variant && product.options.length > 0}
|
|
||||||
>
|
>
|
||||||
Add to Cart
|
Add to Cart
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{wishlist && (
|
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
||||||
<WishlistButton
|
<WishlistButton
|
||||||
className={s.wishlistButton}
|
className={s.wishlistButton}
|
||||||
productId={product.id}
|
productId={product.id}
|
||||||
variant={product.variants[0]!}
|
variant={product.variants[0]! as any}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,32 +1,52 @@
|
|||||||
.root {
|
.swatch {
|
||||||
|
box-sizing: border-box;
|
||||||
|
composes: root from 'components/ui/Button/Button.module.css';
|
||||||
@apply h-12 w-12 bg-primary text-primary rounded-full mr-3 inline-flex
|
@apply h-12 w-12 bg-primary text-primary rounded-full mr-3 inline-flex
|
||||||
items-center justify-center cursor-pointer transition duration-150 ease-in-out
|
items-center justify-center cursor-pointer transition duration-150 ease-in-out
|
||||||
p-0 shadow-none border-gray-200 border box-border;
|
p-0 shadow-none border-gray-200 border box-border;
|
||||||
|
margin-right: calc(0.75rem - 1px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
& > span {
|
.swatch::before,
|
||||||
@apply absolute;
|
.swatch::after {
|
||||||
}
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
.swatch:hover {
|
||||||
@apply transform scale-110 bg-hover;
|
@apply transform scale-110 bg-hover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.swatch > span {
|
||||||
|
@apply absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color {
|
.color {
|
||||||
@apply text-black transition duration-150 ease-in-out;
|
@apply text-black transition duration-150 ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
.color :hover {
|
||||||
@apply text-black;
|
@apply text-black;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.dark,
|
.color.dark,
|
||||||
&.dark:hover {
|
.color.dark:hover {
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.active {
|
.active {
|
||||||
&.size {
|
|
||||||
@apply border-accents-9 border-2;
|
@apply border-accents-9 border-2;
|
||||||
}
|
padding-right: 1px;
|
||||||
|
padding-left: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textLabel {
|
||||||
|
@apply w-auto px-4;
|
||||||
|
min-width: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active.textLabel {
|
||||||
|
@apply border-accents-9 border-2;
|
||||||
|
padding-right: calc(1rem - 1px);
|
||||||
|
padding-left: calc(1rem - 1px);
|
||||||
}
|
}
|
@ -4,50 +4,55 @@ import s from './Swatch.module.css'
|
|||||||
import { Check } from '@components/icons'
|
import { Check } from '@components/icons'
|
||||||
import Button, { ButtonProps } from '@components/ui/Button'
|
import Button, { ButtonProps } from '@components/ui/Button'
|
||||||
import { isDark } from '@lib/colors'
|
import { isDark } from '@lib/colors'
|
||||||
interface Props {
|
interface SwatchProps {
|
||||||
active?: boolean
|
active?: boolean
|
||||||
children?: any
|
children?: any
|
||||||
className?: string
|
className?: string
|
||||||
label?: string
|
|
||||||
variant?: 'size' | 'color' | string
|
variant?: 'size' | 'color' | string
|
||||||
color?: string
|
color?: string
|
||||||
|
label?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const Swatch: FC<Omit<ButtonProps, 'variant'> & Props> = ({
|
const Swatch: FC<Omit<ButtonProps, 'variant'> & SwatchProps> = ({
|
||||||
className,
|
className,
|
||||||
color = '',
|
color = '',
|
||||||
label,
|
label = null,
|
||||||
variant = 'size',
|
variant = 'size',
|
||||||
active,
|
active,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
variant = variant?.toLowerCase()
|
variant = variant?.toLowerCase()
|
||||||
label = label?.toLowerCase()
|
|
||||||
|
|
||||||
const rootClassName = cn(
|
if (label) {
|
||||||
s.root,
|
label = label?.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
const swatchClassName = cn(
|
||||||
|
s.swatch,
|
||||||
{
|
{
|
||||||
[s.active]: active,
|
[s.active]: active,
|
||||||
[s.size]: variant === 'size',
|
[s.size]: variant === 'size',
|
||||||
[s.color]: color,
|
[s.color]: color,
|
||||||
[s.dark]: color ? isDark(color) : false,
|
[s.dark]: color ? isDark(color) : false,
|
||||||
|
[s.textLabel]: !color && label && label.length > 3,
|
||||||
},
|
},
|
||||||
className
|
className
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={rootClassName}
|
className={swatchClassName}
|
||||||
style={color ? { backgroundColor: color } : {}}
|
style={color ? { backgroundColor: color } : {}}
|
||||||
aria-label="Variant Swatch"
|
aria-label="Variant Swatch"
|
||||||
|
{...(label && color && { title: label })}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{variant === 'color' && active && (
|
{variant === 'color' && color && active && (
|
||||||
<span>
|
<span>
|
||||||
<Check />
|
<Check />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{variant === 'size' ? label : null}
|
{variant !== 'color' || !color ? label : null}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
import type { Product } from '@commerce/types'
|
import type { Product } from '@commerce/types'
|
||||||
|
export type SelectedOptions = Record<string, string | null>
|
||||||
export type SelectedOptions = {
|
|
||||||
size: string | null
|
|
||||||
color: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getVariant(product: Product, opts: SelectedOptions) {
|
export function getVariant(product: Product, opts: SelectedOptions) {
|
||||||
const variant = product.variants.find((variant) => {
|
const variant = product.variants.find((variant) => {
|
||||||
|
@ -3,10 +3,6 @@
|
|||||||
@apply grid grid-cols-1 gap-0;
|
@apply grid grid-cols-1 gap-0;
|
||||||
min-height: var(--row-height);
|
min-height: var(--row-height);
|
||||||
|
|
||||||
@screen lg {
|
|
||||||
@apply grid-cols-3 grid-rows-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
@apply row-span-1 bg-transparent box-border overflow-hidden;
|
@apply row-span-1 bg-transparent box-border overflow-hidden;
|
||||||
height: 500px;
|
height: 500px;
|
||||||
@ -19,6 +15,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@screen lg {
|
||||||
|
.root {
|
||||||
|
@apply grid-cols-3 grid-rows-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root & > * {
|
||||||
|
@apply col-span-1;
|
||||||
|
height: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.default {
|
.default {
|
||||||
& > * {
|
& > * {
|
||||||
@apply bg-transparent;
|
@apply bg-transparent;
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply mx-auto grid grid-cols-1 py-32 gap-4;
|
@apply mx-auto grid grid-cols-1 py-32 gap-4;
|
||||||
@screen md {
|
}
|
||||||
|
|
||||||
|
@screen md {
|
||||||
|
.root {
|
||||||
@apply grid-cols-2;
|
@apply grid-cols-2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ const Hero: FC<Props> = ({ headline, description }) => {
|
|||||||
<p className="mt-5 text-xl leading-7 text-accent-2 text-white">
|
<p className="mt-5 text-xl leading-7 text-accent-2 text-white">
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
<Link href="/blog">
|
<Link href="/">
|
||||||
<a className="text-white pt-3 font-bold hover:underline flex flex-row cursor-pointer w-max-content">
|
<a className="text-white pt-3 font-bold hover:underline flex flex-row cursor-pointer w-max-content">
|
||||||
Read it here
|
Read it here
|
||||||
<RightArrow width="20" heigh="20" className="ml-1" />
|
<RightArrow width="20" heigh="20" className="ml-1" />
|
||||||
|
@ -8,6 +8,7 @@ export interface State {
|
|||||||
displayToast: boolean
|
displayToast: boolean
|
||||||
modalView: string
|
modalView: string
|
||||||
toastText: string
|
toastText: string
|
||||||
|
userAvatar: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
@ -17,6 +18,7 @@ const initialState = {
|
|||||||
modalView: 'LOGIN_VIEW',
|
modalView: 'LOGIN_VIEW',
|
||||||
displayToast: false,
|
displayToast: false,
|
||||||
toastText: '',
|
toastText: '',
|
||||||
|
userAvatar: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
@ -57,7 +59,12 @@ type Action =
|
|||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type MODAL_VIEWS = 'SIGNUP_VIEW' | 'LOGIN_VIEW' | 'FORGOT_VIEW'
|
type MODAL_VIEWS =
|
||||||
|
| 'SIGNUP_VIEW'
|
||||||
|
| 'LOGIN_VIEW'
|
||||||
|
| 'FORGOT_VIEW'
|
||||||
|
| 'NEW_SHIPPING_ADDRESS'
|
||||||
|
| 'NEW_PAYMENT_METHOD'
|
||||||
type ToastText = string
|
type ToastText = string
|
||||||
|
|
||||||
export const UIContext = React.createContext<State | any>(initialState)
|
export const UIContext = React.createContext<State | any>(initialState)
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import React, { FC, useState } from 'react'
|
import React, { FC, useState } from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import { Heart } from '@components/icons'
|
|
||||||
|
|
||||||
import { useUI } from '@components/ui'
|
import { useUI } from '@components/ui'
|
||||||
import type { Product, ProductVariant } from '@commerce/types'
|
import { Heart } from '@components/icons'
|
||||||
import useCustomer from '@framework/customer/use-customer'
|
|
||||||
import useAddItem from '@framework/wishlist/use-add-item'
|
import useAddItem from '@framework/wishlist/use-add-item'
|
||||||
|
import useCustomer from '@framework/customer/use-customer'
|
||||||
|
import useWishlist from '@framework/wishlist/use-wishlist'
|
||||||
import useRemoveItem from '@framework/wishlist/use-remove-item'
|
import useRemoveItem from '@framework/wishlist/use-remove-item'
|
||||||
import useWishlist from '@framework/wishlist/use-add-item'
|
import type { Product, ProductVariant } from '@commerce/types'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
productId: Product['id']
|
productId: Product['id']
|
||||||
@ -27,8 +26,12 @@ const WishlistButton: FC<Props> = ({
|
|||||||
const { openModal, setModalView } = useUI()
|
const { openModal, setModalView } = useUI()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
// @ts-ignore Wishlist is not always enabled
|
||||||
const itemInWishlist = data?.items?.find(
|
const itemInWishlist = data?.items?.find(
|
||||||
(item) => item.product_id === productId && item.variant_id === variant.id
|
// @ts-ignore Wishlist is not always enabled
|
||||||
|
(item) =>
|
||||||
|
item.product_id === Number(productId) &&
|
||||||
|
(item.variant_id as any) === Number(variant.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleWishlistChange = async (e: any) => {
|
const handleWishlistChange = async (e: any) => {
|
||||||
|
@ -22,7 +22,8 @@ const WishlistCard: FC<Props> = ({ product }) => {
|
|||||||
baseAmount: product.prices?.retailPrice?.value,
|
baseAmount: product.prices?.retailPrice?.value,
|
||||||
currencyCode: product.prices?.price?.currencyCode!,
|
currencyCode: product.prices?.price?.currencyCode!,
|
||||||
})
|
})
|
||||||
const removeItem = useRemoveItem({ includeProducts: true })
|
// @ts-ignore Wishlist is not always enabled
|
||||||
|
const removeItem = useRemoveItem({ wishlist: { includeProducts: true } })
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [removing, setRemoving] = useState(false)
|
const [removing, setRemoving] = useState(false)
|
||||||
const addItem = useAddItem()
|
const addItem = useAddItem()
|
||||||
|
@ -1,12 +1,22 @@
|
|||||||
{
|
{
|
||||||
"title": "ACME Storefront | Powered by Next.js Commerce",
|
"title": "ACME Storefront | Powered by Next.js Commerce",
|
||||||
"titleTemplate": "%s - ACME Storefront",
|
"titleTemplate": "%s - ACME Storefront",
|
||||||
"description": "Next.js Commerce -> https://www.nextjs.org/commerce",
|
"description": "Next.js Commerce - https://www.nextjs.org/commerce",
|
||||||
"openGraph": {
|
"openGraph": {
|
||||||
|
"title": "ACME Storefront | Powered by Next.js Commerce",
|
||||||
|
"description": "Next.js Commerce - https://www.nextjs.org/commerce",
|
||||||
"type": "website",
|
"type": "website",
|
||||||
"locale": "en_IE",
|
"locale": "en_IE",
|
||||||
"url": "https://nextjs.org/commerce",
|
"url": "https://nextjs.org/commerce",
|
||||||
"site_name": "Next.js Commerce"
|
"site_name": "Next.js Commerce",
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"url": "/card.png",
|
||||||
|
"width": 800,
|
||||||
|
"height": 600,
|
||||||
|
"alt": "Next.js Commerce"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"twitter": {
|
"twitter": {
|
||||||
"handle": "@nextjs",
|
"handle": "@nextjs",
|
||||||
|
@ -1 +0,0 @@
|
|||||||
# Roadmap
|
|
8
framework/bigcommerce/.env.template
Normal file
8
framework/bigcommerce/.env.template
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
COMMERCE_PROVIDER=bigcommerce
|
||||||
|
|
||||||
|
BIGCOMMERCE_STOREFRONT_API_URL=
|
||||||
|
BIGCOMMERCE_STOREFRONT_API_TOKEN=
|
||||||
|
BIGCOMMERCE_STORE_API_URL=
|
||||||
|
BIGCOMMERCE_STORE_API_TOKEN=
|
||||||
|
BIGCOMMERCE_STORE_API_CLIENT_ID=
|
||||||
|
BIGCOMMERCE_CHANNEL_ID=
|
@ -1,45 +1,34 @@
|
|||||||
# Table of Contents
|
# Bigcommerce Provider
|
||||||
|
|
||||||
- [BigCommerce Storefront Data Hooks](#bigcommerce-storefront-data-hooks)
|
**Demo:** https://bigcommerce.demo.vercel.store/
|
||||||
- [Installation](#installation)
|
|
||||||
- [General Usage](#general-usage)
|
|
||||||
- [CommerceProvider](#commerceprovider)
|
|
||||||
- [useLogin hook](#uselogin-hook)
|
|
||||||
- [useLogout](#uselogout)
|
|
||||||
- [useCustomer](#usecustomer)
|
|
||||||
- [useSignup](#usesignup)
|
|
||||||
- [usePrice](#useprice)
|
|
||||||
- [Cart Hooks](#cart-hooks)
|
|
||||||
- [useCart](#usecart)
|
|
||||||
- [useAddItem](#useadditem)
|
|
||||||
- [useUpdateItem](#useupdateitem)
|
|
||||||
- [useRemoveItem](#useremoveitem)
|
|
||||||
- [Wishlist Hooks](#wishlist-hooks)
|
|
||||||
- [Product Hooks and API](#product-hooks-and-api)
|
|
||||||
- [useSearch](#usesearch)
|
|
||||||
- [getAllProducts](#getallproducts)
|
|
||||||
- [getProduct](#getproduct)
|
|
||||||
- [More](#more)
|
|
||||||
|
|
||||||
# BigCommerce Storefront Data Hooks
|
With the deploy button below you'll be able to have a [BigCommerce](https://www.bigcommerce.com/) account and a store that works with this starter:
|
||||||
|
|
||||||
> This project is under active development, new features and updates will be continuously added over time
|
[](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fcommerce&project-name=commerce&repo-name=commerce&demo-title=Next.js%20Commerce&demo-description=An%20all-in-one%20starter%20kit%20for%20high-performance%20e-commerce%20sites.&demo-url=https%3A%2F%2Fdemo.vercel.store&demo-image=https%3A%2F%2Fbigcommerce-demo-asset-ksvtgfvnd.vercel.app%2Fbigcommerce.png&integration-ids=oac_MuWZiE4jtmQ2ejZQaQ7ncuDT)
|
||||||
|
|
||||||
UI hooks and data fetching methods built from the ground up for e-commerce applications written in React, that use BigCommerce as a headless e-commerce platform. The package provides:
|
If you already have a BigCommerce account and want to use your current store, then copy the `.env.template` file in this directory to `.env.local` in the main directory (which will be ignored by Git):
|
||||||
|
|
||||||
- Code splitted hooks for data fetching using [SWR](https://swr.vercel.app/), and to handle common user actions
|
```bash
|
||||||
- Code splitted data fetching methods for initial data population and static generation of content
|
cp framework/bigcommerce/.env.template .env.local
|
||||||
- Helpers to create the API endpoints that connect to the hooks, very well suited for Next.js applications
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
To install:
|
|
||||||
|
|
||||||
```
|
|
||||||
yarn add storefront-data-hooks
|
|
||||||
```
|
```
|
||||||
|
|
||||||
After install, the first thing you do is: <b>set your environment variables</b> in `.env.local`
|
Then, set the environment variables in `.env.local` to match the ones from your store.
|
||||||
|
|
||||||
|
## Contribute
|
||||||
|
|
||||||
|
Our commitment to Open Source can be found [here](https://vercel.com/oss).
|
||||||
|
|
||||||
|
If you find an issue with the provider or want a new feature, feel free to open a PR or [create a new issue](https://github.com/vercel/commerce/issues).
|
||||||
|
|
||||||
|
## Troubleshoot
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>I already own a BigCommerce store. What should I do?</summary>
|
||||||
|
<br>
|
||||||
|
First thing you do is: <b>set your environment variables</b>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
.env.local
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
BIGCOMMERCE_STOREFRONT_API_URL=<>
|
BIGCOMMERCE_STOREFRONT_API_URL=<>
|
||||||
@ -47,333 +36,24 @@ BIGCOMMERCE_STOREFRONT_API_TOKEN=<>
|
|||||||
BIGCOMMERCE_STORE_API_URL=<>
|
BIGCOMMERCE_STORE_API_URL=<>
|
||||||
BIGCOMMERCE_STORE_API_TOKEN=<>
|
BIGCOMMERCE_STORE_API_TOKEN=<>
|
||||||
BIGCOMMERCE_STORE_API_CLIENT_ID=<>
|
BIGCOMMERCE_STORE_API_CLIENT_ID=<>
|
||||||
|
BIGCOMMERCE_CHANNEL_ID=<>
|
||||||
```
|
```
|
||||||
|
|
||||||
## General Usage
|
If your project was started with a "Deploy with Vercel" button, you can use Vercel's CLI to retrieve these credentials.
|
||||||
|
|
||||||
### CommerceProvider
|
1. Install Vercel CLI: `npm i -g vercel`
|
||||||
|
2. Link local instance with Vercel and Github accounts (creates .vercel file): `vercel link`
|
||||||
|
3. Download your environment variables: `vercel env pull .env.local`
|
||||||
|
|
||||||
This component is a provider pattern component that creates commerce context for it's children. It takes config values for the locale and an optional `fetcherRef` object for data fetching.
|
Next, you're free to customize the starter. More updates coming soon. Stay tuned.
|
||||||
|
|
||||||
```jsx
|
</details>
|
||||||
...
|
|
||||||
import { CommerceProvider } from '@bigcommerce/storefront-data-hooks'
|
|
||||||
|
|
||||||
const App = ({ locale = 'en-US', children }) => {
|
<details>
|
||||||
return (
|
<summary>BigCommerce shows a Coming Soon page and requests a Preview Code</summary>
|
||||||
<CommerceProvider locale={locale}>
|
<br>
|
||||||
{children}
|
After Email confirmation, Checkout should be manually enabled through BigCommerce platform. Look for "Review & test your store" section through BigCommerce's dashboard.
|
||||||
</CommerceProvider>
|
<br>
|
||||||
)
|
<br>
|
||||||
}
|
BigCommerce team has been notified and they plan to add more detailed about this subject.
|
||||||
...
|
</details>
|
||||||
```
|
|
||||||
|
|
||||||
### useLogin hook
|
|
||||||
|
|
||||||
Hook for bigcommerce user login functionality, returns `login` function to handle user login.
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
...
|
|
||||||
import useLogin from '@bigcommerce/storefront-data-hooks/use-login'
|
|
||||||
|
|
||||||
const LoginView = () => {
|
|
||||||
const login = useLogin()
|
|
||||||
|
|
||||||
const handleLogin = async () => {
|
|
||||||
await login({
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleLogin}>
|
|
||||||
{children}
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
### useLogout
|
|
||||||
|
|
||||||
Hook to logout user.
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
...
|
|
||||||
import useLogout from '@bigcommerce/storefront-data-hooks/use-logout'
|
|
||||||
|
|
||||||
const LogoutLink = () => {
|
|
||||||
const logout = useLogout()
|
|
||||||
return (
|
|
||||||
<a onClick={() => logout()}>
|
|
||||||
Logout
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### useCustomer
|
|
||||||
|
|
||||||
Hook for getting logged in customer data, and fetching customer info.
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
...
|
|
||||||
import useCustomer from '@bigcommerce/storefront-data-hooks/use-customer'
|
|
||||||
...
|
|
||||||
|
|
||||||
const Profile = () => {
|
|
||||||
const { data } = useCustomer()
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>Hello, {data.firstName}</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### useSignup
|
|
||||||
|
|
||||||
Hook for bigcommerce user signup, returns `signup` function to handle user signups.
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
...
|
|
||||||
import useSignup from '@bigcommerce/storefront-data-hooks/use-login'
|
|
||||||
|
|
||||||
const SignupView = () => {
|
|
||||||
const signup = useSignup()
|
|
||||||
|
|
||||||
const handleSignup = async () => {
|
|
||||||
await signup({
|
|
||||||
email,
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
password,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSignup}>
|
|
||||||
{children}
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
### usePrice
|
|
||||||
|
|
||||||
Helper hook to format price according to commerce locale, and return discount if available.
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import usePrice from '@bigcommerce/storefront-data-hooks/use-price'
|
|
||||||
...
|
|
||||||
const { price, discount, basePrice } = usePrice(
|
|
||||||
data && {
|
|
||||||
amount: data.cart_amount,
|
|
||||||
currencyCode: data.currency.code,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Cart Hooks
|
|
||||||
|
|
||||||
### useCart
|
|
||||||
|
|
||||||
Returns the current cart data for use
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
...
|
|
||||||
import useCart from '@bigcommerce/storefront-data-hooks/cart/use-cart'
|
|
||||||
|
|
||||||
const countItem = (count: number, item: LineItem) => count + item.quantity
|
|
||||||
|
|
||||||
const CartNumber = () => {
|
|
||||||
const { data } = useCart()
|
|
||||||
const itemsCount = data?.lineItems.reduce(countItem, 0) ?? 0
|
|
||||||
|
|
||||||
return itemsCount > 0 ? <span>{itemsCount}</span> : null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### useAddItem
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
...
|
|
||||||
import useAddItem from '@bigcommerce/storefront-data-hooks/cart/use-add-item'
|
|
||||||
|
|
||||||
const AddToCartButton = ({ productId, variantId }) => {
|
|
||||||
const addItem = useAddItem()
|
|
||||||
|
|
||||||
const addToCart = async () => {
|
|
||||||
await addItem({
|
|
||||||
productId,
|
|
||||||
variantId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return <button onClick={addToCart}>Add To Cart</button>
|
|
||||||
}
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
### useUpdateItem
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
...
|
|
||||||
import useUpdateItem from '@bigcommerce/storefront-data-hooks/cart/use-update-item'
|
|
||||||
|
|
||||||
const CartItem = ({ item }) => {
|
|
||||||
const [quantity, setQuantity] = useState(item.quantity)
|
|
||||||
const updateItem = useUpdateItem(item)
|
|
||||||
|
|
||||||
const updateQuantity = async (e) => {
|
|
||||||
const val = e.target.value
|
|
||||||
await updateItem({ quantity: val })
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
max={99}
|
|
||||||
min={0}
|
|
||||||
value={quantity}
|
|
||||||
onChange={updateQuantity}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
### useRemoveItem
|
|
||||||
|
|
||||||
Provided with a cartItemId, will remove an item from the cart:
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
...
|
|
||||||
import useRemoveItem from '@bigcommerce/storefront-data-hooks/cart/use-remove-item'
|
|
||||||
|
|
||||||
const RemoveButton = ({ item }) => {
|
|
||||||
const removeItem = useRemoveItem()
|
|
||||||
|
|
||||||
const handleRemove = async () => {
|
|
||||||
await removeItem({ id: item.id })
|
|
||||||
}
|
|
||||||
|
|
||||||
return <button onClick={handleRemove}>Remove</button>
|
|
||||||
}
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Wishlist Hooks
|
|
||||||
|
|
||||||
Wishlist hooks are similar to cart hooks. See the below example for how to use `useWishlist`, `useAddItem`, and `useRemoveItem`.
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import useAddItem from '@bigcommerce/storefront-data-hooks/wishlist/use-add-item'
|
|
||||||
import useRemoveItem from '@bigcommerce/storefront-data-hooks/wishlist/use-remove-item'
|
|
||||||
import useWishlist from '@bigcommerce/storefront-data-hooks/wishlist/use-wishlist'
|
|
||||||
|
|
||||||
const WishlistButton = ({ productId, variant }) => {
|
|
||||||
const addItem = useAddItem()
|
|
||||||
const removeItem = useRemoveItem()
|
|
||||||
const { data } = useWishlist()
|
|
||||||
const { data: customer } = useCustomer()
|
|
||||||
const itemInWishlist = data?.items?.find(
|
|
||||||
(item) =>
|
|
||||||
item.product_id === productId &&
|
|
||||||
item.variant_id === variant?.node.entityId
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleWishlistChange = async (e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
if (!customer) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (itemInWishlist) {
|
|
||||||
await removeItem({ id: itemInWishlist.id! })
|
|
||||||
} else {
|
|
||||||
await addItem({
|
|
||||||
productId,
|
|
||||||
variantId: variant?.node.entityId!,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button onClick={handleWishlistChange}>
|
|
||||||
<Heart fill={itemInWishlist ? 'var(--pink)' : 'none'} />
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Product Hooks and API
|
|
||||||
|
|
||||||
### useSearch
|
|
||||||
|
|
||||||
`useSearch` handles searching the bigcommerce storefront product catalog by catalog, brand, and query string.
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
...
|
|
||||||
import useSearch from '@bigcommerce/storefront-data-hooks/products/use-search'
|
|
||||||
|
|
||||||
const SearchPage = ({ searchString, category, brand, sortStr }) => {
|
|
||||||
const { data } = useSearch({
|
|
||||||
search: searchString || '',
|
|
||||||
categoryId: category?.entityId,
|
|
||||||
brandId: brand?.entityId,
|
|
||||||
sort: sortStr || '',
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Grid layout="normal">
|
|
||||||
{data.products.map(({ node }) => (
|
|
||||||
<ProductCard key={node.path} product={node} />
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### getAllProducts
|
|
||||||
|
|
||||||
API function to retrieve a product list.
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { getConfig } from '@bigcommerce/storefront-data-hooks/api'
|
|
||||||
import getAllProducts from '@bigcommerce/storefront-data-hooks/api/operations/get-all-products'
|
|
||||||
|
|
||||||
const { products } = await getAllProducts({
|
|
||||||
variables: { field: 'featuredProducts', first: 6 },
|
|
||||||
config,
|
|
||||||
preview,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### getProduct
|
|
||||||
|
|
||||||
API product to retrieve a single product when provided with the product
|
|
||||||
slug string.
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { getConfig } from '@bigcommerce/storefront-data-hooks/api'
|
|
||||||
import getProduct from '@bigcommerce/storefront-data-hooks/api/operations/get-product'
|
|
||||||
|
|
||||||
const { product } = await getProduct({
|
|
||||||
variables: { slug },
|
|
||||||
config,
|
|
||||||
preview,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## More
|
|
||||||
|
|
||||||
Feel free to read through the source for more usage, and check the commerce vercel demo and commerce repo for usage examples: ([demo.vercel.store](https://demo.vercel.store/)) ([repo](https://github.com/vercel/commerce))
|
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import type { ItemBody as WishlistItemBody } from '../wishlist'
|
import type { ItemBody as WishlistItemBody } from '../wishlist'
|
||||||
import type { CartItemBody, OptionSelections } from '../../types'
|
import type { CartItemBody, OptionSelections } from '../../types'
|
||||||
|
|
||||||
|
type BCWishlistItemBody = {
|
||||||
|
product_id: number
|
||||||
|
variant_id: number
|
||||||
|
}
|
||||||
|
|
||||||
type BCCartItemBody = {
|
type BCCartItemBody = {
|
||||||
product_id: number
|
product_id: number
|
||||||
variant_id: number
|
variant_id: number
|
||||||
@ -8,9 +13,11 @@ type BCCartItemBody = {
|
|||||||
option_selections?: OptionSelections
|
option_selections?: OptionSelections
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseWishlistItem = (item: WishlistItemBody) => ({
|
export const parseWishlistItem = (
|
||||||
product_id: item.productId,
|
item: WishlistItemBody
|
||||||
variant_id: item.variantId,
|
): BCWishlistItemBody => ({
|
||||||
|
product_id: Number(item.productId),
|
||||||
|
variant_id: Number(item.variantId),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const parseCartItem = (item: CartItemBody): BCCartItemBody => ({
|
export const parseCartItem = (item: CartItemBody): BCCartItemBody => ({
|
||||||
|
@ -1,22 +1,18 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import type { HookFetcher } from '@commerce/utils/types'
|
import type { MutationHook } from '@commerce/utils/types'
|
||||||
import { CommerceError } from '@commerce/utils/errors'
|
import { CommerceError } from '@commerce/utils/errors'
|
||||||
import useCommerceLogin from '@commerce/use-login'
|
import useLogin, { UseLogin } from '@commerce/auth/use-login'
|
||||||
import type { LoginBody } from '../api/customers/login'
|
import type { LoginBody } from '../api/customers/login'
|
||||||
import useCustomer from '../customer/use-customer'
|
import useCustomer from '../customer/use-customer'
|
||||||
|
|
||||||
const defaultOpts = {
|
export default useLogin as UseLogin<typeof handler>
|
||||||
|
|
||||||
|
export const handler: MutationHook<null, {}, LoginBody> = {
|
||||||
|
fetchOptions: {
|
||||||
url: '/api/bigcommerce/customers/login',
|
url: '/api/bigcommerce/customers/login',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
}
|
},
|
||||||
|
async fetcher({ input: { email, password }, options, fetch }) {
|
||||||
export type LoginInput = LoginBody
|
|
||||||
|
|
||||||
export const fetcher: HookFetcher<null, LoginBody> = (
|
|
||||||
options,
|
|
||||||
{ email, password },
|
|
||||||
fetch
|
|
||||||
) => {
|
|
||||||
if (!(email && password)) {
|
if (!(email && password)) {
|
||||||
throw new CommerceError({
|
throw new CommerceError({
|
||||||
message:
|
message:
|
||||||
@ -25,30 +21,20 @@ export const fetcher: HookFetcher<null, LoginBody> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
return fetch({
|
return fetch({
|
||||||
...defaultOpts,
|
|
||||||
...options,
|
...options,
|
||||||
body: { email, password },
|
body: { email, password },
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
|
useHook: ({ fetch }) => () => {
|
||||||
export function extendHook(customFetcher: typeof fetcher) {
|
|
||||||
const useLogin = () => {
|
|
||||||
const { revalidate } = useCustomer()
|
const { revalidate } = useCustomer()
|
||||||
const fn = useCommerceLogin<null, LoginInput>(defaultOpts, customFetcher)
|
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async function login(input: LoginInput) {
|
async function login(input) {
|
||||||
const data = await fn(input)
|
const data = await fetch({ input })
|
||||||
await revalidate()
|
await revalidate()
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
[fn]
|
[fetch, revalidate]
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
|
||||||
useLogin.extend = extendHook
|
|
||||||
|
|
||||||
return useLogin
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default extendHook(fetcher)
|
|
||||||
|
@ -1,38 +1,25 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import type { HookFetcher } from '@commerce/utils/types'
|
import type { MutationHook } from '@commerce/utils/types'
|
||||||
import useCommerceLogout from '@commerce/use-logout'
|
import useLogout, { UseLogout } from '@commerce/auth/use-logout'
|
||||||
import useCustomer from '../customer/use-customer'
|
import useCustomer from '../customer/use-customer'
|
||||||
|
|
||||||
const defaultOpts = {
|
export default useLogout as UseLogout<typeof handler>
|
||||||
|
|
||||||
|
export const handler: MutationHook<null> = {
|
||||||
|
fetchOptions: {
|
||||||
url: '/api/bigcommerce/customers/logout',
|
url: '/api/bigcommerce/customers/logout',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
}
|
},
|
||||||
|
useHook: ({ fetch }) => () => {
|
||||||
export const fetcher: HookFetcher<null> = (options, _, fetch) => {
|
|
||||||
return fetch({
|
|
||||||
...defaultOpts,
|
|
||||||
...options,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extendHook(customFetcher: typeof fetcher) {
|
|
||||||
const useLogout = () => {
|
|
||||||
const { mutate } = useCustomer()
|
const { mutate } = useCustomer()
|
||||||
const fn = useCommerceLogout<null>(defaultOpts, customFetcher)
|
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async function login() {
|
async function logout() {
|
||||||
const data = await fn(null)
|
const data = await fetch()
|
||||||
await mutate(null, false)
|
await mutate(null, false)
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
[fn]
|
[fetch, mutate]
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
|
||||||
useLogout.extend = extendHook
|
|
||||||
|
|
||||||
return useLogout
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default extendHook(fetcher)
|
|
||||||
|
@ -1,22 +1,22 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import type { HookFetcher } from '@commerce/utils/types'
|
import type { MutationHook } from '@commerce/utils/types'
|
||||||
import { CommerceError } from '@commerce/utils/errors'
|
import { CommerceError } from '@commerce/utils/errors'
|
||||||
import useCommerceSignup from '@commerce/use-signup'
|
import useSignup, { UseSignup } from '@commerce/auth/use-signup'
|
||||||
import type { SignupBody } from '../api/customers/signup'
|
import type { SignupBody } from '../api/customers/signup'
|
||||||
import useCustomer from '../customer/use-customer'
|
import useCustomer from '../customer/use-customer'
|
||||||
|
|
||||||
const defaultOpts = {
|
export default useSignup as UseSignup<typeof handler>
|
||||||
|
|
||||||
|
export const handler: MutationHook<null, {}, SignupBody, SignupBody> = {
|
||||||
|
fetchOptions: {
|
||||||
url: '/api/bigcommerce/customers/signup',
|
url: '/api/bigcommerce/customers/signup',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
}
|
},
|
||||||
|
async fetcher({
|
||||||
export type SignupInput = SignupBody
|
input: { firstName, lastName, email, password },
|
||||||
|
|
||||||
export const fetcher: HookFetcher<null, SignupBody> = (
|
|
||||||
options,
|
options,
|
||||||
{ firstName, lastName, email, password },
|
fetch,
|
||||||
fetch
|
}) {
|
||||||
) => {
|
|
||||||
if (!(firstName && lastName && email && password)) {
|
if (!(firstName && lastName && email && password)) {
|
||||||
throw new CommerceError({
|
throw new CommerceError({
|
||||||
message:
|
message:
|
||||||
@ -25,30 +25,20 @@ export const fetcher: HookFetcher<null, SignupBody> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
return fetch({
|
return fetch({
|
||||||
...defaultOpts,
|
|
||||||
...options,
|
...options,
|
||||||
body: { firstName, lastName, email, password },
|
body: { firstName, lastName, email, password },
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
|
useHook: ({ fetch }) => () => {
|
||||||
export function extendHook(customFetcher: typeof fetcher) {
|
|
||||||
const useSignup = () => {
|
|
||||||
const { revalidate } = useCustomer()
|
const { revalidate } = useCustomer()
|
||||||
const fn = useCommerceSignup<null, SignupInput>(defaultOpts, customFetcher)
|
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async function signup(input: SignupInput) {
|
async function signup(input) {
|
||||||
const data = await fn(input)
|
const data = await fetch({ input })
|
||||||
await revalidate()
|
await revalidate()
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
[fn]
|
[fetch, revalidate]
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
|
||||||
useSignup.extend = extendHook
|
|
||||||
|
|
||||||
return useSignup
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default extendHook(fetcher)
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
export { default as useCart } from './use-cart'
|
export { default as useCart } from './use-cart'
|
||||||
export { default as useAddItem } from './use-add-item'
|
export { default as useAddItem } from './use-add-item'
|
||||||
export { default as useRemoveItem } from './use-remove-item'
|
export { default as useRemoveItem } from './use-remove-item'
|
||||||
export { default as useWishlistActions } from './use-cart-actions'
|
export { default as useUpdateItem } from './use-update-item'
|
||||||
export { default as useUpdateItem } from './use-cart-actions'
|
|
||||||
|
@ -1,29 +1,24 @@
|
|||||||
import type { MutationHandler } from '@commerce/utils/types'
|
import { useCallback } from 'react'
|
||||||
|
import type { MutationHook } from '@commerce/utils/types'
|
||||||
import { CommerceError } from '@commerce/utils/errors'
|
import { CommerceError } from '@commerce/utils/errors'
|
||||||
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
|
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
|
||||||
import { normalizeCart } from '../lib/normalize'
|
import { normalizeCart } from '../lib/normalize'
|
||||||
import type {
|
import type {
|
||||||
AddCartItemBody,
|
|
||||||
Cart,
|
Cart,
|
||||||
BigcommerceCart,
|
BigcommerceCart,
|
||||||
CartItemBody,
|
CartItemBody,
|
||||||
|
AddCartItemBody,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
import useCart from './use-cart'
|
import useCart from './use-cart'
|
||||||
import { BigcommerceProvider } from '..'
|
|
||||||
|
|
||||||
const defaultOpts = {
|
export default useAddItem as UseAddItem<typeof handler>
|
||||||
url: '/api/bigcommerce/cart',
|
|
||||||
method: 'POST',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useAddItem as UseAddItem<BigcommerceProvider, CartItemBody>
|
export const handler: MutationHook<Cart, {}, CartItemBody> = {
|
||||||
|
|
||||||
export const handler: MutationHandler<Cart, {}, AddCartItemBody> = {
|
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
url: '/api/bigcommerce/cart',
|
url: '/api/bigcommerce/cart',
|
||||||
method: 'GET',
|
method: 'POST',
|
||||||
},
|
},
|
||||||
async fetcher({ input: { item }, options, fetch }) {
|
async fetcher({ input: item, options, fetch }) {
|
||||||
if (
|
if (
|
||||||
item.quantity &&
|
item.quantity &&
|
||||||
(!Number.isInteger(item.quantity) || item.quantity! < 1)
|
(!Number.isInteger(item.quantity) || item.quantity! < 1)
|
||||||
@ -34,20 +29,22 @@ export const handler: MutationHandler<Cart, {}, AddCartItemBody> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await fetch<BigcommerceCart, AddCartItemBody>({
|
const data = await fetch<BigcommerceCart, AddCartItemBody>({
|
||||||
...defaultOpts,
|
|
||||||
...options,
|
...options,
|
||||||
body: { item },
|
body: { item },
|
||||||
})
|
})
|
||||||
|
|
||||||
return normalizeCart(data)
|
return normalizeCart(data)
|
||||||
},
|
},
|
||||||
useHook() {
|
useHook: ({ fetch }) => () => {
|
||||||
const { mutate } = useCart()
|
const { mutate } = useCart()
|
||||||
|
|
||||||
return async function addItem({ input, fetch }) {
|
return useCallback(
|
||||||
|
async function addItem(input) {
|
||||||
const data = await fetch({ input })
|
const data = await fetch({ input })
|
||||||
await mutate(data, false)
|
await mutate(data, false)
|
||||||
return data
|
return data
|
||||||
}
|
},
|
||||||
|
[fetch, mutate]
|
||||||
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { HookHandler } from '@commerce/utils/types'
|
import { SWRHook } from '@commerce/utils/types'
|
||||||
import useCart, { UseCart, FetchCartInput } from '@commerce/cart/use-cart'
|
import useCart, { UseCart, FetchCartInput } from '@commerce/cart/use-cart'
|
||||||
import { normalizeCart } from '../lib/normalize'
|
import { normalizeCart } from '../lib/normalize'
|
||||||
import type { Cart } from '../types'
|
import type { Cart } from '../types'
|
||||||
import type { BigcommerceProvider } from '..'
|
|
||||||
|
|
||||||
export default useCart as UseCart<BigcommerceProvider>
|
export default useCart as UseCart<typeof handler>
|
||||||
|
|
||||||
export const handler: HookHandler<
|
export const handler: SWRHook<
|
||||||
Cart | null,
|
Cart | null,
|
||||||
{},
|
{},
|
||||||
FetchCartInput,
|
FetchCartInput,
|
||||||
@ -21,9 +20,9 @@ export const handler: HookHandler<
|
|||||||
const data = cartId ? await fetch(options) : null
|
const data = cartId ? await fetch(options) : null
|
||||||
return data && normalizeCart(data)
|
return data && normalizeCart(data)
|
||||||
},
|
},
|
||||||
useHook({ input, useData }) {
|
useHook: ({ useData }) => (input) => {
|
||||||
const response = useData({
|
const response = useData({
|
||||||
swrOptions: { revalidateOnFocus: false, ...input.swrOptions },
|
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
|
||||||
})
|
})
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { HookFetcher } from '@commerce/utils/types'
|
import type {
|
||||||
|
MutationHookContext,
|
||||||
|
HookFetcherContext,
|
||||||
|
} from '@commerce/utils/types'
|
||||||
import { ValidationError } from '@commerce/utils/errors'
|
import { ValidationError } from '@commerce/utils/errors'
|
||||||
import useCartRemoveItem, {
|
import useRemoveItem, {
|
||||||
RemoveItemInput as UseRemoveItemInput,
|
RemoveItemInput as RemoveItemInputBase,
|
||||||
|
UseRemoveItem,
|
||||||
} from '@commerce/cart/use-remove-item'
|
} from '@commerce/cart/use-remove-item'
|
||||||
import { normalizeCart } from '../lib/normalize'
|
import { normalizeCart } from '../lib/normalize'
|
||||||
import type {
|
import type {
|
||||||
@ -13,41 +17,41 @@ import type {
|
|||||||
} from '../types'
|
} from '../types'
|
||||||
import useCart from './use-cart'
|
import useCart from './use-cart'
|
||||||
|
|
||||||
const defaultOpts = {
|
|
||||||
url: '/api/bigcommerce/cart',
|
|
||||||
method: 'DELETE',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RemoveItemFn<T = any> = T extends LineItem
|
export type RemoveItemFn<T = any> = T extends LineItem
|
||||||
? (input?: RemoveItemInput<T>) => Promise<Cart | null>
|
? (input?: RemoveItemInput<T>) => Promise<Cart | null>
|
||||||
: (input: RemoveItemInput<T>) => Promise<Cart | null>
|
: (input: RemoveItemInput<T>) => Promise<Cart | null>
|
||||||
|
|
||||||
export type RemoveItemInput<T = any> = T extends LineItem
|
export type RemoveItemInput<T = any> = T extends LineItem
|
||||||
? Partial<UseRemoveItemInput>
|
? Partial<RemoveItemInputBase>
|
||||||
: UseRemoveItemInput
|
: RemoveItemInputBase
|
||||||
|
|
||||||
export const fetcher: HookFetcher<Cart | null, RemoveCartItemBody> = async (
|
export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||||
|
|
||||||
|
export const handler = {
|
||||||
|
fetchOptions: {
|
||||||
|
url: '/api/bigcommerce/cart',
|
||||||
|
method: 'DELETE',
|
||||||
|
},
|
||||||
|
async fetcher({
|
||||||
|
input: { itemId },
|
||||||
options,
|
options,
|
||||||
{ itemId },
|
fetch,
|
||||||
fetch
|
}: HookFetcherContext<RemoveCartItemBody>) {
|
||||||
) => {
|
|
||||||
const data = await fetch<BigcommerceCart>({
|
const data = await fetch<BigcommerceCart>({
|
||||||
...defaultOpts,
|
|
||||||
...options,
|
...options,
|
||||||
body: { itemId },
|
body: { itemId },
|
||||||
})
|
})
|
||||||
return normalizeCart(data)
|
return normalizeCart(data)
|
||||||
}
|
},
|
||||||
|
useHook: ({
|
||||||
export function extendHook(customFetcher: typeof fetcher) {
|
fetch,
|
||||||
const useRemoveItem = <T extends LineItem | undefined = undefined>(
|
}: MutationHookContext<Cart | null, RemoveCartItemBody>) => <
|
||||||
item?: T
|
T extends LineItem | undefined = undefined
|
||||||
|
>(
|
||||||
|
ctx: { item?: T } = {}
|
||||||
) => {
|
) => {
|
||||||
|
const { item } = ctx
|
||||||
const { mutate } = useCart()
|
const { mutate } = useCart()
|
||||||
const fn = useCartRemoveItem<Cart | null, RemoveCartItemBody>(
|
|
||||||
defaultOpts,
|
|
||||||
customFetcher
|
|
||||||
)
|
|
||||||
const removeItem: RemoveItemFn<LineItem> = async (input) => {
|
const removeItem: RemoveItemFn<LineItem> = async (input) => {
|
||||||
const itemId = input?.id ?? item?.id
|
const itemId = input?.id ?? item?.id
|
||||||
|
|
||||||
@ -57,17 +61,11 @@ export function extendHook(customFetcher: typeof fetcher) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await fn({ itemId })
|
const data = await fetch({ input: { itemId } })
|
||||||
await mutate(data, false)
|
await mutate(data, false)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
return useCallback(removeItem as RemoveItemFn<T>, [fn, mutate])
|
return useCallback(removeItem as RemoveItemFn<T>, [fetch, mutate])
|
||||||
}
|
},
|
||||||
|
|
||||||
useRemoveItem.extend = extendHook
|
|
||||||
|
|
||||||
return useRemoveItem
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default extendHook(fetcher)
|
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
import type { HookFetcher } from '@commerce/utils/types'
|
import type {
|
||||||
|
MutationHookContext,
|
||||||
|
HookFetcherContext,
|
||||||
|
} from '@commerce/utils/types'
|
||||||
import { ValidationError } from '@commerce/utils/errors'
|
import { ValidationError } from '@commerce/utils/errors'
|
||||||
import useCartUpdateItem, {
|
import useUpdateItem, {
|
||||||
UpdateItemInput as UseUpdateItemInput,
|
UpdateItemInput as UpdateItemInputBase,
|
||||||
|
UseUpdateItem,
|
||||||
} from '@commerce/cart/use-update-item'
|
} from '@commerce/cart/use-update-item'
|
||||||
import { normalizeCart } from '../lib/normalize'
|
import { normalizeCart } from '../lib/normalize'
|
||||||
import type {
|
import type {
|
||||||
@ -12,27 +16,33 @@ import type {
|
|||||||
BigcommerceCart,
|
BigcommerceCart,
|
||||||
LineItem,
|
LineItem,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
import { fetcher as removeFetcher } from './use-remove-item'
|
import { handler as removeItemHandler } from './use-remove-item'
|
||||||
import useCart from './use-cart'
|
import useCart from './use-cart'
|
||||||
|
|
||||||
const defaultOpts = {
|
export type UpdateItemInput<T = any> = T extends LineItem
|
||||||
|
? Partial<UpdateItemInputBase<LineItem>>
|
||||||
|
: UpdateItemInputBase<LineItem>
|
||||||
|
|
||||||
|
export default useUpdateItem as UseUpdateItem<typeof handler>
|
||||||
|
|
||||||
|
export const handler = {
|
||||||
|
fetchOptions: {
|
||||||
url: '/api/bigcommerce/cart',
|
url: '/api/bigcommerce/cart',
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
}
|
},
|
||||||
|
async fetcher({
|
||||||
export type UpdateItemInput<T = any> = T extends LineItem
|
input: { itemId, item },
|
||||||
? Partial<UseUpdateItemInput<LineItem>>
|
|
||||||
: UseUpdateItemInput<LineItem>
|
|
||||||
|
|
||||||
export const fetcher: HookFetcher<Cart | null, UpdateCartItemBody> = async (
|
|
||||||
options,
|
options,
|
||||||
{ itemId, item },
|
fetch,
|
||||||
fetch
|
}: HookFetcherContext<UpdateCartItemBody>) {
|
||||||
) => {
|
|
||||||
if (Number.isInteger(item.quantity)) {
|
if (Number.isInteger(item.quantity)) {
|
||||||
// Also allow the update hook to remove an item if the quantity is lower than 1
|
// Also allow the update hook to remove an item if the quantity is lower than 1
|
||||||
if (item.quantity! < 1) {
|
if (item.quantity! < 1) {
|
||||||
return removeFetcher(null, { itemId }, fetch)
|
return removeItemHandler.fetcher({
|
||||||
|
options: removeItemHandler.fetchOptions,
|
||||||
|
input: { itemId },
|
||||||
|
fetch,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} else if (item.quantity) {
|
} else if (item.quantity) {
|
||||||
throw new ValidationError({
|
throw new ValidationError({
|
||||||
@ -41,23 +51,24 @@ export const fetcher: HookFetcher<Cart | null, UpdateCartItemBody> = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await fetch<BigcommerceCart, UpdateCartItemBody>({
|
const data = await fetch<BigcommerceCart, UpdateCartItemBody>({
|
||||||
...defaultOpts,
|
|
||||||
...options,
|
...options,
|
||||||
body: { itemId, item },
|
body: { itemId, item },
|
||||||
})
|
})
|
||||||
|
|
||||||
return normalizeCart(data)
|
return normalizeCart(data)
|
||||||
}
|
},
|
||||||
|
useHook: ({
|
||||||
function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) {
|
fetch,
|
||||||
const useUpdateItem = <T extends LineItem | undefined = undefined>(
|
}: MutationHookContext<Cart | null, UpdateCartItemBody>) => <
|
||||||
|
T extends LineItem | undefined = undefined
|
||||||
|
>(
|
||||||
|
ctx: {
|
||||||
item?: T
|
item?: T
|
||||||
|
wait?: number
|
||||||
|
} = {}
|
||||||
) => {
|
) => {
|
||||||
const { mutate } = useCart()
|
const { item } = ctx
|
||||||
const fn = useCartUpdateItem<Cart | null, UpdateCartItemBody>(
|
const { mutate } = useCart() as any
|
||||||
defaultOpts,
|
|
||||||
customFetcher
|
|
||||||
)
|
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
debounce(async (input: UpdateItemInput<T>) => {
|
debounce(async (input: UpdateItemInput<T>) => {
|
||||||
@ -71,20 +82,16 @@ function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await fn({
|
const data = await fetch({
|
||||||
|
input: {
|
||||||
itemId,
|
itemId,
|
||||||
item: { productId, variantId, quantity: input.quantity },
|
item: { productId, variantId, quantity: input.quantity },
|
||||||
|
},
|
||||||
})
|
})
|
||||||
await mutate(data, false)
|
await mutate(data, false)
|
||||||
return data
|
return data
|
||||||
}, cfg?.wait ?? 500),
|
}, ctx.wait ?? 500),
|
||||||
[fn, mutate]
|
[fetch, mutate]
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
|
||||||
useUpdateItem.extend = extendHook
|
|
||||||
|
|
||||||
return useUpdateItem
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default extendHook(fetcher)
|
|
||||||
|
6
framework/bigcommerce/commerce.config.json
Normal file
6
framework/bigcommerce/commerce.config.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"provider": "bigcommerce",
|
||||||
|
"features": {
|
||||||
|
"wishlist": true
|
||||||
|
}
|
||||||
|
}
|
@ -68,14 +68,15 @@ async function getCustomerWishlist({
|
|||||||
const productsById = graphqlData.products.reduce<{
|
const productsById = graphqlData.products.reduce<{
|
||||||
[k: number]: ProductEdge
|
[k: number]: ProductEdge
|
||||||
}>((prods, p) => {
|
}>((prods, p) => {
|
||||||
prods[Number(p.node.entityId)] = p as any
|
prods[Number(p.id)] = p as any
|
||||||
return prods
|
return prods
|
||||||
}, {})
|
}, {})
|
||||||
// Populate the wishlist items with the graphql products
|
// Populate the wishlist items with the graphql products
|
||||||
wishlist.items.forEach((item) => {
|
wishlist.items.forEach((item) => {
|
||||||
const product = item && productsById[item.product_id!]
|
const product = item && productsById[item.product_id!]
|
||||||
if (item && product) {
|
if (item && product) {
|
||||||
item.product = product.node
|
// @ts-ignore Fix this type when the wishlist type is properly defined
|
||||||
|
item.product = product
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import { HookHandler } from '@commerce/utils/types'
|
import { SWRHook } from '@commerce/utils/types'
|
||||||
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
|
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
|
||||||
import type { Customer, CustomerData } from '../api/customers'
|
import type { Customer, CustomerData } from '../api/customers'
|
||||||
import type { BigcommerceProvider } from '..'
|
|
||||||
|
|
||||||
export default useCustomer as UseCustomer<BigcommerceProvider>
|
export default useCustomer as UseCustomer<typeof handler>
|
||||||
|
|
||||||
export const handler: HookHandler<Customer | null> = {
|
export const handler: SWRHook<Customer | null> = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
url: '/api/bigcommerce/customers',
|
url: '/api/bigcommerce/customers',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@ -14,11 +13,11 @@ export const handler: HookHandler<Customer | null> = {
|
|||||||
const data = await fetch<CustomerData | null>(options)
|
const data = await fetch<CustomerData | null>(options)
|
||||||
return data?.customer ?? null
|
return data?.customer ?? null
|
||||||
},
|
},
|
||||||
useHook({ input, useData }) {
|
useHook: ({ useData }) => (input) => {
|
||||||
return useData({
|
return useData({
|
||||||
swrOptions: {
|
swrOptions: {
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
...input.swrOptions,
|
...input?.swrOptions,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
8
framework/bigcommerce/next.config.js
Normal file
8
framework/bigcommerce/next.config.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const commerce = require('./commerce.config.json')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
commerce,
|
||||||
|
images: {
|
||||||
|
domains: ['cdn11.bigcommerce.com'],
|
||||||
|
},
|
||||||
|
}
|
@ -2,7 +2,7 @@ import type { GetProductQuery, GetProductQueryVariables } from '../schema'
|
|||||||
import setProductLocaleMeta from '../api/utils/set-product-locale-meta'
|
import setProductLocaleMeta from '../api/utils/set-product-locale-meta'
|
||||||
import { productInfoFragment } from '../api/fragments/product'
|
import { productInfoFragment } from '../api/fragments/product'
|
||||||
import { BigcommerceConfig, getConfig } from '../api'
|
import { BigcommerceConfig, getConfig } from '../api'
|
||||||
import { normalizeProduct } from '@framework/lib/normalize'
|
import { normalizeProduct } from '../lib/normalize'
|
||||||
import type { Product } from '@commerce/types'
|
import type { Product } from '@commerce/types'
|
||||||
|
|
||||||
export const getProductQuery = /* GraphQL */ `
|
export const getProductQuery = /* GraphQL */ `
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
export * from '@commerce/use-price'
|
export * from '@commerce/product/use-price'
|
||||||
export { default } from '@commerce/use-price'
|
export { default } from '@commerce/product/use-price'
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { HookHandler } from '@commerce/utils/types'
|
import { SWRHook } from '@commerce/utils/types'
|
||||||
import useSearch, { UseSearch } from '@commerce/product/use-search'
|
import useSearch, { UseSearch } from '@commerce/product/use-search'
|
||||||
import type { SearchProductsData } from '../api/catalog/products'
|
import type { SearchProductsData } from '../api/catalog/products'
|
||||||
import type { BigcommerceProvider } from '..'
|
|
||||||
|
|
||||||
export default useSearch as UseSearch<BigcommerceProvider>
|
export default useSearch as UseSearch<typeof handler>
|
||||||
|
|
||||||
export type SearchProductsInput = {
|
export type SearchProductsInput = {
|
||||||
search?: string
|
search?: string
|
||||||
@ -12,7 +11,7 @@ export type SearchProductsInput = {
|
|||||||
sort?: string
|
sort?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handler: HookHandler<
|
export const handler: SWRHook<
|
||||||
SearchProductsData,
|
SearchProductsData,
|
||||||
SearchProductsInput,
|
SearchProductsInput,
|
||||||
SearchProductsInput
|
SearchProductsInput
|
||||||
@ -37,7 +36,7 @@ export const handler: HookHandler<
|
|||||||
method: options.method,
|
method: options.method,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
useHook({ input, useData }) {
|
useHook: ({ useData }) => (input = {}) => {
|
||||||
return useData({
|
return useData({
|
||||||
input: [
|
input: [
|
||||||
['search', input.search],
|
['search', input.search],
|
||||||
|
@ -1,18 +1,34 @@
|
|||||||
import { handler as useCart } from './cart/use-cart'
|
import { handler as useCart } from './cart/use-cart'
|
||||||
import { handler as useAddItem } from './cart/use-add-item'
|
import { handler as useAddItem } from './cart/use-add-item'
|
||||||
|
import { handler as useUpdateItem } from './cart/use-update-item'
|
||||||
|
import { handler as useRemoveItem } from './cart/use-remove-item'
|
||||||
|
|
||||||
import { handler as useWishlist } from './wishlist/use-wishlist'
|
import { handler as useWishlist } from './wishlist/use-wishlist'
|
||||||
|
import { handler as useWishlistAddItem } from './wishlist/use-add-item'
|
||||||
|
import { handler as useWishlistRemoveItem } from './wishlist/use-remove-item'
|
||||||
|
|
||||||
import { handler as useCustomer } from './customer/use-customer'
|
import { handler as useCustomer } from './customer/use-customer'
|
||||||
import { handler as useSearch } from './product/use-search'
|
import { handler as useSearch } from './product/use-search'
|
||||||
|
|
||||||
|
import { handler as useLogin } from './auth/use-login'
|
||||||
|
import { handler as useLogout } from './auth/use-logout'
|
||||||
|
import { handler as useSignup } from './auth/use-signup'
|
||||||
|
|
||||||
import fetcher from './fetcher'
|
import fetcher from './fetcher'
|
||||||
|
|
||||||
export const bigcommerceProvider = {
|
export const bigcommerceProvider = {
|
||||||
locale: 'en-us',
|
locale: 'en-us',
|
||||||
cartCookie: 'bc_cartId',
|
cartCookie: 'bc_cartId',
|
||||||
fetcher,
|
fetcher,
|
||||||
cart: { useCart, useAddItem },
|
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
|
||||||
wishlist: { useWishlist },
|
wishlist: {
|
||||||
|
useWishlist,
|
||||||
|
useAddItem: useWishlistAddItem,
|
||||||
|
useRemoveItem: useWishlistRemoveItem,
|
||||||
|
},
|
||||||
customer: { useCustomer },
|
customer: { useCustomer },
|
||||||
products: { useSearch },
|
products: { useSearch },
|
||||||
|
auth: { useLogin, useLogout, useSignup },
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BigcommerceProvider = typeof bigcommerceProvider
|
export type BigcommerceProvider = typeof bigcommerceProvider
|
||||||
|
@ -43,9 +43,6 @@ export type CartItemBody = Core.CartItemBody & {
|
|||||||
optionSelections?: OptionSelections
|
optionSelections?: OptionSelections
|
||||||
}
|
}
|
||||||
|
|
||||||
type X = Core.CartItemBody extends CartItemBody ? any : never
|
|
||||||
type Y = CartItemBody extends Core.CartItemBody ? any : never
|
|
||||||
|
|
||||||
export type GetCartHandlerBody = Core.GetCartHandlerBody
|
export type GetCartHandlerBody = Core.GetCartHandlerBody
|
||||||
|
|
||||||
export type AddCartItemBody = Core.AddCartItemBody<CartItemBody>
|
export type AddCartItemBody = Core.AddCartItemBody<CartItemBody>
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
export { default as useAddItem } from './use-add-item'
|
export { default as useAddItem } from './use-add-item'
|
||||||
export { default as useWishlist } from './use-wishlist'
|
export { default as useWishlist } from './use-wishlist'
|
||||||
export { default as useRemoveItem } from './use-remove-item'
|
export { default as useRemoveItem } from './use-remove-item'
|
||||||
export { default as useWishlistActions } from './use-wishlist-actions'
|
|
||||||
|
@ -1,43 +1,24 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { HookFetcher } from '@commerce/utils/types'
|
import type { MutationHook } from '@commerce/utils/types'
|
||||||
import { CommerceError } from '@commerce/utils/errors'
|
import { CommerceError } from '@commerce/utils/errors'
|
||||||
import useWishlistAddItem, {
|
import useAddItem, { UseAddItem } from '@commerce/wishlist/use-add-item'
|
||||||
AddItemInput,
|
|
||||||
} from '@commerce/wishlist/use-add-item'
|
|
||||||
import { UseWishlistInput } from '@commerce/wishlist/use-wishlist'
|
|
||||||
import type { ItemBody, AddItemBody } from '../api/wishlist'
|
import type { ItemBody, AddItemBody } from '../api/wishlist'
|
||||||
import useCustomer from '../customer/use-customer'
|
import useCustomer from '../customer/use-customer'
|
||||||
import useWishlist from './use-wishlist'
|
import useWishlist from './use-wishlist'
|
||||||
import type { BigcommerceProvider } from '..'
|
|
||||||
|
|
||||||
const defaultOpts = {
|
export default useAddItem as UseAddItem<typeof handler>
|
||||||
|
|
||||||
|
export const handler: MutationHook<any, {}, ItemBody, AddItemBody> = {
|
||||||
|
fetchOptions: {
|
||||||
url: '/api/bigcommerce/wishlist',
|
url: '/api/bigcommerce/wishlist',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
}
|
},
|
||||||
|
useHook: ({ fetch }) => () => {
|
||||||
// export type AddItemInput = ItemBody
|
|
||||||
|
|
||||||
export const fetcher: HookFetcher<any, AddItemBody> = (
|
|
||||||
options,
|
|
||||||
{ item },
|
|
||||||
fetch
|
|
||||||
) => {
|
|
||||||
// TODO: add validations before doing the fetch
|
|
||||||
return fetch({
|
|
||||||
...defaultOpts,
|
|
||||||
...options,
|
|
||||||
body: { item },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extendHook(customFetcher: typeof fetcher) {
|
|
||||||
const useAddItem = (opts?: UseWishlistInput<BigcommerceProvider>) => {
|
|
||||||
const { data: customer } = useCustomer()
|
const { data: customer } = useCustomer()
|
||||||
const { revalidate } = useWishlist(opts)
|
const { revalidate } = useWishlist()
|
||||||
const fn = useWishlistAddItem(defaultOpts, customFetcher)
|
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async function addItem(input: AddItemInput<any>) {
|
async function addItem(item) {
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
// A signed customer is required in order to have a wishlist
|
// A signed customer is required in order to have a wishlist
|
||||||
throw new CommerceError({
|
throw new CommerceError({
|
||||||
@ -45,17 +26,12 @@ export function extendHook(customFetcher: typeof fetcher) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await fn({ item: input })
|
// TODO: add validations before doing the fetch
|
||||||
|
const data = await fetch({ input: { item } })
|
||||||
await revalidate()
|
await revalidate()
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
[fn, revalidate, customer]
|
[fetch, revalidate, customer]
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
|
||||||
useAddItem.extend = extendHook
|
|
||||||
|
|
||||||
return useAddItem
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default extendHook(fetcher)
|
|
||||||
|
@ -1,43 +1,32 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { HookFetcher } from '@commerce/utils/types'
|
import type { MutationHook } from '@commerce/utils/types'
|
||||||
import { CommerceError } from '@commerce/utils/errors'
|
import { CommerceError } from '@commerce/utils/errors'
|
||||||
import useWishlistRemoveItem from '@commerce/wishlist/use-remove-item'
|
import useRemoveItem, {
|
||||||
import type { RemoveItemBody } from '../api/wishlist'
|
RemoveItemInput,
|
||||||
|
UseRemoveItem,
|
||||||
|
} from '@commerce/wishlist/use-remove-item'
|
||||||
|
import type { RemoveItemBody, Wishlist } from '../api/wishlist'
|
||||||
import useCustomer from '../customer/use-customer'
|
import useCustomer from '../customer/use-customer'
|
||||||
import useWishlist from './use-wishlist'
|
import useWishlist, { UseWishlistInput } from './use-wishlist'
|
||||||
|
|
||||||
const defaultOpts = {
|
export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||||
|
|
||||||
|
export const handler: MutationHook<
|
||||||
|
Wishlist | null,
|
||||||
|
{ wishlist?: UseWishlistInput },
|
||||||
|
RemoveItemInput,
|
||||||
|
RemoveItemBody
|
||||||
|
> = {
|
||||||
|
fetchOptions: {
|
||||||
url: '/api/bigcommerce/wishlist',
|
url: '/api/bigcommerce/wishlist',
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
}
|
},
|
||||||
|
useHook: ({ fetch }) => ({ wishlist } = {}) => {
|
||||||
export type RemoveItemInput = {
|
|
||||||
id: string | number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fetcher: HookFetcher<any | null, RemoveItemBody> = (
|
|
||||||
options,
|
|
||||||
{ itemId },
|
|
||||||
fetch
|
|
||||||
) => {
|
|
||||||
return fetch({
|
|
||||||
...defaultOpts,
|
|
||||||
...options,
|
|
||||||
body: { itemId },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extendHook(customFetcher: typeof fetcher) {
|
|
||||||
const useRemoveItem = (opts?: any) => {
|
|
||||||
const { data: customer } = useCustomer()
|
const { data: customer } = useCustomer()
|
||||||
const { revalidate } = useWishlist(opts)
|
const { revalidate } = useWishlist(wishlist)
|
||||||
const fn = useWishlistRemoveItem<any | null, RemoveItemBody>(
|
|
||||||
defaultOpts,
|
|
||||||
customFetcher
|
|
||||||
)
|
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async function removeItem(input: RemoveItemInput) {
|
async function removeItem(input) {
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
// A signed customer is required in order to have a wishlist
|
// A signed customer is required in order to have a wishlist
|
||||||
throw new CommerceError({
|
throw new CommerceError({
|
||||||
@ -45,17 +34,11 @@ export function extendHook(customFetcher: typeof fetcher) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await fn({ itemId: String(input.id) })
|
const data = await fetch({ input: { itemId: String(input.id) } })
|
||||||
await revalidate()
|
await revalidate()
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
[fn, revalidate, customer]
|
[fetch, revalidate, customer]
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
|
||||||
useRemoveItem.extend = extendHook
|
|
||||||
|
|
||||||
return useRemoveItem
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default extendHook(fetcher)
|
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
import useAddItem from './use-add-item'
|
|
||||||
import useRemoveItem from './use-remove-item'
|
|
||||||
|
|
||||||
// This hook is probably not going to be used, but it's here
|
|
||||||
// to show how a commerce should be structuring it
|
|
||||||
export default function useWishlistActions() {
|
|
||||||
const addItem = useAddItem()
|
|
||||||
const removeItem = useRemoveItem()
|
|
||||||
|
|
||||||
return { addItem, removeItem }
|
|
||||||
}
|
|
@ -1,23 +1,24 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { HookHandler } from '@commerce/utils/types'
|
import { SWRHook } from '@commerce/utils/types'
|
||||||
import useWishlist, { UseWishlist } from '@commerce/wishlist/use-wishlist'
|
import useWishlist, { UseWishlist } from '@commerce/wishlist/use-wishlist'
|
||||||
import type { Wishlist } from '../api/wishlist'
|
import type { Wishlist } from '../api/wishlist'
|
||||||
import useCustomer from '../customer/use-customer'
|
import useCustomer from '../customer/use-customer'
|
||||||
import type { BigcommerceProvider } from '..'
|
|
||||||
|
|
||||||
export default useWishlist as UseWishlist<BigcommerceProvider>
|
export type UseWishlistInput = { includeProducts?: boolean }
|
||||||
|
|
||||||
export const handler: HookHandler<
|
export default useWishlist as UseWishlist<typeof handler>
|
||||||
|
|
||||||
|
export const handler: SWRHook<
|
||||||
Wishlist | null,
|
Wishlist | null,
|
||||||
{ includeProducts?: boolean },
|
UseWishlistInput,
|
||||||
{ customerId?: number; includeProducts: boolean },
|
{ customerId?: number } & UseWishlistInput,
|
||||||
{ isEmpty?: boolean }
|
{ isEmpty?: boolean }
|
||||||
> = {
|
> = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
url: '/api/bigcommerce/wishlist',
|
url: '/api/bigcommerce/wishlist',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
},
|
},
|
||||||
fetcher({ input: { customerId, includeProducts }, options, fetch }) {
|
async fetcher({ input: { customerId, includeProducts }, options, fetch }) {
|
||||||
if (!customerId) return null
|
if (!customerId) return null
|
||||||
|
|
||||||
// Use a dummy base as we only care about the relative path
|
// Use a dummy base as we only care about the relative path
|
||||||
@ -30,16 +31,16 @@ export const handler: HookHandler<
|
|||||||
method: options.method,
|
method: options.method,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
useHook({ input, useData }) {
|
useHook: ({ useData }) => (input) => {
|
||||||
const { data: customer } = useCustomer()
|
const { data: customer } = useCustomer()
|
||||||
const response = useData({
|
const response = useData({
|
||||||
input: [
|
input: [
|
||||||
['customerId', (customer as any)?.id],
|
['customerId', customer?.entityId],
|
||||||
['includeProducts', input.includeProducts],
|
['includeProducts', input?.includeProducts],
|
||||||
],
|
],
|
||||||
swrOptions: {
|
swrOptions: {
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
...input.swrOptions,
|
...input?.swrOptions,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
334
framework/commerce/README.md
Normal file
334
framework/commerce/README.md
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
# Commerce Framework
|
||||||
|
|
||||||
|
- [Commerce Framework](#commerce-framework)
|
||||||
|
- [Commerce Hooks](#commerce-hooks)
|
||||||
|
- [CommerceProvider](#commerceprovider)
|
||||||
|
- [Authentication Hooks](#authentication-hooks)
|
||||||
|
- [useSignup](#usesignup)
|
||||||
|
- [useLogin](#uselogin)
|
||||||
|
- [useLogout](#uselogout)
|
||||||
|
- [Customer Hooks](#customer-hooks)
|
||||||
|
- [useCustomer](#usecustomer)
|
||||||
|
- [Product Hooks](#product-hooks)
|
||||||
|
- [usePrice](#useprice)
|
||||||
|
- [useSearch](#usesearch)
|
||||||
|
- [Cart Hooks](#cart-hooks)
|
||||||
|
- [useCart](#usecart)
|
||||||
|
- [useAddItem](#useadditem)
|
||||||
|
- [useUpdateItem](#useupdateitem)
|
||||||
|
- [useRemoveItem](#useremoveitem)
|
||||||
|
- [Wishlist Hooks](#wishlist-hooks)
|
||||||
|
- [Commerce API](#commerce-api)
|
||||||
|
- [More](#more)
|
||||||
|
|
||||||
|
The commerce framework ships multiple hooks and a Node.js API, both using an underlying headless e-commerce platform, which we call commerce providers.
|
||||||
|
|
||||||
|
The core features are:
|
||||||
|
|
||||||
|
- Code splitted hooks for data fetching using [SWR](https://swr.vercel.app/), and to handle common user actions
|
||||||
|
- A Node.js API for initial data population, static generation of content and for creating the API endpoints that connect to the hooks, if required.
|
||||||
|
|
||||||
|
> 👩🔬 If you would like to contribute a new provider, check the docs for [Adding a new Commerce Provider](./new-provider.md).
|
||||||
|
|
||||||
|
> 🚧 The core commerce framework is under active development, new features and updates will be continuously added over time. Breaking changes are expected while we finish the API.
|
||||||
|
|
||||||
|
## Commerce Hooks
|
||||||
|
|
||||||
|
A commerce hook is a [React hook](https://reactjs.org/docs/hooks-intro.html) that's connected to a commerce provider. They focus on user actions and data fetching of data that wasn't statically generated.
|
||||||
|
|
||||||
|
Data fetching hooks use [SWR](https://swr.vercel.app/) underneath and you're welcome to use any of its [return values](https://swr.vercel.app/docs/options#return-values) and [options](https://swr.vercel.app/docs/options#options). For example, using the `useCustomer` hook:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const { data, isLoading, error } = useCustomer({
|
||||||
|
swrOptions: {
|
||||||
|
revalidateOnFocus: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### CommerceProvider
|
||||||
|
|
||||||
|
This component adds the provider config and handlers to the context of your React tree for it's children. You can optionally pass the `locale` to it:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { CommerceProvider } from '@framework'
|
||||||
|
|
||||||
|
const App = ({ locale = 'en-US', children }) => {
|
||||||
|
return <CommerceProvider locale={locale}>{children}</CommerceProvider>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication Hooks
|
||||||
|
|
||||||
|
### useSignup
|
||||||
|
|
||||||
|
Returns a _signup_ function that can be used to sign up the current visitor:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import useSignup from '@framework/auth/use-signup'
|
||||||
|
|
||||||
|
const SignupView = () => {
|
||||||
|
const signup = useSignup()
|
||||||
|
|
||||||
|
const handleSignup = async () => {
|
||||||
|
await signup({
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return <form onSubmit={handleSignup}>{children}</form>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### useLogin
|
||||||
|
|
||||||
|
Returns a _login_ function that can be used to sign in the current visitor into an existing customer:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import useLogin from '@framework/auth/use-login'
|
||||||
|
|
||||||
|
const LoginView = () => {
|
||||||
|
const login = useLogin()
|
||||||
|
const handleLogin = async () => {
|
||||||
|
await login({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return <form onSubmit={handleLogin}>{children}</form>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### useLogout
|
||||||
|
|
||||||
|
Returns a _logout_ function that signs out the current customer when called.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import useLogout from '@framework/auth/use-logout'
|
||||||
|
|
||||||
|
const LogoutButton = () => {
|
||||||
|
const logout = useLogout()
|
||||||
|
return (
|
||||||
|
<button type="button" onClick={() => logout()}>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customer Hooks
|
||||||
|
|
||||||
|
### useCustomer
|
||||||
|
|
||||||
|
Fetches and returns the data of the signed in customer:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import useCustomer from '@framework/customer/use-customer'
|
||||||
|
|
||||||
|
const Profile = () => {
|
||||||
|
const { data, isLoading, error } = useCustomer()
|
||||||
|
|
||||||
|
if (isLoading) return <p>Loading...</p>
|
||||||
|
if (error) return <p>{error.message}</p>
|
||||||
|
if (!data) return null
|
||||||
|
|
||||||
|
return <div>Hello, {data.firstName}</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Product Hooks
|
||||||
|
|
||||||
|
### usePrice
|
||||||
|
|
||||||
|
Helper hook to format price according to the commerce locale and currency code. It also handles discounts:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import useCart from '@framework/cart/use-cart'
|
||||||
|
import usePrice from '@framework/product/use-price'
|
||||||
|
|
||||||
|
// ...
|
||||||
|
const { data } = useCart()
|
||||||
|
const { price, discount, basePrice } = usePrice(
|
||||||
|
data && {
|
||||||
|
amount: data.subtotalPrice,
|
||||||
|
currencyCode: data.currency.code,
|
||||||
|
// If `baseAmount` is used, a discount will be calculated
|
||||||
|
// baseAmount: number,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### useSearch
|
||||||
|
|
||||||
|
Fetches and returns the products that match a set of filters:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import useSearch from '@framework/product/use-search'
|
||||||
|
|
||||||
|
const SearchPage = ({ searchString, category, brand, sortStr }) => {
|
||||||
|
const { data } = useSearch({
|
||||||
|
search: searchString || '',
|
||||||
|
categoryId: category?.entityId,
|
||||||
|
brandId: brand?.entityId,
|
||||||
|
sort: sortStr,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid layout="normal">
|
||||||
|
{data.products.map((product) => (
|
||||||
|
<ProductCard key={product.path} product={product} />
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cart Hooks
|
||||||
|
|
||||||
|
### useCart
|
||||||
|
|
||||||
|
Fetches and returns the data of the current cart:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import useCart from '@framework/cart/use-cart'
|
||||||
|
|
||||||
|
const CartTotal = () => {
|
||||||
|
const { data, isLoading, isEmpty, error } = useCart()
|
||||||
|
|
||||||
|
if (isLoading) return <p>Loading...</p>
|
||||||
|
if (error) return <p>{error.message}</p>
|
||||||
|
if (isEmpty) return <p>The cart is empty</p>
|
||||||
|
|
||||||
|
return <p>The cart total is {data.totalPrice}</p>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### useAddItem
|
||||||
|
|
||||||
|
Returns a function that adds a new item to the cart when called, if this is the first item it will create the cart:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { useAddItem } from '@framework/cart'
|
||||||
|
|
||||||
|
const AddToCartButton = ({ productId, variantId }) => {
|
||||||
|
const addItem = useAddItem()
|
||||||
|
|
||||||
|
const addToCart = async () => {
|
||||||
|
await addItem({
|
||||||
|
productId,
|
||||||
|
variantId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return <button onClick={addToCart}>Add To Cart</button>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### useUpdateItem
|
||||||
|
|
||||||
|
Returns a function that updates a current item in the cart when called, usually the quantity.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { useUpdateItem } from '@framework/cart'
|
||||||
|
|
||||||
|
const CartItemQuantity = ({ item }) => {
|
||||||
|
const [quantity, setQuantity] = useState(item.quantity)
|
||||||
|
const updateItem = useUpdateItem({ item })
|
||||||
|
|
||||||
|
const updateQuantity = async (e) => {
|
||||||
|
const val = e.target.value
|
||||||
|
|
||||||
|
setQuantity(val)
|
||||||
|
await updateItem({ quantity: val })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
max={99}
|
||||||
|
min={0}
|
||||||
|
value={quantity}
|
||||||
|
onChange={updateQuantity}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If the `quantity` is lower than 1 the item will be removed from the cart.
|
||||||
|
|
||||||
|
### useRemoveItem
|
||||||
|
|
||||||
|
Returns a function that removes an item in the cart when called:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { useRemoveItem } from '@framework/cart'
|
||||||
|
|
||||||
|
const RemoveButton = ({ item }) => {
|
||||||
|
const removeItem = useRemoveItem()
|
||||||
|
const handleRemove = async () => {
|
||||||
|
await removeItem(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <button onClick={handleRemove}>Remove</button>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wishlist Hooks
|
||||||
|
|
||||||
|
Wishlist hooks work just like [cart hooks](#cart-hooks). Feel free to check how those work first.
|
||||||
|
|
||||||
|
The example below shows how to use the `useWishlist`, `useAddItem` and `useRemoveItem` hooks:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { useWishlist, useAddItem, useRemoveItem } from '@framework/wishlist'
|
||||||
|
|
||||||
|
const WishlistButton = ({ productId, variant }) => {
|
||||||
|
const addItem = useAddItem()
|
||||||
|
const removeItem = useRemoveItem()
|
||||||
|
const { data, isLoading, isEmpty, error } = useWishlist()
|
||||||
|
|
||||||
|
if (isLoading) return <p>Loading...</p>
|
||||||
|
if (error) return <p>{error.message}</p>
|
||||||
|
if (isEmpty) return <p>The wihslist is empty</p>
|
||||||
|
|
||||||
|
const { data: customer } = useCustomer()
|
||||||
|
const itemInWishlist = data?.items?.find(
|
||||||
|
(item) => item.product_id === productId && item.variant_id === variant.id
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleWishlistChange = async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!customer) return
|
||||||
|
|
||||||
|
if (itemInWishlist) {
|
||||||
|
await removeItem({ id: itemInWishlist.id })
|
||||||
|
} else {
|
||||||
|
await addItem({
|
||||||
|
productId,
|
||||||
|
variantId: variant.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button onClick={handleWishlistChange}>
|
||||||
|
<Heart fill={itemInWishlist ? 'var(--pink)' : 'none'} />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commerce API
|
||||||
|
|
||||||
|
While commerce hooks focus on client side data fetching and interactions, the commerce API focuses on static content generation for pages and API endpoints in a Node.js context.
|
||||||
|
|
||||||
|
> The commerce API is currently going through a refactor in https://github.com/vercel/commerce/pull/252 - We'll update the docs once the API is released.
|
||||||
|
|
||||||
|
## More
|
||||||
|
|
||||||
|
Feel free to read through the source for more usage, and check the commerce vercel demo and commerce repo for usage examples: ([demo.vercel.store](https://demo.vercel.store/)) ([repo](https://github.com/vercel/commerce))
|
@ -2,6 +2,7 @@ import type { RequestInit, Response } from '@vercel/fetch'
|
|||||||
|
|
||||||
export interface CommerceAPIConfig {
|
export interface CommerceAPIConfig {
|
||||||
locale?: string
|
locale?: string
|
||||||
|
locales?: string[]
|
||||||
commerceUrl: string
|
commerceUrl: string
|
||||||
apiToken: string
|
apiToken: string
|
||||||
cartCookie: string
|
cartCookie: string
|
||||||
|
19
framework/commerce/auth/use-login.tsx
Normal file
19
framework/commerce/auth/use-login.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { useHook, useMutationHook } from '../utils/use-hook'
|
||||||
|
import { mutationFetcher } from '../utils/default-fetcher'
|
||||||
|
import type { MutationHook, HookFetcherFn } from '../utils/types'
|
||||||
|
import type { Provider } from '..'
|
||||||
|
|
||||||
|
export type UseLogin<
|
||||||
|
H extends MutationHook<any, any, any> = MutationHook<null, {}, {}>
|
||||||
|
> = ReturnType<H['useHook']>
|
||||||
|
|
||||||
|
export const fetcher: HookFetcherFn<null, {}> = mutationFetcher
|
||||||
|
|
||||||
|
const fn = (provider: Provider) => provider.auth?.useLogin!
|
||||||
|
|
||||||
|
const useLogin: UseLogin = (...args) => {
|
||||||
|
const hook = useHook(fn)
|
||||||
|
return useMutationHook({ fetcher, ...hook })(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useLogin
|
19
framework/commerce/auth/use-logout.tsx
Normal file
19
framework/commerce/auth/use-logout.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { useHook, useMutationHook } from '../utils/use-hook'
|
||||||
|
import { mutationFetcher } from '../utils/default-fetcher'
|
||||||
|
import type { HookFetcherFn, MutationHook } from '../utils/types'
|
||||||
|
import type { Provider } from '..'
|
||||||
|
|
||||||
|
export type UseLogout<
|
||||||
|
H extends MutationHook<any, any, any> = MutationHook<null>
|
||||||
|
> = ReturnType<H['useHook']>
|
||||||
|
|
||||||
|
export const fetcher: HookFetcherFn<null> = mutationFetcher
|
||||||
|
|
||||||
|
const fn = (provider: Provider) => provider.auth?.useLogout!
|
||||||
|
|
||||||
|
const useLogout: UseLogout = (...args) => {
|
||||||
|
const hook = useHook(fn)
|
||||||
|
return useMutationHook({ fetcher, ...hook })(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useLogout
|
19
framework/commerce/auth/use-signup.tsx
Normal file
19
framework/commerce/auth/use-signup.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { useHook, useMutationHook } from '../utils/use-hook'
|
||||||
|
import { mutationFetcher } from '../utils/default-fetcher'
|
||||||
|
import type { HookFetcherFn, MutationHook } from '../utils/types'
|
||||||
|
import type { Provider } from '..'
|
||||||
|
|
||||||
|
export type UseSignup<
|
||||||
|
H extends MutationHook<any, any, any> = MutationHook<null>
|
||||||
|
> = ReturnType<H['useHook']>
|
||||||
|
|
||||||
|
export const fetcher: HookFetcherFn<null> = mutationFetcher
|
||||||
|
|
||||||
|
const fn = (provider: Provider) => provider.auth?.useSignup!
|
||||||
|
|
||||||
|
const useSignup: UseSignup = (...args) => {
|
||||||
|
const hook = useHook(fn)
|
||||||
|
return useMutationHook({ fetcher, ...hook })(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useSignup
|
@ -1,69 +1,23 @@
|
|||||||
import { useCallback } from 'react'
|
import { useHook, useMutationHook } from '../utils/use-hook'
|
||||||
import type {
|
import { mutationFetcher } from '../utils/default-fetcher'
|
||||||
Prop,
|
import type { HookFetcherFn, MutationHook } from '../utils/types'
|
||||||
HookFetcherFn,
|
|
||||||
UseHookInput,
|
|
||||||
UseHookResponse,
|
|
||||||
} from '../utils/types'
|
|
||||||
import type { Cart, CartItemBody, AddCartItemBody } from '../types'
|
import type { Cart, CartItemBody, AddCartItemBody } from '../types'
|
||||||
import { Provider, useCommerce } from '..'
|
import type { Provider } from '..'
|
||||||
import { BigcommerceProvider } from '@framework'
|
|
||||||
|
|
||||||
export type UseAddItemHandler<P extends Provider> = Prop<
|
export type UseAddItem<
|
||||||
Prop<P, 'cart'>,
|
H extends MutationHook<any, any, any> = MutationHook<Cart, {}, CartItemBody>
|
||||||
'useAddItem'
|
> = ReturnType<H['useHook']>
|
||||||
>
|
|
||||||
|
|
||||||
// Input expected by the action returned by the `useAddItem` hook
|
|
||||||
export type UseAddItemInput<P extends Provider> = UseHookInput<
|
|
||||||
UseAddItemHandler<P>
|
|
||||||
>
|
|
||||||
|
|
||||||
export type UseAddItemResult<P extends Provider> = ReturnType<
|
|
||||||
UseHookResponse<UseAddItemHandler<P>>
|
|
||||||
>
|
|
||||||
|
|
||||||
export type UseAddItem<P extends Provider, Input> = Partial<
|
|
||||||
UseAddItemInput<P>
|
|
||||||
> extends UseAddItemInput<P>
|
|
||||||
? (input?: UseAddItemInput<P>) => (input: Input) => UseAddItemResult<P>
|
|
||||||
: (input: UseAddItemInput<P>) => (input: Input) => UseAddItemResult<P>
|
|
||||||
|
|
||||||
export const fetcher: HookFetcherFn<
|
export const fetcher: HookFetcherFn<
|
||||||
Cart,
|
Cart,
|
||||||
AddCartItemBody<CartItemBody>
|
AddCartItemBody<CartItemBody>
|
||||||
> = async ({ options, input, fetch }) => {
|
> = mutationFetcher
|
||||||
return fetch({ ...options, body: input })
|
|
||||||
|
const fn = (provider: Provider) => provider.cart?.useAddItem!
|
||||||
|
|
||||||
|
const useAddItem: UseAddItem = (...args) => {
|
||||||
|
const hook = useHook(fn)
|
||||||
|
return useMutationHook({ fetcher, ...hook })(...args)
|
||||||
}
|
}
|
||||||
|
|
||||||
type X = UseAddItemResult<BigcommerceProvider>
|
export default useAddItem
|
||||||
|
|
||||||
export default function useAddItem<P extends Provider, Input>(
|
|
||||||
input: UseAddItemInput<P>
|
|
||||||
) {
|
|
||||||
const { providerRef, fetcherRef } = useCommerce<P>()
|
|
||||||
|
|
||||||
const provider = providerRef.current
|
|
||||||
const opts = provider.cart?.useAddItem
|
|
||||||
|
|
||||||
const fetcherFn = opts?.fetcher ?? fetcher
|
|
||||||
const useHook = opts?.useHook ?? (() => () => {})
|
|
||||||
const fetchFn = provider.fetcher ?? fetcherRef.current
|
|
||||||
const action = useHook({ input })
|
|
||||||
|
|
||||||
return useCallback(
|
|
||||||
function addItem(input: Input) {
|
|
||||||
return action({
|
|
||||||
input,
|
|
||||||
fetch({ input }) {
|
|
||||||
return fetcherFn({
|
|
||||||
input,
|
|
||||||
options: opts!.fetchOptions,
|
|
||||||
fetch: fetchFn,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[input, fetchFn, opts?.fetchOptions]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
import type { HookFetcher, HookFetcherOptions } from '../utils/types'
|
|
||||||
import useAddItem from './use-add-item'
|
|
||||||
import useRemoveItem from './use-remove-item'
|
|
||||||
import useUpdateItem from './use-update-item'
|
|
||||||
|
|
||||||
// This hook is probably not going to be used, but it's here
|
|
||||||
// to show how a commerce should be structuring it
|
|
||||||
export default function useCartActions<T, Input>(
|
|
||||||
options: HookFetcherOptions,
|
|
||||||
fetcher: HookFetcher<T, Input>
|
|
||||||
) {
|
|
||||||
const addItem = useAddItem<T, Input>(options, fetcher)
|
|
||||||
const updateItem = useUpdateItem<T, Input>(options, fetcher)
|
|
||||||
const removeItem = useRemoveItem<T, Input>(options, fetcher)
|
|
||||||
|
|
||||||
return { addItem, updateItem, removeItem }
|
|
||||||
}
|
|
@ -1,34 +1,21 @@
|
|||||||
import Cookies from 'js-cookie'
|
import Cookies from 'js-cookie'
|
||||||
|
import { useHook, useSWRHook } from '../utils/use-hook'
|
||||||
|
import type { HookFetcherFn, SWRHook } from '../utils/types'
|
||||||
import type { Cart } from '../types'
|
import type { Cart } from '../types'
|
||||||
import type {
|
|
||||||
Prop,
|
|
||||||
HookFetcherFn,
|
|
||||||
UseHookInput,
|
|
||||||
UseHookResponse,
|
|
||||||
} from '../utils/types'
|
|
||||||
import useData from '../utils/use-data'
|
|
||||||
import { Provider, useCommerce } from '..'
|
import { Provider, useCommerce } from '..'
|
||||||
|
|
||||||
export type FetchCartInput = {
|
export type FetchCartInput = {
|
||||||
cartId?: Cart['id']
|
cartId?: Cart['id']
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UseCartHandler<P extends Provider> = Prop<
|
export type UseCart<
|
||||||
Prop<P, 'cart'>,
|
H extends SWRHook<any, any, any> = SWRHook<
|
||||||
'useCart'
|
Cart | null,
|
||||||
>
|
{},
|
||||||
|
FetchCartInput,
|
||||||
export type UseCartInput<P extends Provider> = UseHookInput<UseCartHandler<P>>
|
{ isEmpty?: boolean }
|
||||||
|
>
|
||||||
export type CartResponse<P extends Provider> = UseHookResponse<
|
> = ReturnType<H['useHook']>
|
||||||
UseCartHandler<P>
|
|
||||||
>
|
|
||||||
|
|
||||||
export type UseCart<P extends Provider> = Partial<
|
|
||||||
UseCartInput<P>
|
|
||||||
> extends UseCartInput<P>
|
|
||||||
? (input?: UseCartInput<P>) => CartResponse<P>
|
|
||||||
: (input: UseCartInput<P>) => CartResponse<P>
|
|
||||||
|
|
||||||
export const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = async ({
|
export const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = async ({
|
||||||
options,
|
options,
|
||||||
@ -38,32 +25,17 @@ export const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = async ({
|
|||||||
return cartId ? await fetch({ ...options }) : null
|
return cartId ? await fetch({ ...options }) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useCart<P extends Provider>(
|
const fn = (provider: Provider) => provider.cart?.useCart!
|
||||||
input: UseCartInput<P> = {}
|
|
||||||
) {
|
|
||||||
const { providerRef, fetcherRef, cartCookie } = useCommerce<P>()
|
|
||||||
|
|
||||||
const provider = providerRef.current
|
|
||||||
const opts = provider.cart?.useCart
|
|
||||||
|
|
||||||
const fetcherFn = opts?.fetcher ?? fetcher
|
|
||||||
const useHook = opts?.useHook ?? ((ctx) => ctx.useData())
|
|
||||||
|
|
||||||
|
const useCart: UseCart = (input) => {
|
||||||
|
const hook = useHook(fn)
|
||||||
|
const { cartCookie } = useCommerce()
|
||||||
|
const fetcherFn = hook.fetcher ?? fetcher
|
||||||
const wrapper: typeof fetcher = (context) => {
|
const wrapper: typeof fetcher = (context) => {
|
||||||
context.input.cartId = Cookies.get(cartCookie)
|
context.input.cartId = Cookies.get(cartCookie)
|
||||||
return fetcherFn(context)
|
return fetcherFn(context)
|
||||||
}
|
}
|
||||||
|
return useSWRHook({ ...hook, fetcher: wrapper })(input)
|
||||||
return useHook({
|
|
||||||
input,
|
|
||||||
useData(ctx) {
|
|
||||||
const response = useData(
|
|
||||||
{ ...opts!, fetcher: wrapper },
|
|
||||||
ctx?.input ?? [],
|
|
||||||
provider.fetcher ?? fetcherRef.current,
|
|
||||||
ctx?.swrOptions ?? input.swrOptions
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useCart
|
||||||
|
@ -1,10 +1,35 @@
|
|||||||
import useAction from '../utils/use-action'
|
import { useHook, useMutationHook } from '../utils/use-hook'
|
||||||
|
import { mutationFetcher } from '../utils/default-fetcher'
|
||||||
|
import type { HookFetcherFn, MutationHook } from '../utils/types'
|
||||||
|
import type { Cart, LineItem, RemoveCartItemBody } from '../types'
|
||||||
|
import type { Provider } from '..'
|
||||||
|
|
||||||
// Input expected by the action returned by the `useRemoveItem` hook
|
/**
|
||||||
export interface RemoveItemInput {
|
* Input expected by the action returned by the `useRemoveItem` hook
|
||||||
|
*/
|
||||||
|
export type RemoveItemInput = {
|
||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const useRemoveItem = useAction
|
export type UseRemoveItem<
|
||||||
|
H extends MutationHook<any, any, any> = MutationHook<
|
||||||
|
Cart | null,
|
||||||
|
{ item?: LineItem },
|
||||||
|
RemoveItemInput,
|
||||||
|
RemoveCartItemBody
|
||||||
|
>
|
||||||
|
> = ReturnType<H['useHook']>
|
||||||
|
|
||||||
|
export const fetcher: HookFetcherFn<
|
||||||
|
Cart | null,
|
||||||
|
RemoveCartItemBody
|
||||||
|
> = mutationFetcher
|
||||||
|
|
||||||
|
const fn = (provider: Provider) => provider.cart?.useRemoveItem!
|
||||||
|
|
||||||
|
const useRemoveItem: UseRemoveItem = (input) => {
|
||||||
|
const hook = useHook(fn)
|
||||||
|
return useMutationHook({ fetcher, ...hook })(input)
|
||||||
|
}
|
||||||
|
|
||||||
export default useRemoveItem
|
export default useRemoveItem
|
||||||
|
@ -1,11 +1,38 @@
|
|||||||
import useAction from '../utils/use-action'
|
import { useHook, useMutationHook } from '../utils/use-hook'
|
||||||
import type { CartItemBody } from '../types'
|
import { mutationFetcher } from '../utils/default-fetcher'
|
||||||
|
import type { HookFetcherFn, MutationHook } from '../utils/types'
|
||||||
|
import type { Cart, CartItemBody, LineItem, UpdateCartItemBody } from '../types'
|
||||||
|
import type { Provider } from '..'
|
||||||
|
|
||||||
// Input expected by the action returned by the `useUpdateItem` hook
|
/**
|
||||||
|
* Input expected by the action returned by the `useUpdateItem` hook
|
||||||
|
*/
|
||||||
export type UpdateItemInput<T extends CartItemBody> = T & {
|
export type UpdateItemInput<T extends CartItemBody> = T & {
|
||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const useUpdateItem = useAction
|
export type UseUpdateItem<
|
||||||
|
H extends MutationHook<any, any, any> = MutationHook<
|
||||||
|
Cart | null,
|
||||||
|
{
|
||||||
|
item?: LineItem
|
||||||
|
wait?: number
|
||||||
|
},
|
||||||
|
UpdateItemInput<CartItemBody>,
|
||||||
|
UpdateCartItemBody<CartItemBody>
|
||||||
|
>
|
||||||
|
> = ReturnType<H['useHook']>
|
||||||
|
|
||||||
|
export const fetcher: HookFetcherFn<
|
||||||
|
Cart | null,
|
||||||
|
UpdateCartItemBody<CartItemBody>
|
||||||
|
> = mutationFetcher
|
||||||
|
|
||||||
|
const fn = (provider: Provider) => provider.cart?.useUpdateItem!
|
||||||
|
|
||||||
|
const useUpdateItem: UseUpdateItem = (input) => {
|
||||||
|
const hook = useHook(fn)
|
||||||
|
return useMutationHook({ fetcher, ...hook })(input)
|
||||||
|
}
|
||||||
|
|
||||||
export default useUpdateItem
|
export default useUpdateItem
|
||||||
|
68
framework/commerce/config.js
Normal file
68
framework/commerce/config.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* This file is expected to be used in next.config.js only
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require('path')
|
||||||
|
const fs = require('fs')
|
||||||
|
const merge = require('deepmerge')
|
||||||
|
const prettier = require('prettier')
|
||||||
|
|
||||||
|
const PROVIDERS = ['bigcommerce', 'shopify', 'swell', 'vendure']
|
||||||
|
|
||||||
|
function getProviderName() {
|
||||||
|
return (
|
||||||
|
process.env.COMMERCE_PROVIDER ||
|
||||||
|
(process.env.BIGCOMMERCE_STOREFRONT_API_URL
|
||||||
|
? 'bigcommerce'
|
||||||
|
: process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN
|
||||||
|
? 'shopify'
|
||||||
|
: process.env.NEXT_PUBLIC_SWELL_STORE_ID
|
||||||
|
? 'swell'
|
||||||
|
: null)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function withCommerceConfig(nextConfig = {}) {
|
||||||
|
const commerce = nextConfig.commerce || {}
|
||||||
|
const name = commerce.provider || getProviderName()
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
throw new Error(
|
||||||
|
`The commerce provider is missing, please add a valid provider name or its environment variables`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!PROVIDERS.includes(name)) {
|
||||||
|
throw new Error(
|
||||||
|
`The commerce provider "${name}" can't be found, please use one of "${PROVIDERS.join(
|
||||||
|
', '
|
||||||
|
)}"`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const commerceNextConfig = require(path.join('../', name, 'next.config'))
|
||||||
|
const config = merge(commerceNextConfig, nextConfig)
|
||||||
|
|
||||||
|
config.env = config.env || {}
|
||||||
|
|
||||||
|
Object.entries(config.commerce.features).forEach(([k, v]) => {
|
||||||
|
if (v) config.env[`COMMERCE_${k.toUpperCase()}_ENABLED`] = true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update paths in `tsconfig.json` to point to the selected provider
|
||||||
|
if (config.commerce.updateTSConfig !== false) {
|
||||||
|
const tsconfigPath = path.join(process.cwd(), 'tsconfig.json')
|
||||||
|
const tsconfig = require(tsconfigPath)
|
||||||
|
|
||||||
|
tsconfig.compilerOptions.paths['@framework'] = [`framework/${name}`]
|
||||||
|
tsconfig.compilerOptions.paths['@framework/*'] = [`framework/${name}/*`]
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
tsconfigPath,
|
||||||
|
prettier.format(JSON.stringify(tsconfig), { parser: 'json' })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { withCommerceConfig, getProviderName }
|
@ -1,56 +1,20 @@
|
|||||||
|
import { useHook, useSWRHook } from '../utils/use-hook'
|
||||||
|
import { SWRFetcher } from '../utils/default-fetcher'
|
||||||
|
import type { HookFetcherFn, SWRHook } from '../utils/types'
|
||||||
import type { Customer } from '../types'
|
import type { Customer } from '../types'
|
||||||
import type {
|
import { Provider } from '..'
|
||||||
Prop,
|
|
||||||
HookFetcherFn,
|
|
||||||
UseHookInput,
|
|
||||||
UseHookResponse,
|
|
||||||
} from '../utils/types'
|
|
||||||
import defaultFetcher from '../utils/default-fetcher'
|
|
||||||
import useData from '../utils/use-data'
|
|
||||||
import { Provider, useCommerce } from '..'
|
|
||||||
|
|
||||||
export type UseCustomerHandler<P extends Provider> = Prop<
|
export type UseCustomer<
|
||||||
Prop<P, 'customer'>,
|
H extends SWRHook<any, any, any> = SWRHook<Customer | null>
|
||||||
'useCustomer'
|
> = ReturnType<H['useHook']>
|
||||||
>
|
|
||||||
|
|
||||||
export type UseCustomerInput<P extends Provider> = UseHookInput<
|
export const fetcher: HookFetcherFn<Customer | null, any> = SWRFetcher
|
||||||
UseCustomerHandler<P>
|
|
||||||
>
|
|
||||||
|
|
||||||
export type CustomerResponse<P extends Provider> = UseHookResponse<
|
const fn = (provider: Provider) => provider.customer?.useCustomer!
|
||||||
UseCustomerHandler<P>
|
|
||||||
>
|
|
||||||
|
|
||||||
export type UseCustomer<P extends Provider> = Partial<
|
const useCustomer: UseCustomer = (input) => {
|
||||||
UseCustomerInput<P>
|
const hook = useHook(fn)
|
||||||
> extends UseCustomerInput<P>
|
return useSWRHook({ fetcher, ...hook })(input)
|
||||||
? (input?: UseCustomerInput<P>) => CustomerResponse<P>
|
|
||||||
: (input: UseCustomerInput<P>) => CustomerResponse<P>
|
|
||||||
|
|
||||||
export const fetcher = defaultFetcher as HookFetcherFn<Customer | null>
|
|
||||||
|
|
||||||
export default function useCustomer<P extends Provider>(
|
|
||||||
input: UseCustomerInput<P> = {}
|
|
||||||
) {
|
|
||||||
const { providerRef, fetcherRef } = useCommerce<P>()
|
|
||||||
|
|
||||||
const provider = providerRef.current
|
|
||||||
const opts = provider.customer?.useCustomer
|
|
||||||
|
|
||||||
const fetcherFn = opts?.fetcher ?? fetcher
|
|
||||||
const useHook = opts?.useHook ?? ((ctx) => ctx.useData())
|
|
||||||
|
|
||||||
return useHook({
|
|
||||||
input,
|
|
||||||
useData(ctx) {
|
|
||||||
const response = useData(
|
|
||||||
{ ...opts!, fetcher: fetcherFn },
|
|
||||||
ctx?.input ?? [],
|
|
||||||
provider.fetcher ?? fetcherRef.current,
|
|
||||||
ctx?.swrOptions ?? input.swrOptions
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useCustomer
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { Fetcher, HookHandler, MutationHandler } from './utils/types'
|
import { Fetcher, SWRHook, MutationHook } from './utils/types'
|
||||||
import type { FetchCartInput } from './cart/use-cart'
|
import type { FetchCartInput } from './cart/use-cart'
|
||||||
import type { Cart, Wishlist, Customer, SearchProductsData } from './types'
|
import type { Cart, Wishlist, Customer, SearchProductsData } from './types'
|
||||||
|
|
||||||
@ -15,17 +15,26 @@ const Commerce = createContext<CommerceContextValue<any> | {}>({})
|
|||||||
export type Provider = CommerceConfig & {
|
export type Provider = CommerceConfig & {
|
||||||
fetcher: Fetcher
|
fetcher: Fetcher
|
||||||
cart?: {
|
cart?: {
|
||||||
useCart?: HookHandler<Cart | null, any, FetchCartInput>
|
useCart?: SWRHook<Cart | null, any, FetchCartInput>
|
||||||
useAddItem?: MutationHandler<Cart, any, any>
|
useAddItem?: MutationHook<any, any, any>
|
||||||
|
useUpdateItem?: MutationHook<any, any, any>
|
||||||
|
useRemoveItem?: MutationHook<any, any, any>
|
||||||
}
|
}
|
||||||
wishlist?: {
|
wishlist?: {
|
||||||
useWishlist?: HookHandler<Wishlist | null, any, any>
|
useWishlist?: SWRHook<Wishlist | null, any, any>
|
||||||
|
useAddItem?: MutationHook<any, any, any>
|
||||||
|
useRemoveItem?: MutationHook<any, any, any>
|
||||||
}
|
}
|
||||||
customer: {
|
customer?: {
|
||||||
useCustomer?: HookHandler<Customer | null, any, any>
|
useCustomer?: SWRHook<Customer | null, any, any>
|
||||||
}
|
}
|
||||||
products: {
|
products?: {
|
||||||
useSearch?: HookHandler<SearchProductsData, any, any>
|
useSearch?: SWRHook<SearchProductsData, any, any>
|
||||||
|
}
|
||||||
|
auth?: {
|
||||||
|
useSignup?: MutationHook<any, any, any>
|
||||||
|
useLogin?: MutationHook<any, any, any>
|
||||||
|
useLogout?: MutationHook<any, any, any>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
239
framework/commerce/new-provider.md
Normal file
239
framework/commerce/new-provider.md
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
# Adding a new Commerce Provider
|
||||||
|
|
||||||
|
A commerce provider is a headless e-commerce platform that integrates with the [Commerce Framework](./README.md). Right now we have the following providers:
|
||||||
|
|
||||||
|
- BigCommerce ([framework/bigcommerce](../bigcommerce))
|
||||||
|
- Shopify ([framework/shopify](../shopify))
|
||||||
|
|
||||||
|
Adding a commerce provider means adding a new folder in `framework` with a folder structure like the next one:
|
||||||
|
|
||||||
|
- `api`
|
||||||
|
- index.ts
|
||||||
|
- `product`
|
||||||
|
- usePrice
|
||||||
|
- useSearch
|
||||||
|
- getProduct
|
||||||
|
- getAllProducts
|
||||||
|
- `wishlist`
|
||||||
|
- useWishlist
|
||||||
|
- useAddItem
|
||||||
|
- useRemoveItem
|
||||||
|
- `auth`
|
||||||
|
- useLogin
|
||||||
|
- useLogout
|
||||||
|
- useSignup
|
||||||
|
- `customer`
|
||||||
|
- useCustomer
|
||||||
|
- getCustomerId
|
||||||
|
- getCustomerWistlist
|
||||||
|
- `cart`
|
||||||
|
- useCart
|
||||||
|
- useAddItem
|
||||||
|
- useRemoveItem
|
||||||
|
- useUpdateItem
|
||||||
|
- `env.template`
|
||||||
|
- `index.ts`
|
||||||
|
- `provider.ts`
|
||||||
|
- `commerce.config.json`
|
||||||
|
- `next.config.js`
|
||||||
|
- `README.md`
|
||||||
|
|
||||||
|
`provider.ts` exports a provider object with handlers for the [Commerce Hooks](./README.md#commerce-hooks) and `api/index.ts` exports a Node.js provider for the [Commerce API](./README.md#commerce-api)
|
||||||
|
|
||||||
|
> **Important:** We use TypeScript for every provider and expect its usage for every new one.
|
||||||
|
|
||||||
|
The app imports from the provider directly instead of the core commerce folder (`framework/commerce`), but all providers are interchangeable and to achieve it every provider always has to implement the core types and helpers.
|
||||||
|
|
||||||
|
The provider folder should only depend on `framework/commerce` and dependencies in the main `package.json`. In the future we'll move the `framework` folder to a package that can be shared easily for multiple apps.
|
||||||
|
|
||||||
|
## Adding the provider hooks
|
||||||
|
|
||||||
|
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 type { ReactNode } from 'react'
|
||||||
|
import {
|
||||||
|
CommerceConfig,
|
||||||
|
CommerceProvider as CoreCommerceProvider,
|
||||||
|
useCommerce as useCoreCommerce,
|
||||||
|
} from '@commerce'
|
||||||
|
import { bigcommerceProvider, BigcommerceProvider } from './provider'
|
||||||
|
|
||||||
|
export { bigcommerceProvider }
|
||||||
|
export type { BigcommerceProvider }
|
||||||
|
|
||||||
|
export const bigcommerceConfig: CommerceConfig = {
|
||||||
|
locale: 'en-us',
|
||||||
|
cartCookie: 'bc_cartId',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BigcommerceConfig = Partial<CommerceConfig>
|
||||||
|
|
||||||
|
export type BigcommerceProps = {
|
||||||
|
children?: ReactNode
|
||||||
|
locale: string
|
||||||
|
} & BigcommerceConfig
|
||||||
|
|
||||||
|
export function CommerceProvider({ children, ...config }: BigcommerceProps) {
|
||||||
|
return (
|
||||||
|
<CoreCommerceProvider
|
||||||
|
provider={bigcommerceProvider}
|
||||||
|
config={{ ...bigcommerceConfig, ...config }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</CoreCommerceProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCommerce = () => useCoreCommerce<BigcommerceProvider>()
|
||||||
|
```
|
||||||
|
|
||||||
|
The exported types and components extend from the core ones exported by `@commerce`, which refers to `framework/commerce`.
|
||||||
|
|
||||||
|
The `bigcommerceProvider` object looks like this:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { handler as useCart } from './cart/use-cart'
|
||||||
|
import { handler as useAddItem } from './cart/use-add-item'
|
||||||
|
import { handler as useUpdateItem } from './cart/use-update-item'
|
||||||
|
import { handler as useRemoveItem } from './cart/use-remove-item'
|
||||||
|
|
||||||
|
import { handler as useWishlist } from './wishlist/use-wishlist'
|
||||||
|
import { handler as useWishlistAddItem } from './wishlist/use-add-item'
|
||||||
|
import { handler as useWishlistRemoveItem } from './wishlist/use-remove-item'
|
||||||
|
|
||||||
|
import { handler as useCustomer } from './customer/use-customer'
|
||||||
|
import { handler as useSearch } from './product/use-search'
|
||||||
|
|
||||||
|
import { handler as useLogin } from './auth/use-login'
|
||||||
|
import { handler as useLogout } from './auth/use-logout'
|
||||||
|
import { handler as useSignup } from './auth/use-signup'
|
||||||
|
|
||||||
|
import fetcher from './fetcher'
|
||||||
|
|
||||||
|
export const bigcommerceProvider = {
|
||||||
|
locale: 'en-us',
|
||||||
|
cartCookie: 'bc_cartId',
|
||||||
|
fetcher,
|
||||||
|
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
|
||||||
|
wishlist: {
|
||||||
|
useWishlist,
|
||||||
|
useAddItem: useWishlistAddItem,
|
||||||
|
useRemoveItem: useWishlistRemoveItem,
|
||||||
|
},
|
||||||
|
customer: { useCustomer },
|
||||||
|
products: { useSearch },
|
||||||
|
auth: { useLogin, useLogout, useSignup },
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BigcommerceProvider = typeof bigcommerceProvider
|
||||||
|
```
|
||||||
|
|
||||||
|
The provider object, in this case `bigcommerceProvider`, has to match the `Provider` type defined in [framework/commerce](./index.ts).
|
||||||
|
|
||||||
|
A hook handler, like `useCart`, looks like this:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { SWRHook } from '@commerce/utils/types'
|
||||||
|
import useCart, { UseCart, FetchCartInput } from '@commerce/cart/use-cart'
|
||||||
|
import { normalizeCart } from '../lib/normalize'
|
||||||
|
import type { Cart } from '../types'
|
||||||
|
|
||||||
|
export default useCart as UseCart<typeof handler>
|
||||||
|
|
||||||
|
export const handler: SWRHook<
|
||||||
|
Cart | null,
|
||||||
|
{},
|
||||||
|
FetchCartInput,
|
||||||
|
{ isEmpty?: boolean }
|
||||||
|
> = {
|
||||||
|
fetchOptions: {
|
||||||
|
url: '/api/bigcommerce/cart',
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
async fetcher({ input: { cartId }, options, fetch }) {
|
||||||
|
const data = cartId ? await fetch(options) : null
|
||||||
|
return data && normalizeCart(data)
|
||||||
|
},
|
||||||
|
useHook: ({ useData }) => (input) => {
|
||||||
|
const response = useData({
|
||||||
|
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
|
||||||
|
})
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
Object.create(response, {
|
||||||
|
isEmpty: {
|
||||||
|
get() {
|
||||||
|
return (response.data?.lineItems.length ?? 0) <= 0
|
||||||
|
},
|
||||||
|
enumerable: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[response]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the case of data fetching hooks like `useCart` each handler has to implement the `SWRHook` type that's defined in the core types. For mutations it's the `MutationHook`, e.g for `useAddItem`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import type { MutationHook } from '@commerce/utils/types'
|
||||||
|
import { CommerceError } from '@commerce/utils/errors'
|
||||||
|
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
|
||||||
|
import { normalizeCart } from '../lib/normalize'
|
||||||
|
import type {
|
||||||
|
Cart,
|
||||||
|
BigcommerceCart,
|
||||||
|
CartItemBody,
|
||||||
|
AddCartItemBody,
|
||||||
|
} from '../types'
|
||||||
|
import useCart from './use-cart'
|
||||||
|
|
||||||
|
export default useAddItem as UseAddItem<typeof handler>
|
||||||
|
|
||||||
|
export const handler: MutationHook<Cart, {}, CartItemBody> = {
|
||||||
|
fetchOptions: {
|
||||||
|
url: '/api/bigcommerce/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<BigcommerceCart, AddCartItemBody>({
|
||||||
|
...options,
|
||||||
|
body: { item },
|
||||||
|
})
|
||||||
|
|
||||||
|
return normalizeCart(data)
|
||||||
|
},
|
||||||
|
useHook: ({ fetch }) => () => {
|
||||||
|
const { mutate } = useCart()
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
async function addItem(input) {
|
||||||
|
const data = await fetch({ input })
|
||||||
|
await mutate(data, false)
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
[fetch, mutate]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding the Node.js provider API
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
> The commerce API is currently going through a refactor in https://github.com/vercel/commerce/pull/252 - We'll update the docs once the API is released.
|
@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useCommerce } from '.'
|
import { useCommerce } from '..'
|
||||||
|
|
||||||
export function formatPrice({
|
export function formatPrice({
|
||||||
amount,
|
amount,
|
@ -1,57 +1,20 @@
|
|||||||
|
import { useHook, useSWRHook } from '../utils/use-hook'
|
||||||
|
import { SWRFetcher } from '../utils/default-fetcher'
|
||||||
|
import type { HookFetcherFn, SWRHook } from '../utils/types'
|
||||||
import type { SearchProductsData } from '../types'
|
import type { SearchProductsData } from '../types'
|
||||||
import type {
|
import { Provider } from '..'
|
||||||
Prop,
|
|
||||||
HookFetcherFn,
|
|
||||||
UseHookInput,
|
|
||||||
UseHookResponse,
|
|
||||||
} from '../utils/types'
|
|
||||||
import defaultFetcher from '../utils/default-fetcher'
|
|
||||||
import useData from '../utils/use-data'
|
|
||||||
import { Provider, useCommerce } from '..'
|
|
||||||
import { BigcommerceProvider } from '@framework'
|
|
||||||
|
|
||||||
export type UseSearchHandler<P extends Provider> = Prop<
|
export type UseSearch<
|
||||||
Prop<P, 'products'>,
|
H extends SWRHook<any, any, any> = SWRHook<SearchProductsData>
|
||||||
'useSearch'
|
> = ReturnType<H['useHook']>
|
||||||
>
|
|
||||||
|
|
||||||
export type UseSeachInput<P extends Provider> = UseHookInput<
|
export const fetcher: HookFetcherFn<SearchProductsData, any> = SWRFetcher
|
||||||
UseSearchHandler<P>
|
|
||||||
>
|
|
||||||
|
|
||||||
export type SearchResponse<P extends Provider> = UseHookResponse<
|
const fn = (provider: Provider) => provider.products?.useSearch!
|
||||||
UseSearchHandler<P>
|
|
||||||
>
|
|
||||||
|
|
||||||
export type UseSearch<P extends Provider> = Partial<
|
const useSearch: UseSearch = (input) => {
|
||||||
UseSeachInput<P>
|
const hook = useHook(fn)
|
||||||
> extends UseSeachInput<P>
|
return useSWRHook({ fetcher, ...hook })(input)
|
||||||
? (input?: UseSeachInput<P>) => SearchResponse<P>
|
|
||||||
: (input: UseSeachInput<P>) => SearchResponse<P>
|
|
||||||
|
|
||||||
export const fetcher = defaultFetcher as HookFetcherFn<SearchProductsData>
|
|
||||||
|
|
||||||
export default function useSearch<P extends Provider>(
|
|
||||||
input: UseSeachInput<P> = {}
|
|
||||||
) {
|
|
||||||
const { providerRef, fetcherRef } = useCommerce<P>()
|
|
||||||
|
|
||||||
const provider = providerRef.current
|
|
||||||
const opts = provider.products?.useSearch
|
|
||||||
|
|
||||||
const fetcherFn = opts?.fetcher ?? fetcher
|
|
||||||
const useHook = opts?.useHook ?? ((ctx) => ctx.useData())
|
|
||||||
|
|
||||||
return useHook({
|
|
||||||
input,
|
|
||||||
useData(ctx) {
|
|
||||||
const response = useData(
|
|
||||||
{ ...opts!, fetcher: fetcherFn },
|
|
||||||
ctx?.input ?? [],
|
|
||||||
provider.fetcher ?? fetcherRef.current,
|
|
||||||
ctx?.swrOptions ?? input.swrOptions
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useSearch
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
import type { Wishlist as BCWishlist } from '@framework/api/wishlist'
|
import type { Wishlist as BCWishlist } from '../bigcommerce/api/wishlist'
|
||||||
import type { Customer as BCCustomer } from '@framework/api/customers'
|
import type { Customer as BCCustomer } from '../bigcommerce/api/customers'
|
||||||
import type { SearchProductsData as BCSearchProductsData } from '@framework/api/catalog/products'
|
import type { SearchProductsData as BCSearchProductsData } from '../bigcommerce/api/catalog/products'
|
||||||
|
|
||||||
export type CommerceProviderConfig = {
|
|
||||||
features: Record<string, boolean>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Discount = {
|
export type Discount = {
|
||||||
// The value of the discount, can be an amount or percentage
|
// The value of the discount, can be an amount or percentage
|
||||||
@ -167,6 +163,7 @@ interface Entity {
|
|||||||
export interface Product extends Entity {
|
export interface Product extends Entity {
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
|
descriptionHtml?: string
|
||||||
slug?: string
|
slug?: string
|
||||||
path?: string
|
path?: string
|
||||||
images: ProductImage[]
|
images: ProductImage[]
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
import useAction from './utils/use-action'
|
|
||||||
|
|
||||||
const useLogin = useAction
|
|
||||||
|
|
||||||
export default useLogin
|
|
@ -1,5 +0,0 @@
|
|||||||
import useAction from './utils/use-action'
|
|
||||||
|
|
||||||
const useLogout = useAction
|
|
||||||
|
|
||||||
export default useLogout
|
|
@ -1,5 +0,0 @@
|
|||||||
import useAction from './utils/use-action'
|
|
||||||
|
|
||||||
const useSignup = useAction
|
|
||||||
|
|
||||||
export default useSignup
|
|
@ -1,6 +1,12 @@
|
|||||||
import type { HookFetcherFn } from './types'
|
import type { HookFetcherFn } from './types'
|
||||||
|
|
||||||
const defaultFetcher: HookFetcherFn<any> = ({ options, fetch }) =>
|
export const SWRFetcher: HookFetcherFn<any, any> = ({ options, fetch }) =>
|
||||||
fetch(options)
|
fetch(options)
|
||||||
|
|
||||||
export default defaultFetcher
|
export const mutationFetcher: HookFetcherFn<any, any> = ({
|
||||||
|
input,
|
||||||
|
options,
|
||||||
|
fetch,
|
||||||
|
}) => fetch({ ...options, body: input })
|
||||||
|
|
||||||
|
export default SWRFetcher
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
import commerceProviderConfig from '@framework/config.json'
|
|
||||||
import type { CommerceProviderConfig } from '../types'
|
|
||||||
import memo from 'lodash.memoize'
|
|
||||||
|
|
||||||
type FeaturesAPI = {
|
|
||||||
isEnabled: (desideredFeature: string) => boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
function isFeatureEnabled(config: CommerceProviderConfig) {
|
|
||||||
const features = config.features
|
|
||||||
return (desideredFeature: string) =>
|
|
||||||
Object.keys(features)
|
|
||||||
.filter((k) => features[k])
|
|
||||||
.includes(desideredFeature)
|
|
||||||
}
|
|
||||||
|
|
||||||
function boostrap(): FeaturesAPI {
|
|
||||||
const basis = {
|
|
||||||
isEnabled: () => false,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!commerceProviderConfig) {
|
|
||||||
console.log('No config.json found - Please add a config.json')
|
|
||||||
return basis
|
|
||||||
}
|
|
||||||
|
|
||||||
if (commerceProviderConfig.features) {
|
|
||||||
return {
|
|
||||||
...basis,
|
|
||||||
isEnabled: memo(isFeatureEnabled(commerceProviderConfig)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return basis
|
|
||||||
}
|
|
||||||
|
|
||||||
export default boostrap()
|
|
@ -2,13 +2,18 @@ import type { ConfigInterface } from 'swr'
|
|||||||
import type { CommerceError } from './errors'
|
import type { CommerceError } from './errors'
|
||||||
import type { ResponseState } from './use-data'
|
import type { ResponseState } from './use-data'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the properties in T with the properties in type K, overriding properties defined in T
|
||||||
|
*/
|
||||||
export type Override<T, K> = Omit<T, keyof K> & K
|
export type Override<T, K> = Omit<T, keyof K> & K
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the properties in T with the properties in type K changed from optional to required
|
* Returns the properties in T with the properties in type K changed from optional to required
|
||||||
*/
|
*/
|
||||||
export type PickRequired<T, K extends keyof T> = Omit<T, K> &
|
export type PickRequired<T, K extends keyof T> = Omit<T, K> &
|
||||||
Required<Pick<T, K>>
|
{
|
||||||
|
[P in K]-?: NonNullable<T[P]>
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core fetcher added by CommerceProvider
|
* Core fetcher added by CommerceProvider
|
||||||
@ -31,16 +36,15 @@ export type HookFetcher<Data, Input = null, Result = any> = (
|
|||||||
fetch: <T = Result, Body = any>(options: FetcherOptions<Body>) => Promise<T>
|
fetch: <T = Result, Body = any>(options: FetcherOptions<Body>) => Promise<T>
|
||||||
) => Data | Promise<Data>
|
) => Data | Promise<Data>
|
||||||
|
|
||||||
export type HookFetcherFn<
|
export type HookFetcherFn<Data, Input = undefined, Result = any, Body = any> = (
|
||||||
Data,
|
context: HookFetcherContext<Input, Result, Body>
|
||||||
Input = never,
|
) => Data | Promise<Data>
|
||||||
Result = any,
|
|
||||||
Body = any
|
export type HookFetcherContext<Input = undefined, Result = any, Body = any> = {
|
||||||
> = (context: {
|
|
||||||
options: HookFetcherOptions
|
options: HookFetcherOptions
|
||||||
input: Input
|
input: Input
|
||||||
fetch: <T = Result, B = Body>(options: FetcherOptions<B>) => Promise<T>
|
fetch: <T = Result, B = Body>(options: FetcherOptions<B>) => Promise<T>
|
||||||
}) => Data | Promise<Data>
|
}
|
||||||
|
|
||||||
export type HookFetcherOptions = { method?: string } & (
|
export type HookFetcherOptions = { method?: string } & (
|
||||||
| { query: string; url?: string }
|
| { query: string; url?: string }
|
||||||
@ -49,13 +53,20 @@ export type HookFetcherOptions = { method?: string } & (
|
|||||||
|
|
||||||
export type HookInputValue = string | number | boolean | undefined
|
export type HookInputValue = string | number | boolean | undefined
|
||||||
|
|
||||||
export type HookSwrInput = [string, HookInputValue][]
|
export type HookSWRInput = [string, HookInputValue][]
|
||||||
|
|
||||||
export type HookFetchInput = { [k: string]: HookInputValue }
|
export type HookFetchInput = { [k: string]: HookInputValue }
|
||||||
|
|
||||||
export type HookInput = {}
|
export type HookFunction<
|
||||||
|
Input extends { [k: string]: unknown } | null,
|
||||||
|
T
|
||||||
|
> = keyof Input extends never
|
||||||
|
? () => T
|
||||||
|
: Partial<Input> extends Input
|
||||||
|
? (input?: Input) => T
|
||||||
|
: (input: Input) => T
|
||||||
|
|
||||||
export type HookHandler<
|
export type SWRHook<
|
||||||
// Data obj returned by the hook and fetch operation
|
// Data obj returned by the hook and fetch operation
|
||||||
Data,
|
Data,
|
||||||
// Input expected by the hook
|
// Input expected by the hook
|
||||||
@ -65,58 +76,56 @@ export type HookHandler<
|
|||||||
// Custom state added to the response object of SWR
|
// Custom state added to the response object of SWR
|
||||||
State = {}
|
State = {}
|
||||||
> = {
|
> = {
|
||||||
useHook?(context: {
|
useHook(
|
||||||
input: Input & { swrOptions?: SwrOptions<Data, FetchInput> }
|
context: SWRHookContext<Data, FetchInput>
|
||||||
useData(context?: {
|
): HookFunction<
|
||||||
input?: HookFetchInput | HookSwrInput
|
Input & { swrOptions?: SwrOptions<Data, FetchInput> },
|
||||||
swrOptions?: SwrOptions<Data, FetchInput>
|
ResponseState<Data> & State
|
||||||
}): ResponseState<Data>
|
>
|
||||||
}): ResponseState<Data> & State
|
|
||||||
fetchOptions: HookFetcherOptions
|
fetchOptions: HookFetcherOptions
|
||||||
fetcher?: HookFetcherFn<Data, FetchInput>
|
fetcher?: HookFetcherFn<Data, FetchInput>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationHandler<
|
export type SWRHookContext<
|
||||||
|
Data,
|
||||||
|
FetchInput extends { [k: string]: unknown } = {}
|
||||||
|
> = {
|
||||||
|
useData(context?: {
|
||||||
|
input?: HookFetchInput | HookSWRInput
|
||||||
|
swrOptions?: SwrOptions<Data, FetchInput>
|
||||||
|
}): ResponseState<Data>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MutationHook<
|
||||||
// Data obj returned by the hook and fetch operation
|
// Data obj returned by the hook and fetch operation
|
||||||
Data,
|
Data,
|
||||||
// Input expected by the hook
|
// Input expected by the hook
|
||||||
Input extends { [k: string]: unknown } = {},
|
Input extends { [k: string]: unknown } = {},
|
||||||
|
// Input expected by the action returned by the hook
|
||||||
|
ActionInput extends { [k: string]: unknown } = {},
|
||||||
// Input expected before doing a fetch operation
|
// Input expected before doing a fetch operation
|
||||||
FetchInput extends { [k: string]: unknown } = {}
|
FetchInput extends { [k: string]: unknown } = ActionInput
|
||||||
> = {
|
> = {
|
||||||
useHook?(context: {
|
useHook(
|
||||||
input: Input
|
context: MutationHookContext<Data, FetchInput>
|
||||||
}): (context: {
|
): HookFunction<Input, HookFunction<ActionInput, Data | Promise<Data>>>
|
||||||
input: FetchInput
|
|
||||||
fetch: (context: { input: FetchInput }) => Data | Promise<Data>
|
|
||||||
}) => Data | Promise<Data>
|
|
||||||
fetchOptions: HookFetcherOptions
|
fetchOptions: HookFetcherOptions
|
||||||
fetcher?: HookFetcherFn<Data, FetchInput>
|
fetcher?: HookFetcherFn<Data, FetchInput>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MutationHookContext<
|
||||||
|
Data,
|
||||||
|
FetchInput extends { [k: string]: unknown } | null = {}
|
||||||
|
> = {
|
||||||
|
fetch: keyof FetchInput extends never
|
||||||
|
? () => Data | Promise<Data>
|
||||||
|
: Partial<FetchInput> extends FetchInput
|
||||||
|
? (context?: { input?: FetchInput }) => Data | Promise<Data>
|
||||||
|
: (context: { input: FetchInput }) => Data | Promise<Data>
|
||||||
|
}
|
||||||
|
|
||||||
export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<
|
export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<
|
||||||
Data,
|
Data,
|
||||||
CommerceError,
|
CommerceError,
|
||||||
HookFetcher<Data, Input, Result>
|
HookFetcher<Data, Input, Result>
|
||||||
>
|
>
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the property K from type T excluding nullables
|
|
||||||
*/
|
|
||||||
export type Prop<T, K extends keyof T> = NonNullable<T[K]>
|
|
||||||
|
|
||||||
export type HookHandlerType =
|
|
||||||
| HookHandler<any, any, any>
|
|
||||||
| MutationHandler<any, any, any>
|
|
||||||
|
|
||||||
export type UseHookParameters<H extends HookHandlerType> = Parameters<
|
|
||||||
Prop<H, 'useHook'>
|
|
||||||
>
|
|
||||||
|
|
||||||
export type UseHookResponse<H extends HookHandlerType> = ReturnType<
|
|
||||||
Prop<H, 'useHook'>
|
|
||||||
>
|
|
||||||
|
|
||||||
export type UseHookInput<
|
|
||||||
H extends HookHandlerType
|
|
||||||
> = UseHookParameters<H>[0]['input']
|
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
import { useCallback } from 'react'
|
|
||||||
import type { HookFetcher, HookFetcherOptions } from './types'
|
|
||||||
import { useCommerce } from '..'
|
|
||||||
|
|
||||||
export default function useAction<T, Input = null>(
|
|
||||||
options: HookFetcherOptions,
|
|
||||||
fetcher: HookFetcher<T, Input>
|
|
||||||
) {
|
|
||||||
const { fetcherRef } = useCommerce()
|
|
||||||
|
|
||||||
return useCallback(
|
|
||||||
(input: Input) => fetcher(options, input, fetcherRef.current),
|
|
||||||
[fetcher]
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,11 +1,11 @@
|
|||||||
import useSWR, { responseInterface } from 'swr'
|
import useSWR, { responseInterface } from 'swr'
|
||||||
import type {
|
import type {
|
||||||
HookHandler,
|
HookSWRInput,
|
||||||
HookSwrInput,
|
|
||||||
HookFetchInput,
|
HookFetchInput,
|
||||||
PickRequired,
|
|
||||||
Fetcher,
|
Fetcher,
|
||||||
SwrOptions,
|
SwrOptions,
|
||||||
|
HookFetcherOptions,
|
||||||
|
HookFetcherFn,
|
||||||
} from './types'
|
} from './types'
|
||||||
import defineProperty from './define-property'
|
import defineProperty from './define-property'
|
||||||
import { CommerceError } from './errors'
|
import { CommerceError } from './errors'
|
||||||
@ -14,13 +14,12 @@ export type ResponseState<Result> = responseInterface<Result, CommerceError> & {
|
|||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UseData = <
|
export type UseData = <Data = any, FetchInput extends HookFetchInput = {}>(
|
||||||
Data = any,
|
options: {
|
||||||
Input extends { [k: string]: unknown } = {},
|
fetchOptions: HookFetcherOptions
|
||||||
FetchInput extends HookFetchInput = {}
|
fetcher: HookFetcherFn<Data, FetchInput>
|
||||||
>(
|
},
|
||||||
options: PickRequired<HookHandler<Data, Input, FetchInput>, 'fetcher'>,
|
input: HookFetchInput | HookSWRInput,
|
||||||
input: HookFetchInput | HookSwrInput,
|
|
||||||
fetcherFn: Fetcher,
|
fetcherFn: Fetcher,
|
||||||
swrOptions?: SwrOptions<Data, FetchInput>
|
swrOptions?: SwrOptions<Data, FetchInput>
|
||||||
) => ResponseState<Data>
|
) => ResponseState<Data>
|
||||||
|
50
framework/commerce/utils/use-hook.ts
Normal file
50
framework/commerce/utils/use-hook.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
import { Provider, useCommerce } from '..'
|
||||||
|
import type { MutationHook, PickRequired, SWRHook } from './types'
|
||||||
|
import useData from './use-data'
|
||||||
|
|
||||||
|
export function useFetcher() {
|
||||||
|
const { providerRef, fetcherRef } = useCommerce()
|
||||||
|
return providerRef.current.fetcher ?? fetcherRef.current
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useHook<
|
||||||
|
P extends Provider,
|
||||||
|
H extends MutationHook<any, any, any> | SWRHook<any, any, any>
|
||||||
|
>(fn: (provider: P) => H) {
|
||||||
|
const { providerRef } = useCommerce<P>()
|
||||||
|
const provider = providerRef.current
|
||||||
|
return fn(provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSWRHook<H extends SWRHook<any, any, any>>(
|
||||||
|
hook: PickRequired<H, 'fetcher'>
|
||||||
|
) {
|
||||||
|
const fetcher = useFetcher()
|
||||||
|
|
||||||
|
return hook.useHook({
|
||||||
|
useData(ctx) {
|
||||||
|
const response = useData(hook, ctx?.input ?? [], fetcher, ctx?.swrOptions)
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMutationHook<H extends MutationHook<any, any, any>>(
|
||||||
|
hook: PickRequired<H, 'fetcher'>
|
||||||
|
) {
|
||||||
|
const fetcher = useFetcher()
|
||||||
|
|
||||||
|
return hook.useHook({
|
||||||
|
fetch: useCallback(
|
||||||
|
({ input } = {}) => {
|
||||||
|
return hook.fetcher({
|
||||||
|
input,
|
||||||
|
options: hook.fetchOptions,
|
||||||
|
fetch: fetcher,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[fetcher, hook.fetchOptions]
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
@ -1,40 +0,0 @@
|
|||||||
import { useMemo } from 'react'
|
|
||||||
import { responseInterface } from 'swr'
|
|
||||||
import { CommerceError } from './errors'
|
|
||||||
import { Override } from './types'
|
|
||||||
|
|
||||||
export type UseResponseOptions<
|
|
||||||
D,
|
|
||||||
R extends responseInterface<any, CommerceError>
|
|
||||||
> = {
|
|
||||||
descriptors?: PropertyDescriptorMap
|
|
||||||
normalizer?: (data: R['data']) => D
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UseResponse = <D, R extends responseInterface<any, CommerceError>>(
|
|
||||||
response: R,
|
|
||||||
options: UseResponseOptions<D, R>
|
|
||||||
) => D extends object ? Override<R, { data?: D }> : R
|
|
||||||
|
|
||||||
const useResponse: UseResponse = (response, { descriptors, normalizer }) => {
|
|
||||||
const memoizedResponse = useMemo(
|
|
||||||
() =>
|
|
||||||
Object.create(response, {
|
|
||||||
...descriptors,
|
|
||||||
...(normalizer
|
|
||||||
? {
|
|
||||||
data: {
|
|
||||||
get() {
|
|
||||||
return response.data && normalizer(response.data)
|
|
||||||
},
|
|
||||||
enumerable: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
}),
|
|
||||||
[response]
|
|
||||||
)
|
|
||||||
return memoizedResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useResponse
|
|
@ -1,12 +1,19 @@
|
|||||||
import useAction from '../utils/use-action'
|
import { useHook, useMutationHook } from '../utils/use-hook'
|
||||||
import type { CartItemBody } from '../types'
|
import { mutationFetcher } from '../utils/default-fetcher'
|
||||||
|
import type { MutationHook } from '../utils/types'
|
||||||
|
import type { Provider } from '..'
|
||||||
|
|
||||||
// Input expected by the action returned by the `useAddItem` hook
|
export type UseAddItem<
|
||||||
// export interface AddItemInput {
|
H extends MutationHook<any, any, any> = MutationHook<any, {}, {}>
|
||||||
// includeProducts?: boolean
|
> = ReturnType<H['useHook']>
|
||||||
// }
|
|
||||||
export type AddItemInput<T extends CartItemBody> = T
|
|
||||||
|
|
||||||
const useAddItem = useAction
|
export const fetcher = mutationFetcher
|
||||||
|
|
||||||
|
const fn = (provider: Provider) => provider.wishlist?.useAddItem!
|
||||||
|
|
||||||
|
const useAddItem: UseAddItem = (...args) => {
|
||||||
|
const hook = useHook(fn)
|
||||||
|
return useMutationHook({ fetcher, ...hook })(...args)
|
||||||
|
}
|
||||||
|
|
||||||
export default useAddItem
|
export default useAddItem
|
||||||
|
@ -1,5 +1,28 @@
|
|||||||
import useAction from '../utils/use-action'
|
import { useHook, useMutationHook } from '../utils/use-hook'
|
||||||
|
import { mutationFetcher } from '../utils/default-fetcher'
|
||||||
|
import type { HookFetcherFn, MutationHook } from '../utils/types'
|
||||||
|
import type { Provider } from '..'
|
||||||
|
|
||||||
const useRemoveItem = useAction
|
export type RemoveItemInput = {
|
||||||
|
id: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseRemoveItem<
|
||||||
|
H extends MutationHook<any, any, any> = MutationHook<
|
||||||
|
any | null,
|
||||||
|
{ wishlist?: any },
|
||||||
|
RemoveItemInput,
|
||||||
|
{}
|
||||||
|
>
|
||||||
|
> = ReturnType<H['useHook']>
|
||||||
|
|
||||||
|
export const fetcher: HookFetcherFn<any | null, {}> = mutationFetcher
|
||||||
|
|
||||||
|
const fn = (provider: Provider) => provider.wishlist?.useRemoveItem!
|
||||||
|
|
||||||
|
const useRemoveItem: UseRemoveItem = (input) => {
|
||||||
|
const hook = useHook(fn)
|
||||||
|
return useMutationHook({ fetcher, ...hook })(input)
|
||||||
|
}
|
||||||
|
|
||||||
export default useRemoveItem
|
export default useRemoveItem
|
||||||
|
@ -1,56 +1,25 @@
|
|||||||
|
import { useHook, useSWRHook } from '../utils/use-hook'
|
||||||
|
import { SWRFetcher } from '../utils/default-fetcher'
|
||||||
|
import type { HookFetcherFn, SWRHook } from '../utils/types'
|
||||||
import type { Wishlist } from '../types'
|
import type { Wishlist } from '../types'
|
||||||
import type {
|
import type { Provider } from '..'
|
||||||
Prop,
|
|
||||||
HookFetcherFn,
|
|
||||||
UseHookInput,
|
|
||||||
UseHookResponse,
|
|
||||||
} from '../utils/types'
|
|
||||||
import defaultFetcher from '../utils/default-fetcher'
|
|
||||||
import useData from '../utils/use-data'
|
|
||||||
import { Provider, useCommerce } from '..'
|
|
||||||
|
|
||||||
export type UseWishlistHandler<P extends Provider> = Prop<
|
export type UseWishlist<
|
||||||
Prop<P, 'wishlist'>,
|
H extends SWRHook<any, any, any> = SWRHook<
|
||||||
'useWishlist'
|
Wishlist | null,
|
||||||
>
|
{ includeProducts?: boolean },
|
||||||
|
{ customerId?: number; includeProducts: boolean },
|
||||||
|
{ isEmpty?: boolean }
|
||||||
|
>
|
||||||
|
> = ReturnType<H['useHook']>
|
||||||
|
|
||||||
export type UseWishlistInput<P extends Provider> = UseHookInput<
|
export const fetcher: HookFetcherFn<Wishlist | null, any> = SWRFetcher
|
||||||
UseWishlistHandler<P>
|
|
||||||
>
|
|
||||||
|
|
||||||
export type WishlistResponse<P extends Provider> = UseHookResponse<
|
const fn = (provider: Provider) => provider.wishlist?.useWishlist!
|
||||||
UseWishlistHandler<P>
|
|
||||||
>
|
|
||||||
|
|
||||||
export type UseWishlist<P extends Provider> = Partial<
|
const useWishlist: UseWishlist = (input) => {
|
||||||
UseWishlistInput<P>
|
const hook = useHook(fn)
|
||||||
> extends UseWishlistInput<P>
|
return useSWRHook({ fetcher, ...hook })(input)
|
||||||
? (input?: UseWishlistInput<P>) => WishlistResponse<P>
|
|
||||||
: (input: UseWishlistInput<P>) => WishlistResponse<P>
|
|
||||||
|
|
||||||
export const fetcher = defaultFetcher as HookFetcherFn<Wishlist | null>
|
|
||||||
|
|
||||||
export default function useWishlist<P extends Provider>(
|
|
||||||
input: UseWishlistInput<P> = {}
|
|
||||||
) {
|
|
||||||
const { providerRef, fetcherRef } = useCommerce<P>()
|
|
||||||
|
|
||||||
const provider = providerRef.current
|
|
||||||
const opts = provider.wishlist?.useWishlist
|
|
||||||
|
|
||||||
const fetcherFn = opts?.fetcher ?? fetcher
|
|
||||||
const useHook = opts?.useHook ?? ((ctx) => ctx.useData())
|
|
||||||
|
|
||||||
return useHook({
|
|
||||||
input,
|
|
||||||
useData(ctx) {
|
|
||||||
const response = useData(
|
|
||||||
{ ...opts!, fetcher: fetcherFn },
|
|
||||||
ctx?.input ?? [],
|
|
||||||
provider.fetcher ?? fetcherRef.current,
|
|
||||||
ctx?.swrOptions ?? input.swrOptions
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default useWishlist
|
||||||
|
4
framework/shopify/.env.template
Normal file
4
framework/shopify/.env.template
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
COMMERCE_PROVIDER=shopify
|
||||||
|
|
||||||
|
NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=
|
||||||
|
NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=
|
123
framework/shopify/README.md
Normal file
123
framework/shopify/README.md
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
## Shopify Provider
|
||||||
|
|
||||||
|
**Demo:** https://shopify.demo.vercel.store/
|
||||||
|
|
||||||
|
Before getting starter, a [Shopify](https://www.shopify.com/) account and store is required before using the provider.
|
||||||
|
|
||||||
|
Next, copy the `.env.template` file in this directory to `.env.local` in the main directory (which will be ignored by Git):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp framework/shopify/.env.template .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, set the environment variables in `.env.local` to match the ones from your store.
|
||||||
|
|
||||||
|
## Contribute
|
||||||
|
|
||||||
|
Our commitment to Open Source can be found [here](https://vercel.com/oss).
|
||||||
|
|
||||||
|
If you find an issue with the provider or want a new feature, feel free to open a PR or [create a new issue](https://github.com/vercel/commerce/issues).
|
||||||
|
|
||||||
|
## Modifications
|
||||||
|
|
||||||
|
These modifications are temporarily until contributions are made to remove them.
|
||||||
|
|
||||||
|
### Adding item to Cart
|
||||||
|
|
||||||
|
```js
|
||||||
|
// components/product/ProductView/ProductView.tsx
|
||||||
|
const ProductView: FC<Props> = ({ product }) => {
|
||||||
|
const addToCart = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
await addItem({
|
||||||
|
productId: product.id,
|
||||||
|
variantId: variant ? variant.id : product.variants[0].id,
|
||||||
|
})
|
||||||
|
openSidebar()
|
||||||
|
setLoading(false)
|
||||||
|
} catch (err) {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Proceed to Checkout
|
||||||
|
|
||||||
|
```js
|
||||||
|
// components/cart/CartSidebarView/CartSidebarView.tsx
|
||||||
|
import { useCommerce } from '@framework'
|
||||||
|
|
||||||
|
const CartSidebarView: FC = () => {
|
||||||
|
const { checkout } = useCommerce()
|
||||||
|
return (
|
||||||
|
<Button href={checkout.webUrl} Component="a" width="100%">
|
||||||
|
Proceed to Checkout
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## APIs
|
||||||
|
|
||||||
|
Collections of APIs to fetch data from a Shopify store.
|
||||||
|
|
||||||
|
The data is fetched using the [Shopify JavaScript Buy SDK](https://github.com/Shopify/js-buy-sdk#readme). Read the [Shopify Storefront API reference](https://shopify.dev/docs/storefront-api/reference) for more information.
|
||||||
|
|
||||||
|
### getProduct
|
||||||
|
|
||||||
|
Get a single product by its `handle`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import getProduct from '@framework/product/get-product'
|
||||||
|
import { getConfig } from '@framework/api'
|
||||||
|
|
||||||
|
const config = getConfig()
|
||||||
|
|
||||||
|
const product = await getProduct({
|
||||||
|
variables: { slug },
|
||||||
|
config,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### getAllProducts
|
||||||
|
|
||||||
|
```js
|
||||||
|
import getAllProducts from '@framework/product/get-all-products'
|
||||||
|
import { getConfig } from '@framework/api'
|
||||||
|
|
||||||
|
const config = getConfig()
|
||||||
|
|
||||||
|
const { products } = await getAllProducts({
|
||||||
|
variables: { first: 12 },
|
||||||
|
config,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### getAllCollections
|
||||||
|
|
||||||
|
```js
|
||||||
|
import getAllCollections from '@framework/product/get-all-collections'
|
||||||
|
import { getConfig } from '@framework/api'
|
||||||
|
|
||||||
|
const config = getConfig()
|
||||||
|
|
||||||
|
const collections = await getAllCollections({
|
||||||
|
config,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### getAllPages
|
||||||
|
|
||||||
|
```js
|
||||||
|
import getAllPages from '@framework/common/get-all-pages'
|
||||||
|
import { getConfig } from '@framework/api'
|
||||||
|
|
||||||
|
const config = getConfig()
|
||||||
|
|
||||||
|
const pages = await getAllPages({
|
||||||
|
variables: { first: 12 },
|
||||||
|
config,
|
||||||
|
})
|
||||||
|
```
|
1
framework/shopify/api/cart/index.ts
Normal file
1
framework/shopify/api/cart/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function () {}
|
1
framework/shopify/api/catalog/index.ts
Normal file
1
framework/shopify/api/catalog/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function () {}
|
1
framework/shopify/api/catalog/products.ts
Normal file
1
framework/shopify/api/catalog/products.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function () {}
|
46
framework/shopify/api/checkout/index.ts
Normal file
46
framework/shopify/api/checkout/index.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import isAllowedMethod from '../utils/is-allowed-method'
|
||||||
|
import createApiHandler, {
|
||||||
|
ShopifyApiHandler,
|
||||||
|
} from '../utils/create-api-handler'
|
||||||
|
|
||||||
|
import {
|
||||||
|
SHOPIFY_CHECKOUT_ID_COOKIE,
|
||||||
|
SHOPIFY_CHECKOUT_URL_COOKIE,
|
||||||
|
SHOPIFY_CUSTOMER_TOKEN_COOKIE,
|
||||||
|
} from '../../const'
|
||||||
|
|
||||||
|
import { getConfig } from '..'
|
||||||
|
import associateCustomerWithCheckoutMutation from '../../utils/mutations/associate-customer-with-checkout'
|
||||||
|
|
||||||
|
const METHODS = ['GET']
|
||||||
|
|
||||||
|
const checkoutApi: ShopifyApiHandler<any> = async (req, res, config) => {
|
||||||
|
if (!isAllowedMethod(req, res, METHODS)) return
|
||||||
|
|
||||||
|
config = getConfig()
|
||||||
|
|
||||||
|
const { cookies } = req
|
||||||
|
const checkoutUrl = cookies[SHOPIFY_CHECKOUT_URL_COOKIE]
|
||||||
|
const customerCookie = cookies[SHOPIFY_CUSTOMER_TOKEN_COOKIE]
|
||||||
|
|
||||||
|
if (customerCookie) {
|
||||||
|
try {
|
||||||
|
await config.fetch(associateCustomerWithCheckoutMutation, {
|
||||||
|
variables: {
|
||||||
|
checkoutId: cookies[SHOPIFY_CHECKOUT_ID_COOKIE],
|
||||||
|
customerAccessToken: cookies[SHOPIFY_CUSTOMER_TOKEN_COOKIE],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkoutUrl) {
|
||||||
|
res.redirect(checkoutUrl)
|
||||||
|
} else {
|
||||||
|
res.redirect('/cart')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createApiHandler(checkoutApi, {}, {})
|
1
framework/shopify/api/customer.ts
Normal file
1
framework/shopify/api/customer.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function () {}
|
1
framework/shopify/api/customers/index.ts
Normal file
1
framework/shopify/api/customers/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function () {}
|
1
framework/shopify/api/customers/login.ts
Normal file
1
framework/shopify/api/customers/login.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function () {}
|
1
framework/shopify/api/customers/logout.ts
Normal file
1
framework/shopify/api/customers/logout.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function () {}
|
1
framework/shopify/api/customers/signup.ts
Normal file
1
framework/shopify/api/customers/signup.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function () {}
|
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