mirror of
https://github.com/vercel/commerce.git
synced 2025-03-14 14:42:31 +00:00
Agnostic UI (#199)
* changes * Progress * Normalized Products output * Progress * Restored Index Agnostic * Progress * Reordering * Moved normalizer to BC function * Removed Futures * More Types * More Types * More Types * Fix useCallback * Progress, Changes types, readme and restoring functionality * Changes * TS Issues * Changes * Normalizer * Normalizing more operations * Normalizing more operations * changes * Merge Issues * Cleanup * change * changes * index.ts broke my tree shaking * slug * Normalized Options and Swatches * Restored Add to cart * Correct Variant Added to Cart * Normalizing Cart Responses * Changes * changes breaking * Adding immutable normalizer for Product * Cart Normalized * changes * Progress * More updates * Removed some comments * Add loading state for data hooks * Bug fix * Changed the way isEmpty works * Improve navbar performance * Added useResponse hook * added useResponse to useWhishlist * Added husky and lint-staged * Ran prettier fix * Added more cart types * Moved types.d.ts to the commerce folder * Minor changes * Moved normalizer to happen after fetch * updated useCart types * Updated normalizer for useData * Added new normalizer for the cart to the UI * More corrections for useCart * Updated cart update hooks * Removed import * Progress * Switch away from global types * Making multiple changes * Improved types for operations * Moved and updated cart types * Updated the useAddItem and useRemoveItem hooks * Minor life improvement * Minor change * Implement Shopify Provider * Update README.md * Update README.md * normalizations & missing files * Update index.ts * fixes * Update normalize.ts * fix: cart error on first load * shopify checkout redirect & api handler * Update get-checkout-id.ts * userAvatar * Fix: color option * Update normalize.ts * changes * Update next.config.js * start customer auth & signup * Update config.ts * Login, Sign Up, Log Out, and checkout & customer association * Automatic login after sign-up * Update handle-login.ts * MOving stuff around and adding temporal new files * changes * Replace use-cart with the new hook * Removed old hook * Improved HookHandler type * Moved types * Simplified useData types * Updated Fetcher type * Moved SwrOptions type * Removed duplicated fetcher * Moved provider to its own file * Added proper type for fetch input * Revert "Merge branch 'agnostic' of https://github.com/vercel/commerce into agnostic" This reverts commit 23c8ed7c2d48d30e74ad94216f9910650fadf30c, reversing changes made to bf50965a39ef0b1b956461ebe62070809fbe1d63. * change readme * Revert "Merge branch 'master' of https://github.com/vercel/commerce into agnostic" This reverts commit bf50965a39ef0b1b956461ebe62070809fbe1d63, reversing changes made to 0dad4ddedbf0bff2d0b5800ca469fda0073889ea. * Revert "Revert "Merge branch 'agnostic' of https://github.com/vercel/commerce into agnostic"" This reverts commit c9a43f1bce0572d0eff41f3af893be8bdb00bedd. * align with upstream changes * Updated how the hook input is handled * Add more options to the hook handler * Final touches to the hook handler type * Moved useWishlist to use new handler * Move useCustomer to the new hook * Added a default fetcher * query all products for vendors & paths, improve search * Update use-search.tsx * fix cart after upstream changes * Shopify Provider (#186) * Start of Shopify provider * add missing comment to documentation * add missing env vars to documentation * update reference to types file * Moved useSearch to the new hook * Removed old use-data lib * Removed generics for result and body * Removed normalizr * Wishlist * New changes and initial Features API * Fixed some types * Fixed more types * fixes after upstream changes * Fixed product types * Fixed another product type * Updated type * Fixed remaining issues with types * Added a MutationHandler * Moved the handlers to each hook * Moved the fetcher to its own file * Moved handler to each hook * Added initial version of useAddItem * Added better mutation types, and moved some hooks * Removed use-cart-actions * Added initial version of useAddItem * Updated types * Update use-add-item.tsx * changes * Changes * Reordering and changes * Adding Features APO * Adding wishlist api * Implementing FeaturesAPI with Wishlist * Removing bug with touchstart * Adding tyni typing * moved use-remove-item * Removed MutationHandler type * Moved more hooks and updated types to make them smaller * Moved data hooks to new format * Removed no longer required types * Removed useResponse helper * Updated useData type * Moved wishlist use-add-item * Moved wishlist use-remove-item to provider * Moved use-login and use-logout * Update use-signup * Removed use-action helper * Moved auth & cart hooks + several fixes * Updated cart item, fixed deprecations * Update next.config.js * Updates to wishlist feature * Moved the features to be environment variable only * More changes for wishlist config * Disable wishlist * Removed useWishlistActions * Updated readme * updates * typos * Updated the way the provider config is set * Removed features.ts * Removed bootstrap.js * Aligned with upstream changes * Updates * shopify: changes * shopify: changes * Update next.config.js * Shopify Provider Updates (#209) * Implement Shopify Provider * Update README.md * Update README.md * normalizations & missing files * Update index.ts * fixes * Update normalize.ts * fix: cart error on first load * shopify checkout redirect & api handler * Update get-checkout-id.ts * Fix: color option * Update normalize.ts * changes * Update next.config.js * start customer auth & signup * Update config.ts * Login, Sign Up, Log Out, and checkout & customer association * Automatic login after sign-up * Update handle-login.ts * changes * Revert "Merge branch 'agnostic' of https://github.com/vercel/commerce into agnostic" This reverts commit 23c8ed7c2d48d30e74ad94216f9910650fadf30c, reversing changes made to bf50965a39ef0b1b956461ebe62070809fbe1d63. * change readme * Revert "Merge branch 'master' of https://github.com/vercel/commerce into agnostic" This reverts commit bf50965a39ef0b1b956461ebe62070809fbe1d63, reversing changes made to 0dad4ddedbf0bff2d0b5800ca469fda0073889ea. * Revert "Revert "Merge branch 'agnostic' of https://github.com/vercel/commerce into agnostic"" This reverts commit c9a43f1bce0572d0eff41f3af893be8bdb00bedd. * align with upstream changes * query all products for vendors & paths, improve search * Update use-search.tsx * fix cart after upstream changes * fixes after upstream changes * Moved handler to each hook * Added initial version of useAddItem * Updated types * Update use-add-item.tsx * Moved auth & cart hooks + several fixes * Updated cart item, fixed deprecations * Update next.config.js * Aligned with upstream changes * Updates * Update next.config.js * Updated the commerce config structure * Removed @framework imports within framework providers * Fixed types * changes * Adding extra config * Adding shopify commit * Adding env templates to the providers * Ignore some types * Adding link for Cart * Adding customCheckout * multiple changes to fix the wishlist * Shopify Provier Updates (#212) * changes * Adding shopify commit * Changed to query page by id * Fixed page query, Changed use-search GraphQl query * Update use-search.tsx * remove unused util * Changed cookie expiration * Update tsconfig.json Co-authored-by: okbel <curciobel@gmail.com> * Bump and adding dependency * Adding color checks * Now colors work with lighter colors * Stable commerce.config.json * Updated main readme * Readme changes * Default to bigcommerce Co-authored-by: bc <bc@bcs-MacBook-Pro.fibertel.com.ar> Co-authored-by: Luis Alvarez <luis@vercel.com> Co-authored-by: cond0r <pinte_catalin@yahoo.com> Co-authored-by: Peter Mekhaeil <4616064+petermekhaeil@users.noreply.github.com>
This commit is contained in:
parent
b121078151
commit
9b71bd77fc
@ -2,4 +2,8 @@ BIGCOMMERCE_STOREFRONT_API_URL=
|
||||
BIGCOMMERCE_STOREFRONT_API_TOKEN=
|
||||
BIGCOMMERCE_STORE_API_URL=
|
||||
BIGCOMMERCE_STORE_API_TOKEN=
|
||||
BIGCOMMERCE_STORE_API_CLIENT_ID=
|
||||
BIGCOMMERCE_STORE_API_CLIENT_ID=
|
||||
BIGCOMMERCE_CHANNEL_ID=
|
||||
|
||||
SHOPIFY_STORE_DOMAIN=
|
||||
SHOPIFY_STOREFRONT_ACCESS_TOKEN=
|
||||
|
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"]
|
||||
}
|
149
README.md
149
README.md
@ -7,7 +7,8 @@ Start right now at [nextjs.org/commerce](https://nextjs.org/commerce)
|
||||
|
||||
Demo live at: [demo.vercel.store](https://demo.vercel.store/)
|
||||
|
||||
This project is currently <b>under development</b>.
|
||||
- Shopify Demo: https://shopify.demo.vercel.store/
|
||||
- BigCommerce Demo: https://bigcommerce.demo.vercel.store/
|
||||
|
||||
## Features
|
||||
|
||||
@ -21,26 +22,122 @@ This project is currently <b>under development</b>.
|
||||
- Integrations - Integrate seamlessly with the most common ecommerce platforms.
|
||||
- Dark Mode Support
|
||||
|
||||
## Integrations
|
||||
|
||||
Next.js Commerce integrates out-of-the-box with BigCommerce and Shopify. We plan to support all major ecommerce backends.
|
||||
|
||||
## Considerations
|
||||
|
||||
- `framework/commerce` contains all types, helpers and functions to be used as base to build a new **provider**.
|
||||
- **Providers** live under `framework`'s root folder and they will extend Next.js Commerce types and functionality.
|
||||
- **Features API** is 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.
|
||||
- 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`
|
||||
- We recommend that each **provider** ships with an `env.template` file and a `[readme.md](http://readme.md)` file.
|
||||
|
||||
## Provider Structure
|
||||
|
||||
Next.js Commerce provides a set of utilities and functions to create new providers. This is how a provider structure looks like.
|
||||
|
||||
- `product`
|
||||
- usePrice
|
||||
- useSearch
|
||||
- getProduct
|
||||
- getAllProducts
|
||||
- `wishlist`
|
||||
- useWishlist
|
||||
- useAddItem
|
||||
- useRemoveItem
|
||||
- `auth`
|
||||
- useLogin
|
||||
- useLogout
|
||||
- useSignup
|
||||
- `customer`
|
||||
- useCustomer
|
||||
- getCustomerId
|
||||
- getCustomerWistlist
|
||||
- `cart`
|
||||
- useCart
|
||||
- useAddItem
|
||||
- useRemoveItem
|
||||
- useUpdateItem
|
||||
- `env.template`
|
||||
- `provider.ts`
|
||||
- `commerce.config.json`
|
||||
- `next.config.js`
|
||||
- `README.md`
|
||||
|
||||
## Configuration
|
||||
|
||||
### How to change providers
|
||||
|
||||
First, update the provider selected in `commerce.config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"provider": "bigcommerce",
|
||||
"features": {
|
||||
"wishlist": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then, change the paths defined in `tsconfig.json` and update the `@framework` paths to point to the right folder provider:
|
||||
|
||||
```json
|
||||
"@framework": ["framework/bigcommerce"],
|
||||
"@framework/*": ["framework/bigcommerce/*"]
|
||||
```
|
||||
|
||||
Make sure to add the environment variables required by the new provider.
|
||||
|
||||
### Features
|
||||
|
||||
Every provider defines the features that it supports under `framework/{provider}/commerce.config.json`
|
||||
|
||||
#### 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
|
||||
{
|
||||
"provider": "bigcommerce",
|
||||
"features": {
|
||||
"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
|
||||
|
||||
We'd recommend to duplicate a provider folder and push your providers SDK.
|
||||
|
||||
If you succeeded building a provider, submit a PR so we can all enjoy it.
|
||||
|
||||
## 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
|
||||
Next.js Commerce integrates out-of-the-box with BigCommerce. We plan to support all major ecommerce backends.
|
||||
|
||||
|
||||
## Goals
|
||||
|
||||
* **Next.js Commerce** should have a completely data **agnostic** UI
|
||||
* **Aware of schema**: should ship with the right data schemas and types.
|
||||
* All providers should return the right data types and schemas to blend correctly with Next.js Commerce.
|
||||
* `@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._
|
||||
|
||||
There is a `framework` folder in the root folder that will contain multiple ecommerce providers.
|
||||
|
||||
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.
|
||||
|
||||
People actively working on this project: @okbel & @lfades.
|
||||
|
||||
## Contribute
|
||||
|
||||
Our commitment to Open Source can be found [here](https://vercel.com/oss).
|
||||
|
||||
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
||||
2. Create a new branch `git checkout -b MY_BRANCH_NAME`
|
||||
3. Install yarn: `npm install -g yarn`
|
||||
4. Install the dependencies: `yarn`
|
||||
5. Duplicate `.env.template` and rename it to `.env.local`.
|
||||
6. Add proper store values to `.env.local`.
|
||||
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`.
|
||||
|
||||
## Troubleshoot
|
||||
|
||||
<details>
|
||||
@ -57,6 +154,7 @@ BIGCOMMERCE_STOREFRONT_API_TOKEN=<>
|
||||
BIGCOMMERCE_STORE_API_URL=<>
|
||||
BIGCOMMERCE_STORE_API_TOKEN=<>
|
||||
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.
|
||||
@ -77,22 +175,3 @@ After Email confirmation, Checkout should be manually enabled through BigCommerc
|
||||
<br>
|
||||
BigCommerce team has been notified and they plan to add more detailed about this subject.
|
||||
</details>
|
||||
|
||||
## Contribute
|
||||
|
||||
Our commitment to Open Source can be found [here](https://vercel.com/oss).
|
||||
|
||||
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
||||
2. Create a new branch `git checkout -b MY_BRANCH_NAME`
|
||||
3. Install yarn: `npm install -g yarn`
|
||||
4. Install the dependencies: `yarn`
|
||||
5. Duplicate `.env.template` and rename it to `.env.local`.
|
||||
6. Add proper store values to `.env.local`.
|
||||
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`.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
7
commerce.config.json
Normal file
7
commerce.config.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"provider": "bigcommerce",
|
||||
"features": {
|
||||
"wishlist": true,
|
||||
"customCheckout": false
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { FC, useEffect, useState, useCallback } from 'react'
|
||||
import { Logo, Button, Input } from '@components/ui'
|
||||
import useLogin from '@framework/use-login'
|
||||
import useLogin from '@framework/auth/use-login'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import { validate } from 'email-validator'
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { validate } from 'email-validator'
|
||||
import { Info } from '@components/icons'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import { Logo, Button, Input } from '@components/ui'
|
||||
import useSignup from '@framework/use-signup'
|
||||
import useSignup from '@framework/auth/use-signup'
|
||||
|
||||
interface Props {}
|
||||
|
||||
|
@ -2,43 +2,51 @@ import { ChangeEvent, useEffect, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import s from './CartItem.module.css'
|
||||
import { Trash, Plus, Minus } from '@components/icons'
|
||||
import usePrice from '@framework/use-price'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import type { LineItem } from '@framework/types'
|
||||
import usePrice from '@framework/product/use-price'
|
||||
import useUpdateItem from '@framework/cart/use-update-item'
|
||||
import useRemoveItem from '@framework/cart/use-remove-item'
|
||||
import s from './CartItem.module.css'
|
||||
|
||||
type ItemOption = {
|
||||
name: string,
|
||||
nameId: number,
|
||||
value: string,
|
||||
name: string
|
||||
nameId: number
|
||||
value: string
|
||||
valueId: number
|
||||
}
|
||||
|
||||
const CartItem = ({
|
||||
item,
|
||||
currencyCode,
|
||||
...rest
|
||||
}: {
|
||||
item: any
|
||||
item: LineItem
|
||||
currencyCode: string
|
||||
}) => {
|
||||
const { closeSidebarIfPresent } = useUI()
|
||||
|
||||
const { price } = usePrice({
|
||||
amount: item.extended_sale_price,
|
||||
baseAmount: item.extended_list_price,
|
||||
amount: item.variant.price * item.quantity,
|
||||
baseAmount: item.variant.listPrice * item.quantity,
|
||||
currencyCode,
|
||||
})
|
||||
const updateItem = useUpdateItem(item)
|
||||
|
||||
const updateItem = useUpdateItem({ item })
|
||||
const removeItem = useRemoveItem()
|
||||
const [quantity, setQuantity] = useState(item.quantity)
|
||||
const [removing, setRemoving] = useState(false)
|
||||
|
||||
const updateQuantity = async (val: number) => {
|
||||
await updateItem({ quantity: val })
|
||||
}
|
||||
|
||||
const handleQuantity = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const val = Number(e.target.value)
|
||||
|
||||
if (Number.isInteger(val) && val >= 0) {
|
||||
setQuantity(e.target.value)
|
||||
setQuantity(Number(e.target.value))
|
||||
}
|
||||
}
|
||||
const handleBlur = () => {
|
||||
@ -62,11 +70,13 @@ const CartItem = ({
|
||||
try {
|
||||
// If this action succeeds then there's no need to do `setRemoving(true)`
|
||||
// because the component will be removed from the view
|
||||
await removeItem({ id: item.id })
|
||||
await removeItem(item)
|
||||
} catch (error) {
|
||||
setRemoving(false)
|
||||
}
|
||||
}
|
||||
// TODO: Add a type for this
|
||||
const options = (item as any).options
|
||||
|
||||
useEffect(() => {
|
||||
// Reset the quantity state if the item quantity changes
|
||||
@ -80,32 +90,38 @@ const CartItem = ({
|
||||
className={cn('flex flex-row space-x-8 py-8', {
|
||||
'opacity-75 pointer-events-none': removing,
|
||||
})}
|
||||
{...rest}
|
||||
>
|
||||
<div className="w-16 h-16 bg-violet relative overflow-hidden">
|
||||
<Image
|
||||
className={s.productImage}
|
||||
src={item.image_url}
|
||||
width={150}
|
||||
height={150}
|
||||
alt="Product Image"
|
||||
// The cart item image is already optimized and very small in size
|
||||
src={item.variant.image!.url}
|
||||
alt={item.variant.image!.altText}
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col text-base">
|
||||
{/** TODO: Replace this. No `path` found at Cart */}
|
||||
<Link href={`/product/${item.url.split('/')[3]}`}>
|
||||
<span className="font-bold text-lg cursor-pointer leading-6">
|
||||
<Link href={`/product/${item.path}`}>
|
||||
<span
|
||||
className="font-bold text-lg cursor-pointer leading-6"
|
||||
onClick={() => closeSidebarIfPresent()}
|
||||
>
|
||||
{item.name}
|
||||
</span>
|
||||
</Link>
|
||||
{item.options && item.options.length > 0 ? (
|
||||
{options && options.length > 0 ? (
|
||||
<div className="">
|
||||
{item.options.map((option:ItemOption, i: number) =>
|
||||
<span key={`${item.id}-${option.name}`} className="text-sm font-semibold text-accents-7">
|
||||
{option.value}{ i === item.options.length -1 ? "" : ", " }
|
||||
{options.map((option: ItemOption, i: number) => (
|
||||
<span
|
||||
key={`${item.id}-${option.name}`}
|
||||
className="text-sm font-semibold text-accents-7"
|
||||
>
|
||||
{option.value}
|
||||
{i === options.length - 1 ? '' : ', '}
|
||||
</span>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex items-center mt-3">
|
||||
@ -130,7 +146,10 @@ const CartItem = ({
|
||||
</div>
|
||||
<div className="flex flex-col justify-between space-y-2 text-base">
|
||||
<span>{price}</span>
|
||||
<button className="flex justify-end" onClick={handleRemove}>
|
||||
<button
|
||||
className="flex justify-end outline-none"
|
||||
onClick={handleRemove}
|
||||
>
|
||||
<Trash />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -1,42 +1,40 @@
|
||||
import { FC } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { UserNav } from '@components/common'
|
||||
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/use-price'
|
||||
import Link from 'next/link'
|
||||
import CartItem from '../CartItem'
|
||||
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 = () => {
|
||||
const { closeSidebar } = useUI()
|
||||
const { data, isEmpty } = useCart()
|
||||
const { data, isLoading, isEmpty } = useCart()
|
||||
|
||||
const { price: subTotal } = usePrice(
|
||||
data && {
|
||||
amount: data.base_amount,
|
||||
amount: Number(data.subtotalPrice),
|
||||
currencyCode: data.currency.code,
|
||||
}
|
||||
)
|
||||
const { price: total } = usePrice(
|
||||
data && {
|
||||
amount: data.cart_amount,
|
||||
amount: Number(data.totalPrice),
|
||||
currencyCode: data.currency.code,
|
||||
}
|
||||
)
|
||||
const handleClose = () => closeSidebar()
|
||||
|
||||
const items = data?.line_items.physical_items ?? []
|
||||
|
||||
const error = null
|
||||
const success = null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(s.root, {
|
||||
[s.empty]: error,
|
||||
[s.empty]: success,
|
||||
[s.empty]: isEmpty,
|
||||
[s.empty]: error || success || isLoading || isEmpty,
|
||||
})}
|
||||
>
|
||||
<header className="px-4 pt-6 pb-4 sm:px-6">
|
||||
@ -51,12 +49,12 @@ const CartSidebarView: FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<UserNav className="" />
|
||||
<UserNav />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{isEmpty ? (
|
||||
{isLoading || isEmpty ? (
|
||||
<div className="flex-1 px-4 flex flex-col justify-center items-center">
|
||||
<span className="border border-dashed border-primary rounded-full flex items-center justify-center w-16 h-16 p-12 bg-secondary text-secondary">
|
||||
<Bag className="absolute" />
|
||||
@ -90,15 +88,20 @@ const CartSidebarView: FC = () => {
|
||||
) : (
|
||||
<>
|
||||
<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">
|
||||
My Cart
|
||||
</h2>
|
||||
<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
|
||||
</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">
|
||||
{items.map((item: any) => (
|
||||
{data!.lineItems.map((item: any) => (
|
||||
<CartItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
currencyCode={data?.currency.code!}
|
||||
currencyCode={data!.currency.code}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
@ -2,7 +2,7 @@ import { FC } from 'react'
|
||||
import cn from 'classnames'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import type { Page } from '@framework/api/operations/get-all-pages'
|
||||
import type { Page } from '@framework/common/get-all-pages'
|
||||
import getSlug from '@lib/get-slug'
|
||||
import { Github, Vercel } from '@components/icons'
|
||||
import { Logo, Container } from '@components/ui'
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import type { Product } from '@commerce/types'
|
||||
import { Grid } from '@components/ui'
|
||||
import { ProductCard } from '@components/product'
|
||||
import s from './HomeAllProductsGrid.module.css'
|
||||
@ -8,10 +9,14 @@ import { getCategoryPath, getDesignerPath } from '@lib/search'
|
||||
interface Props {
|
||||
categories?: any
|
||||
brands?: any
|
||||
newestProducts?: any
|
||||
products?: Product[]
|
||||
}
|
||||
|
||||
const Head: FC<Props> = ({ categories, brands, newestProducts }) => {
|
||||
const HomeAllProductsGrid: FC<Props> = ({
|
||||
categories,
|
||||
brands,
|
||||
products = [],
|
||||
}) => {
|
||||
return (
|
||||
<div className={s.root}>
|
||||
<div className={s.asideWrapper}>
|
||||
@ -48,13 +53,15 @@ const Head: FC<Props> = ({ categories, brands, newestProducts }) => {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Grid layout="normal">
|
||||
{newestProducts.map(({ node }: any) => (
|
||||
{products.map((product) => (
|
||||
<ProductCard
|
||||
key={node.path}
|
||||
product={node}
|
||||
key={product.path}
|
||||
product={product}
|
||||
variant="simple"
|
||||
imgWidth={480}
|
||||
imgHeight={480}
|
||||
imgProps={{
|
||||
width: 480,
|
||||
height: 480,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
@ -63,4 +70,4 @@ const Head: FC<Props> = ({ categories, brands, newestProducts }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default Head
|
||||
export default HomeAllProductsGrid
|
||||
|
@ -43,7 +43,7 @@ const I18nWidget: FC = () => {
|
||||
const currentLocale = locale || defaultLocale
|
||||
|
||||
return (
|
||||
<ClickOutside active={display} onClick={() => setDisplay(false)} >
|
||||
<ClickOutside active={display} onClick={() => setDisplay(false)}>
|
||||
<nav className={s.root}>
|
||||
<div
|
||||
className="flex items-center relative"
|
||||
|
@ -7,12 +7,11 @@ import { useUI } from '@components/ui/context'
|
||||
import { Navbar, Footer } from '@components/common'
|
||||
import { useAcceptCookies } from '@lib/hooks/useAcceptCookies'
|
||||
import { Sidebar, Button, Modal, LoadingDots } from '@components/ui'
|
||||
import { CartSidebarView } from '@components/cart'
|
||||
import CartSidebarView from '@components/cart/CartSidebarView'
|
||||
|
||||
import LoginView from '@components/auth/LoginView'
|
||||
import { CommerceProvider } from '@framework'
|
||||
import type { Page } from '@framework/api/operations/get-all-pages'
|
||||
|
||||
import type { Page } from '@framework/common/get-all-pages'
|
||||
|
||||
const Loading = () => (
|
||||
<div className="w-80 h-80 flex items-center text-center justify-center p-3">
|
||||
@ -28,10 +27,12 @@ const SignUpView = dynamic(
|
||||
() => import('@components/auth/SignUpView'),
|
||||
dynamicProps
|
||||
)
|
||||
|
||||
const ForgotPassword = dynamic(
|
||||
() => import('@components/auth/ForgotPassword'),
|
||||
dynamicProps
|
||||
)
|
||||
|
||||
const FeatureBar = dynamic(
|
||||
() => import('@components/common/FeatureBar'),
|
||||
dynamicProps
|
||||
@ -40,10 +41,14 @@ const FeatureBar = dynamic(
|
||||
interface Props {
|
||||
pageProps: {
|
||||
pages?: Page[]
|
||||
commerceFeatures: Record<string, boolean>
|
||||
}
|
||||
}
|
||||
|
||||
const Layout: FC<Props> = ({ children, pageProps }) => {
|
||||
const Layout: FC<Props> = ({
|
||||
children,
|
||||
pageProps: { commerceFeatures, ...pageProps },
|
||||
}) => {
|
||||
const {
|
||||
displaySidebar,
|
||||
displayModal,
|
||||
@ -53,7 +58,6 @@ const Layout: FC<Props> = ({ children, pageProps }) => {
|
||||
} = useUI()
|
||||
const { acceptedCookies, onAcceptCookies } = useAcceptCookies()
|
||||
const { locale = 'en-US' } = useRouter()
|
||||
|
||||
return (
|
||||
<CommerceProvider locale={locale}>
|
||||
<div className={cn(s.root)}>
|
||||
|
@ -1,66 +1,50 @@
|
||||
import { FC, useState, useEffect } from 'react'
|
||||
import { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import s from './Navbar.module.css'
|
||||
import { Logo, Container } from '@components/ui'
|
||||
import { Searchbar, UserNav } from '@components/common'
|
||||
import cn from 'classnames'
|
||||
import throttle from 'lodash.throttle'
|
||||
import NavbarRoot from './NavbarRoot'
|
||||
import s from './Navbar.module.css'
|
||||
|
||||
const Navbar: FC = () => {
|
||||
const [hasScrolled, setHasScrolled] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = throttle(() => {
|
||||
const offset = 0
|
||||
const { scrollTop } = document.documentElement
|
||||
const scrolled = scrollTop > offset
|
||||
setHasScrolled(scrolled)
|
||||
}, 200)
|
||||
|
||||
document.addEventListener('scroll', handleScroll)
|
||||
return () => {
|
||||
document.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={cn(s.root, { 'shadow-magical': hasScrolled })}>
|
||||
<Container>
|
||||
<div className="relative flex flex-row justify-between py-4 align-center md:py-6">
|
||||
<div className="flex items-center flex-1">
|
||||
<Link href="/">
|
||||
<a className={s.logo} aria-label="Logo">
|
||||
<Logo />
|
||||
</a>
|
||||
const Navbar: FC = () => (
|
||||
<NavbarRoot>
|
||||
<Container>
|
||||
<div className="relative flex flex-row justify-between py-4 align-center md:py-6">
|
||||
<div className="flex items-center flex-1">
|
||||
<Link href="/">
|
||||
<a className={s.logo} aria-label="Logo">
|
||||
<Logo />
|
||||
</a>
|
||||
</Link>
|
||||
<nav className="hidden ml-6 space-x-4 lg:block">
|
||||
<Link href="/search">
|
||||
<a className={s.link}>All</a>
|
||||
</Link>
|
||||
<nav className="hidden ml-6 space-x-4 lg:block">
|
||||
<Link href="/search">
|
||||
<a className={s.link}>All</a>
|
||||
</Link>
|
||||
<Link href="/search?q=clothes">
|
||||
<a className={s.link}>Clothes</a>
|
||||
</Link>
|
||||
<Link href="/search?q=accessories">
|
||||
<a className={s.link}>Accessories</a>
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="justify-center flex-1 hidden lg:flex">
|
||||
<Searchbar />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end flex-1 space-x-8">
|
||||
<UserNav />
|
||||
</div>
|
||||
<Link href="/search?q=clothes">
|
||||
<a className={s.link}>Clothes</a>
|
||||
</Link>
|
||||
<Link href="/search?q=accessories">
|
||||
<a className={s.link}>Accessories</a>
|
||||
</Link>
|
||||
<Link href="/search?q=shoes">
|
||||
<a className={s.link}>Shoes</a>
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="flex pb-4 lg:px-6 lg:hidden">
|
||||
<Searchbar id="mobile-search" />
|
||||
<div className="justify-center flex-1 hidden lg:flex">
|
||||
<Searchbar />
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div className="flex justify-end flex-1 space-x-8">
|
||||
<UserNav />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex pb-4 lg:px-6 lg:hidden">
|
||||
<Searchbar id="mobile-search" />
|
||||
</div>
|
||||
</Container>
|
||||
</NavbarRoot>
|
||||
)
|
||||
|
||||
export default Navbar
|
||||
|
33
components/common/Navbar/NavbarRoot.tsx
Normal file
33
components/common/Navbar/NavbarRoot.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { FC, useState, useEffect } from 'react'
|
||||
import throttle from 'lodash.throttle'
|
||||
import cn from 'classnames'
|
||||
import s from './Navbar.module.css'
|
||||
|
||||
const NavbarRoot: FC = ({ children }) => {
|
||||
const [hasScrolled, setHasScrolled] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = throttle(() => {
|
||||
const offset = 0
|
||||
const { scrollTop } = document.documentElement
|
||||
const scrolled = scrollTop > offset
|
||||
|
||||
if (hasScrolled !== scrolled) {
|
||||
setHasScrolled(scrolled)
|
||||
}
|
||||
}, 200)
|
||||
|
||||
document.addEventListener('scroll', handleScroll)
|
||||
return () => {
|
||||
document.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}, [hasScrolled])
|
||||
|
||||
return (
|
||||
<div className={cn(s.root, { 'shadow-magical': hasScrolled })}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NavbarRoot
|
@ -8,6 +8,7 @@ import { Avatar } from '@components/common'
|
||||
import { Moon, Sun } from '@components/icons'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import ClickOutside from '@lib/click-outside'
|
||||
import useLogout from '@framework/auth/use-logout'
|
||||
|
||||
import {
|
||||
disableBodyScroll,
|
||||
@ -15,8 +16,6 @@ import {
|
||||
clearAllBodyScrollLocks,
|
||||
} from 'body-scroll-lock'
|
||||
|
||||
import useLogout from '@framework/use-logout'
|
||||
|
||||
interface DropdownMenuProps {
|
||||
open?: boolean
|
||||
}
|
||||
|
@ -1,27 +1,26 @@
|
||||
import { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import cn from 'classnames'
|
||||
import type { LineItem } from '@framework/types'
|
||||
import useCart from '@framework/cart/use-cart'
|
||||
import useCustomer from '@framework/use-customer'
|
||||
import useCustomer from '@framework/customer/use-customer'
|
||||
import { Avatar } from '@components/common'
|
||||
import { Heart, Bag } from '@components/icons'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import DropdownMenu from './DropdownMenu'
|
||||
import s from './UserNav.module.css'
|
||||
import { Avatar } from '@components/common'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const countItem = (count: number, item: any) => count + item.quantity
|
||||
const countItems = (count: number, items: any[]) =>
|
||||
items.reduce(countItem, count)
|
||||
const countItem = (count: number, item: LineItem) => count + item.quantity
|
||||
|
||||
const UserNav: FC<Props> = ({ className, children, ...props }) => {
|
||||
const UserNav: FC<Props> = ({ className }) => {
|
||||
const { data } = useCart()
|
||||
const { data: customer } = useCustomer()
|
||||
const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI()
|
||||
const itemsCount = Object.values(data?.line_items ?? {}).reduce(countItems, 0)
|
||||
const itemsCount = data?.lineItems.reduce(countItem, 0) ?? 0
|
||||
|
||||
return (
|
||||
<nav className={cn(s.root, className)}>
|
||||
@ -31,13 +30,15 @@ const UserNav: FC<Props> = ({ className, children, ...props }) => {
|
||||
<Bag />
|
||||
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
|
||||
</li>
|
||||
<li className={s.item}>
|
||||
<Link href="/wishlist">
|
||||
<a onClick={closeSidebarIfPresent} aria-label="Wishlist">
|
||||
<Heart />
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
||||
<li className={s.item}>
|
||||
<Link href="/wishlist">
|
||||
<a onClick={closeSidebarIfPresent} aria-label="Wishlist">
|
||||
<Heart />
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
<li className={s.item}>
|
||||
{customer ? (
|
||||
<DropdownMenu />
|
||||
|
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
|
@ -1,15 +1,39 @@
|
||||
const Vercel = ({ ...props }) => {
|
||||
return (
|
||||
<svg width="89" height="20" viewBox="0 0 89 20" fill="none" xmlns="http://www.w3.org/2000/svg" { ...props }>
|
||||
<path d="M11.5625 0L23.125 20H0L11.5625 0Z" fill="currentColor"/>
|
||||
<path d="M49.875 10.625C49.875 7.40625 47.5 5.15625 44.0937 5.15625C40.6875 5.15625 38.3125 7.40625 38.3125 10.625C38.3125 13.7812 40.875 16.0937 44.4062 16.0937C46.3438 16.0937 48.0938 15.375 49.2188 14.0625L47.0938 12.8437C46.4375 13.5 45.4688 13.9062 44.4062 13.9062C42.8438 13.9062 41.5 13.0625 41.0312 11.7812L40.9375 11.5625H49.7812C49.8438 11.25 49.875 10.9375 49.875 10.625ZM40.9062 9.6875L40.9688 9.5C41.375 8.15625 42.5625 7.34375 44.0625 7.34375C45.5938 7.34375 46.75 8.15625 47.1562 9.5L47.2188 9.6875H40.9062Z" fill="currentColor"/>
|
||||
<path d="M83.5313 10.625C83.5313 7.40625 81.1563 5.15625 77.75 5.15625C74.3438 5.15625 71.9688 7.40625 71.9688 10.625C71.9688 13.7812 74.5313 16.0937 78.0625 16.0937C80 16.0937 81.75 15.375 82.875 14.0625L80.75 12.8437C80.0938 13.5 79.125 13.9062 78.0625 13.9062C76.5 13.9062 75.1563 13.0625 74.6875 11.7812L74.5938 11.5625H83.4375C83.5 11.25 83.5313 10.9375 83.5313 10.625ZM74.5625 9.6875L74.625 9.5C75.0313 8.15625 76.2188 7.34375 77.7188 7.34375C79.25 7.34375 80.4063 8.15625 80.8125 9.5L80.875 9.6875H74.5625Z" fill="currentColor"/>
|
||||
<path d="M68.5313 8.84374L70.6563 7.62499C69.6563 6.06249 67.875 5.18749 65.7188 5.18749C62.3125 5.18749 59.9375 7.43749 59.9375 10.6562C59.9375 13.875 62.3125 16.125 65.7188 16.125C67.875 16.125 69.6563 15.25 70.6563 13.6875L68.5313 12.4687C67.9688 13.4062 66.9688 13.9375 65.7188 13.9375C63.75 13.9375 62.4375 12.625 62.4375 10.6562C62.4375 8.68749 63.75 7.37499 65.7188 7.37499C66.9375 7.37499 67.9688 7.90624 68.5313 8.84374Z" fill="currentColor"/>
|
||||
<path d="M88.2188 1.75H85.7188V15.8125H88.2188V1.75Z" fill="currentColor"/>
|
||||
<path d="M40.1563 1.75H37.2813L31.7813 11.25L26.2813 1.75H23.375L31.7813 16.25L40.1563 1.75Z" fill="currentColor"/>
|
||||
<path d="M57.8438 8.0625C58.125 8.0625 58.4062 8.09375 58.6875 8.15625V5.5C56.5625 5.5625 54.5625 6.75 54.5625 8.21875V5.5H52.0625V15.8125H54.5625V11.3437C54.5625 9.40625 55.9062 8.0625 57.8438 8.0625Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="89"
|
||||
height="20"
|
||||
viewBox="0 0 89 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path d="M11.5625 0L23.125 20H0L11.5625 0Z" fill="currentColor" />
|
||||
<path
|
||||
d="M49.875 10.625C49.875 7.40625 47.5 5.15625 44.0937 5.15625C40.6875 5.15625 38.3125 7.40625 38.3125 10.625C38.3125 13.7812 40.875 16.0937 44.4062 16.0937C46.3438 16.0937 48.0938 15.375 49.2188 14.0625L47.0938 12.8437C46.4375 13.5 45.4688 13.9062 44.4062 13.9062C42.8438 13.9062 41.5 13.0625 41.0312 11.7812L40.9375 11.5625H49.7812C49.8438 11.25 49.875 10.9375 49.875 10.625ZM40.9062 9.6875L40.9688 9.5C41.375 8.15625 42.5625 7.34375 44.0625 7.34375C45.5938 7.34375 46.75 8.15625 47.1562 9.5L47.2188 9.6875H40.9062Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M83.5313 10.625C83.5313 7.40625 81.1563 5.15625 77.75 5.15625C74.3438 5.15625 71.9688 7.40625 71.9688 10.625C71.9688 13.7812 74.5313 16.0937 78.0625 16.0937C80 16.0937 81.75 15.375 82.875 14.0625L80.75 12.8437C80.0938 13.5 79.125 13.9062 78.0625 13.9062C76.5 13.9062 75.1563 13.0625 74.6875 11.7812L74.5938 11.5625H83.4375C83.5 11.25 83.5313 10.9375 83.5313 10.625ZM74.5625 9.6875L74.625 9.5C75.0313 8.15625 76.2188 7.34375 77.7188 7.34375C79.25 7.34375 80.4063 8.15625 80.8125 9.5L80.875 9.6875H74.5625Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M68.5313 8.84374L70.6563 7.62499C69.6563 6.06249 67.875 5.18749 65.7188 5.18749C62.3125 5.18749 59.9375 7.43749 59.9375 10.6562C59.9375 13.875 62.3125 16.125 65.7188 16.125C67.875 16.125 69.6563 15.25 70.6563 13.6875L68.5313 12.4687C67.9688 13.4062 66.9688 13.9375 65.7188 13.9375C63.75 13.9375 62.4375 12.625 62.4375 10.6562C62.4375 8.68749 63.75 7.37499 65.7188 7.37499C66.9375 7.37499 67.9688 7.90624 68.5313 8.84374Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M88.2188 1.75H85.7188V15.8125H88.2188V1.75Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M40.1563 1.75H37.2813L31.7813 11.25L26.2813 1.75H23.375L31.7813 16.25L40.1563 1.75Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M57.8438 8.0625C58.125 8.0625 58.4062 8.09375 58.6875 8.15625V5.5C56.5625 5.5625 54.5625 6.75 54.5625 8.21875V5.5H52.0625V15.8125H54.5625V11.3437C54.5625 9.40625 55.9062 8.0625 57.8438 8.0625Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -14,3 +14,5 @@ export { default as RightArrow } from './RightArrow'
|
||||
export { default as Info } from './Info'
|
||||
export { default as ChevronUp } from './ChevronUp'
|
||||
export { default as Vercel } from './Vercel'
|
||||
export { default as MapPin } from './MapPin'
|
||||
export { default as CreditCard } from './CreditCard'
|
||||
|
@ -1,103 +1,88 @@
|
||||
import { FC } from 'react'
|
||||
import cn from 'classnames'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import type { FC } from 'react'
|
||||
import type { Product } from '@commerce/types'
|
||||
import s from './ProductCard.module.css'
|
||||
import Image, { ImageProps } from 'next/image'
|
||||
import WishlistButton from '@components/wishlist/WishlistButton'
|
||||
|
||||
import usePrice from '@framework/use-price'
|
||||
import type { ProductNode } from '@framework/api/operations/get-all-products'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
product: ProductNode
|
||||
product: Product
|
||||
variant?: 'slim' | 'simple'
|
||||
imgWidth: number | string
|
||||
imgHeight: number | string
|
||||
imgLayout?: 'fixed' | 'intrinsic' | 'responsive' | undefined
|
||||
imgPriority?: boolean
|
||||
imgLoading?: 'eager' | 'lazy'
|
||||
imgSizes?: string
|
||||
imgProps?: Omit<ImageProps, 'src'>
|
||||
}
|
||||
|
||||
const placeholderImg = '/product-img-placeholder.svg'
|
||||
|
||||
const ProductCard: FC<Props> = ({
|
||||
className,
|
||||
product: p,
|
||||
product,
|
||||
variant,
|
||||
imgWidth,
|
||||
imgHeight,
|
||||
imgPriority,
|
||||
imgLoading,
|
||||
imgSizes,
|
||||
imgLayout = 'responsive',
|
||||
}) => {
|
||||
const src = p.images.edges?.[0]?.node?.urlOriginal!
|
||||
const placeholderImg = '/product-img-placeholder.svg';
|
||||
const { price } = usePrice({
|
||||
amount: p.prices?.price?.value,
|
||||
baseAmount: p.prices?.retailPrice?.value,
|
||||
currencyCode: p.prices?.price?.currencyCode!,
|
||||
})
|
||||
|
||||
return (
|
||||
<Link href={`/product${p.path}`}>
|
||||
<a
|
||||
className={cn(s.root, { [s.simple]: variant === 'simple' }, className)}
|
||||
>
|
||||
{variant === 'slim' ? (
|
||||
<div className="relative overflow-hidden box-border">
|
||||
<div className="absolute inset-0 flex items-center justify-end mr-8 z-20">
|
||||
<span className="bg-black text-white inline-block p-3 font-bold text-xl break-words">
|
||||
{p.name}
|
||||
</span>
|
||||
</div>
|
||||
imgProps,
|
||||
...props
|
||||
}) => (
|
||||
<Link href={`/product/${product.slug}`} {...props}>
|
||||
<a className={cn(s.root, { [s.simple]: variant === 'simple' }, className)}>
|
||||
{variant === 'slim' ? (
|
||||
<div className="relative overflow-hidden box-border">
|
||||
<div className="absolute inset-0 flex items-center justify-end mr-8 z-20">
|
||||
<span className="bg-black text-white inline-block p-3 font-bold text-xl break-words">
|
||||
{product.name}
|
||||
</span>
|
||||
</div>
|
||||
{product?.images && (
|
||||
<Image
|
||||
quality="85"
|
||||
width={imgWidth}
|
||||
sizes={imgSizes}
|
||||
height={imgHeight}
|
||||
layout={imgLayout}
|
||||
loading={imgLoading}
|
||||
priority={imgPriority}
|
||||
src={p.images.edges?.[0]?.node.urlOriginal! || placeholderImg}
|
||||
alt={p.images.edges?.[0]?.node.altText || 'Product Image'}
|
||||
src={product.images[0].url || placeholderImg}
|
||||
alt={product.name || 'Product Image'}
|
||||
height={320}
|
||||
width={320}
|
||||
layout="fixed"
|
||||
{...imgProps}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={s.squareBg} />
|
||||
<div className="flex flex-row justify-between box-border w-full z-20 absolute">
|
||||
<div className="absolute top-0 left-0 pr-16 max-w-full">
|
||||
<h3 className={s.productTitle}>
|
||||
<span>{p.name}</span>
|
||||
</h3>
|
||||
<span className={s.productPrice}>{price}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={s.squareBg} />
|
||||
<div className="flex flex-row justify-between box-border w-full z-20 absolute">
|
||||
<div className="absolute top-0 left-0 pr-16 max-w-full">
|
||||
<h3 className={s.productTitle}>
|
||||
<span>{product.name}</span>
|
||||
</h3>
|
||||
<span className={s.productPrice}>
|
||||
{product.price.value}
|
||||
|
||||
{product.price.currencyCode}
|
||||
</span>
|
||||
</div>
|
||||
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
||||
<WishlistButton
|
||||
className={s.wishlistButton}
|
||||
productId={p.entityId}
|
||||
variant={p.variants.edges?.[0]!}
|
||||
productId={product.id}
|
||||
variant={product.variants[0] as any}
|
||||
/>
|
||||
</div>
|
||||
<div className={s.imageContainer}>
|
||||
)}
|
||||
</div>
|
||||
<div className={s.imageContainer}>
|
||||
{product?.images && (
|
||||
<Image
|
||||
quality="85"
|
||||
src={src || placeholderImg}
|
||||
alt={p.name}
|
||||
alt={product.name || 'Product Image'}
|
||||
className={s.productImage}
|
||||
width={imgWidth}
|
||||
sizes={imgSizes}
|
||||
height={imgHeight}
|
||||
layout={imgLayout}
|
||||
loading={imgLoading}
|
||||
priority={imgPriority}
|
||||
src={product.images[0].url || placeholderImg}
|
||||
height={540}
|
||||
width={540}
|
||||
quality="85"
|
||||
layout="responsive"
|
||||
{...imgProps}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
|
||||
export default ProductCard
|
||||
|
@ -1,5 +1,12 @@
|
||||
import { useKeenSlider } from 'keen-slider/react'
|
||||
import React, { Children, FC, isValidElement, useState, useRef, useEffect } from 'react'
|
||||
import React, {
|
||||
Children,
|
||||
FC,
|
||||
isValidElement,
|
||||
useState,
|
||||
useRef,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import cn from 'classnames'
|
||||
|
||||
import s from './ProductSlider.module.css'
|
||||
@ -25,7 +32,7 @@ const ProductSlider: FC = ({ children }) => {
|
||||
const touchXPosition = event.touches[0].pageX
|
||||
// Size of the touch area
|
||||
const touchXRadius = event.touches[0].radiusX || 0
|
||||
|
||||
|
||||
// We set a threshold (10px) on both sizes of the screen,
|
||||
// if the touch area overlaps with the screen edges
|
||||
// it's likely to trigger the navigation. We prevent the
|
||||
@ -33,15 +40,22 @@ const ProductSlider: FC = ({ children }) => {
|
||||
if (
|
||||
touchXPosition - touchXRadius < 10 ||
|
||||
touchXPosition + touchXRadius > window.innerWidth - 10
|
||||
) event.preventDefault()
|
||||
)
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
sliderContainerRef.current!
|
||||
.addEventListener('touchstart', preventNavigation)
|
||||
sliderContainerRef.current!.addEventListener(
|
||||
'touchstart',
|
||||
preventNavigation
|
||||
)
|
||||
|
||||
return () => {
|
||||
sliderContainerRef.current!
|
||||
.removeEventListener('touchstart', preventNavigation)
|
||||
if (sliderContainerRef.current) {
|
||||
sliderContainerRef.current!.removeEventListener(
|
||||
'touchstart',
|
||||
preventNavigation
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
@screen md {
|
||||
|
@ -1,51 +1,48 @@
|
||||
import { FC, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import Image from 'next/image'
|
||||
import { NextSeo } from 'next-seo'
|
||||
|
||||
import { FC, useState } from 'react'
|
||||
import s from './ProductView.module.css'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import { Swatch, ProductSlider } from '@components/product'
|
||||
import { Button, Container, Text } from '@components/ui'
|
||||
|
||||
import usePrice from '@framework/use-price'
|
||||
import useAddItem from '@framework/cart/use-add-item'
|
||||
import type { ProductNode } from '@framework/api/operations/get-product'
|
||||
import {
|
||||
getCurrentVariant,
|
||||
getProductOptions,
|
||||
SelectedOptions,
|
||||
} from '../helpers'
|
||||
import { Swatch, ProductSlider } from '@components/product'
|
||||
import { Button, Container, Text, useUI } from '@components/ui'
|
||||
|
||||
import type { Product } from '@commerce/types'
|
||||
import usePrice from '@framework/product/use-price'
|
||||
import { useAddItem } from '@framework/cart'
|
||||
|
||||
import { getVariant, SelectedOptions } from '../helpers'
|
||||
import WishlistButton from '@components/wishlist/WishlistButton'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
children?: any
|
||||
product: ProductNode
|
||||
product: Product
|
||||
}
|
||||
|
||||
const ProductView: FC<Props> = ({ product }) => {
|
||||
const addItem = useAddItem()
|
||||
const { price } = usePrice({
|
||||
amount: product.prices?.price?.value,
|
||||
baseAmount: product.prices?.retailPrice?.value,
|
||||
currencyCode: product.prices?.price?.currencyCode!,
|
||||
amount: product.price.value,
|
||||
baseAmount: product.price.retailPrice,
|
||||
currencyCode: product.price.currencyCode!,
|
||||
})
|
||||
const { openSidebar } = useUI()
|
||||
const options = getProductOptions(product)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [choices, setChoices] = useState<SelectedOptions>({
|
||||
size: null,
|
||||
color: null,
|
||||
})
|
||||
const variant = getCurrentVariant(product, choices)
|
||||
|
||||
// Select the correct variant based on choices
|
||||
const variant = getVariant(product, choices)
|
||||
|
||||
const addToCart = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
await addItem({
|
||||
productId: product.entityId,
|
||||
variantId: variant?.node.entityId!,
|
||||
productId: String(product.id),
|
||||
variantId: String(variant ? variant.id : product.variants[0].id),
|
||||
})
|
||||
openSidebar()
|
||||
setLoading(false)
|
||||
@ -65,7 +62,7 @@ const ProductView: FC<Props> = ({ product }) => {
|
||||
description: product.description,
|
||||
images: [
|
||||
{
|
||||
url: product.images.edges?.[0]?.node.urlOriginal!,
|
||||
url: product.images[0]?.url!,
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: product.name,
|
||||
@ -80,18 +77,18 @@ const ProductView: FC<Props> = ({ product }) => {
|
||||
<div className={s.price}>
|
||||
{price}
|
||||
{` `}
|
||||
{product.prices?.price.currencyCode}
|
||||
{product.price?.currencyCode}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={s.sliderContainer}>
|
||||
<ProductSlider key={product.entityId}>
|
||||
{product.images.edges?.map((image, i) => (
|
||||
<div key={image?.node.urlOriginal} className={s.imageContainer}>
|
||||
<ProductSlider key={product.id}>
|
||||
{product.images.map((image, i) => (
|
||||
<div key={image.url} className={s.imageContainer}>
|
||||
<Image
|
||||
className={s.img}
|
||||
src={image?.node.urlOriginal!}
|
||||
alt={image?.node.altText || 'Product Image'}
|
||||
src={image.url!}
|
||||
alt={image.alt || 'Product Image'}
|
||||
width={1050}
|
||||
height={1050}
|
||||
priority={i === 0}
|
||||
@ -102,20 +99,21 @@ const ProductView: FC<Props> = ({ product }) => {
|
||||
</ProductSlider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={s.sidebar}>
|
||||
<section>
|
||||
{options?.map((opt: any) => (
|
||||
{product.options?.map((opt) => (
|
||||
<div className="pb-4" key={opt.displayName}>
|
||||
<h2 className="uppercase font-medium">{opt.displayName}</h2>
|
||||
<div className="flex flex-row py-4">
|
||||
{opt.values.map((v: any, i: number) => {
|
||||
const active = (choices as any)[opt.displayName]
|
||||
{opt.values.map((v, i: number) => {
|
||||
const active = (choices as any)[
|
||||
opt.displayName.toLowerCase()
|
||||
]
|
||||
|
||||
return (
|
||||
<Swatch
|
||||
key={`${v.entityId}-${i}`}
|
||||
active={v.label === active}
|
||||
key={`${opt.id}-${i}`}
|
||||
active={v.label.toLowerCase() === active}
|
||||
variant={opt.displayName}
|
||||
color={v.hexColors ? v.hexColors[0] : ''}
|
||||
label={v.label}
|
||||
@ -123,7 +121,7 @@ const ProductView: FC<Props> = ({ product }) => {
|
||||
setChoices((choices) => {
|
||||
return {
|
||||
...choices,
|
||||
[opt.displayName]: v.label,
|
||||
[opt.displayName.toLowerCase()]: v.label.toLowerCase(),
|
||||
}
|
||||
})
|
||||
}}
|
||||
@ -145,18 +143,19 @@ const ProductView: FC<Props> = ({ product }) => {
|
||||
className={s.button}
|
||||
onClick={addToCart}
|
||||
loading={loading}
|
||||
disabled={!variant}
|
||||
disabled={!variant && product.options.length > 0}
|
||||
>
|
||||
Add to Cart
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WishlistButton
|
||||
className={s.wishlistButton}
|
||||
productId={product.entityId}
|
||||
variant={variant!}
|
||||
/>
|
||||
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
||||
<WishlistButton
|
||||
className={s.wishlistButton}
|
||||
productId={product.id}
|
||||
variant={product.variants[0]! as any}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
|
@ -1,4 +1,5 @@
|
||||
.root {
|
||||
composes: root from 'components/ui/Button/Button.module.css';
|
||||
@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
|
||||
p-0 shadow-none border-gray-200 border box-border;
|
||||
|
@ -13,7 +13,7 @@ interface Props {
|
||||
color?: string
|
||||
}
|
||||
|
||||
const Swatch: FC<Props & ButtonProps> = ({
|
||||
const Swatch: FC<Omit<ButtonProps, 'variant'> & Props> = ({
|
||||
className,
|
||||
color = '',
|
||||
label,
|
||||
|
@ -1,56 +1,22 @@
|
||||
import type { ProductNode } from '@framework/api/operations/get-product'
|
||||
import type { Product } from '@commerce/types'
|
||||
|
||||
export type SelectedOptions = {
|
||||
size: string | null
|
||||
color: string | null
|
||||
}
|
||||
|
||||
export type ProductOption = {
|
||||
displayName: string
|
||||
values: any
|
||||
}
|
||||
|
||||
// Returns the available options of a product
|
||||
export function getProductOptions(product: ProductNode) {
|
||||
const options = product.productOptions.edges?.reduce<ProductOption[]>(
|
||||
(arr, edge) => {
|
||||
if (edge?.node.__typename === 'MultipleChoiceOption') {
|
||||
arr.push({
|
||||
displayName: edge.node.displayName.toLowerCase(),
|
||||
values: edge.node.values.edges?.map((edge) => edge?.node),
|
||||
})
|
||||
}
|
||||
return arr
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// Finds a variant in the product that matches the selected options
|
||||
export function getCurrentVariant(product: ProductNode, opts: SelectedOptions) {
|
||||
const variant = product.variants.edges?.find((edge) => {
|
||||
const { node } = edge ?? {}
|
||||
const numberOfDefinedOpts = Object.values(opts).filter(value => value !== null).length;
|
||||
const numberOfEdges = node?.productOptions?.edges?.length;
|
||||
|
||||
const isEdgeEqualToOption = ([key, value]:[string, string | null]) =>
|
||||
node?.productOptions.edges?.find((edge) => {
|
||||
export function getVariant(product: Product, opts: SelectedOptions) {
|
||||
const variant = product.variants.find((variant) => {
|
||||
return Object.entries(opts).every(([key, value]) =>
|
||||
variant.options.find((option) => {
|
||||
if (
|
||||
edge?.node.__typename === 'MultipleChoiceOption' &&
|
||||
edge.node.displayName.toLowerCase() === key
|
||||
option.__typename === 'MultipleChoiceOption' &&
|
||||
option.displayName.toLowerCase() === key.toLowerCase()
|
||||
) {
|
||||
return edge.node.values.edges?.find(
|
||||
(valueEdge) => valueEdge?.node.label === value
|
||||
)
|
||||
return option.values.find((v) => v.label.toLowerCase() === value)
|
||||
}
|
||||
});
|
||||
|
||||
return numberOfDefinedOpts === numberOfEdges ?
|
||||
Object.entries(opts).every(isEdgeEqualToOption)
|
||||
: Object.entries(opts).some(isEdgeEqualToOption)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
return variant ?? product.variants.edges?.[0]
|
||||
return variant
|
||||
}
|
||||
|
@ -13,9 +13,9 @@ const Container: FC<Props> = ({ children, className, el = 'div', clean }) => {
|
||||
'mx-auto max-w-8xl px-6': !clean,
|
||||
})
|
||||
|
||||
let Component: React.ComponentType<React.HTMLAttributes<
|
||||
HTMLDivElement
|
||||
>> = el as any
|
||||
let Component: React.ComponentType<
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
> = el as any
|
||||
|
||||
return <Component className={rootClassName}>{children}</Component>
|
||||
}
|
||||
|
@ -1,15 +1,16 @@
|
||||
.root {
|
||||
@apply w-full;
|
||||
@apply w-full relative;
|
||||
height: 320px;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
@apply flex flex-row items-center;
|
||||
}
|
||||
|
||||
& > * {
|
||||
@apply flex-1 px-16 py-4;
|
||||
width: 430px;
|
||||
}
|
||||
.container > * {
|
||||
@apply relative flex-1 px-16 py-4 h-full;
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
.primary {
|
||||
|
@ -8,6 +8,7 @@ export interface State {
|
||||
displayToast: boolean
|
||||
modalView: string
|
||||
toastText: string
|
||||
userAvatar: string
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
@ -17,6 +18,7 @@ const initialState = {
|
||||
modalView: 'LOGIN_VIEW',
|
||||
displayToast: false,
|
||||
toastText: '',
|
||||
userAvatar: '',
|
||||
}
|
||||
|
||||
type Action =
|
||||
@ -55,9 +57,14 @@ type Action =
|
||||
| {
|
||||
type: 'SET_USER_AVATAR'
|
||||
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
|
||||
|
||||
export const UIContext = React.createContext<State | any>(initialState)
|
||||
@ -157,7 +164,8 @@ export const UIProvider: FC = (props) => {
|
||||
const openToast = () => dispatch({ type: 'OPEN_TOAST' })
|
||||
const closeToast = () => dispatch({ type: 'CLOSE_TOAST' })
|
||||
|
||||
const setUserAvatar = (value: string) => dispatch({ type: 'SET_USER_AVATAR', value })
|
||||
const setUserAvatar = (value: string) =>
|
||||
dispatch({ type: 'SET_USER_AVATAR', value })
|
||||
|
||||
const setModalView = (view: MODAL_VIEWS) =>
|
||||
dispatch({ type: 'SET_MODAL_VIEW', view })
|
||||
@ -176,7 +184,7 @@ export const UIProvider: FC = (props) => {
|
||||
setModalView,
|
||||
openToast,
|
||||
closeToast,
|
||||
setUserAvatar
|
||||
setUserAvatar,
|
||||
}),
|
||||
[state]
|
||||
)
|
||||
|
@ -10,3 +10,4 @@ export { default as Skeleton } from './Skeleton'
|
||||
export { default as Modal } from './Modal'
|
||||
export { default as Text } from './Text'
|
||||
export { default as Input } from './Input'
|
||||
export { useUI } from './context'
|
||||
|
@ -1,16 +1,16 @@
|
||||
import React, { FC, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import type { ProductNode } from '@framework/api/operations/get-all-products'
|
||||
import useAddItem from '@framework/wishlist/use-add-item'
|
||||
import useRemoveItem from '@framework/wishlist/use-remove-item'
|
||||
import useWishlist from '@framework/wishlist/use-wishlist'
|
||||
import useCustomer from '@framework/use-customer'
|
||||
import { useUI } from '@components/ui'
|
||||
import { Heart } from '@components/icons'
|
||||
import { useUI } from '@components/ui/context'
|
||||
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 type { Product, ProductVariant } from '@commerce/types'
|
||||
|
||||
type Props = {
|
||||
productId: number
|
||||
variant: NonNullable<ProductNode['variants']['edges']>[0]
|
||||
productId: Product['id']
|
||||
variant: ProductVariant
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>
|
||||
|
||||
const WishlistButton: FC<Props> = ({
|
||||
@ -19,16 +19,19 @@ const WishlistButton: FC<Props> = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const { data } = useWishlist()
|
||||
const addItem = useAddItem()
|
||||
const removeItem = useRemoveItem()
|
||||
const { data } = useWishlist()
|
||||
const { data: customer } = useCustomer()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const { openModal, setModalView } = useUI()
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// @ts-ignore Wishlist is not always enabled
|
||||
const itemInWishlist = data?.items?.find(
|
||||
// @ts-ignore Wishlist is not always enabled
|
||||
(item) =>
|
||||
item.product_id === productId &&
|
||||
item.variant_id === variant?.node.entityId
|
||||
item.product_id === Number(productId) &&
|
||||
(item.variant_id as any) === Number(variant.id)
|
||||
)
|
||||
|
||||
const handleWishlistChange = async (e: any) => {
|
||||
@ -50,7 +53,7 @@ const WishlistButton: FC<Props> = ({
|
||||
} else {
|
||||
await addItem({
|
||||
productId,
|
||||
variantId: variant?.node.entityId!,
|
||||
variantId: variant?.id!,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -2,27 +2,28 @@ import { FC, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import type { WishlistItem } from '@framework/api/wishlist'
|
||||
import usePrice from '@framework/use-price'
|
||||
import useRemoveItem from '@framework/wishlist/use-remove-item'
|
||||
import useAddItem from '@framework/cart/use-add-item'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import { Button, Text } from '@components/ui'
|
||||
import { Trash } from '@components/icons'
|
||||
import s from './WishlistCard.module.css'
|
||||
import { Trash } from '@components/icons'
|
||||
import { Button, Text } from '@components/ui'
|
||||
|
||||
import { useUI } from '@components/ui/context'
|
||||
import type { Product } from '@commerce/types'
|
||||
import usePrice from '@framework/product/use-price'
|
||||
import useAddItem from '@framework/cart/use-add-item'
|
||||
import useRemoveItem from '@framework/wishlist/use-remove-item'
|
||||
|
||||
interface Props {
|
||||
item: WishlistItem
|
||||
product: Product
|
||||
}
|
||||
|
||||
const WishlistCard: FC<Props> = ({ item }) => {
|
||||
const product = item.product!
|
||||
const WishlistCard: FC<Props> = ({ product }) => {
|
||||
const { price } = usePrice({
|
||||
amount: product.prices?.price?.value,
|
||||
baseAmount: product.prices?.retailPrice?.value,
|
||||
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 [removing, setRemoving] = useState(false)
|
||||
const addItem = useAddItem()
|
||||
@ -34,7 +35,7 @@ const WishlistCard: FC<Props> = ({ item }) => {
|
||||
try {
|
||||
// If this action succeeds then there's no need to do `setRemoving(true)`
|
||||
// because the component will be removed from the view
|
||||
await removeItem({ id: item.id! })
|
||||
await removeItem({ id: product.id! })
|
||||
} catch (error) {
|
||||
setRemoving(false)
|
||||
}
|
||||
@ -43,8 +44,8 @@ const WishlistCard: FC<Props> = ({ item }) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
await addItem({
|
||||
productId: product.entityId,
|
||||
variantId: product.variants.edges?.[0]?.node.entityId!,
|
||||
productId: String(product.id),
|
||||
variantId: String(product.variants[0].id),
|
||||
})
|
||||
openSidebar()
|
||||
setLoading(false)
|
||||
@ -57,10 +58,10 @@ const WishlistCard: FC<Props> = ({ item }) => {
|
||||
<div className={cn(s.root, { 'opacity-75 pointer-events-none': removing })}>
|
||||
<div className={`col-span-3 ${s.productBg}`}>
|
||||
<Image
|
||||
src={product.images.edges?.[0]?.node.urlOriginal!}
|
||||
src={product.images[0].url}
|
||||
width={400}
|
||||
height={400}
|
||||
alt={product.images.edges?.[0]?.node.altText || 'Product Image'}
|
||||
alt={product.images[0].alt || 'Product Image'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -1 +1,2 @@
|
||||
export { default as WishlistCard } from './WishlistCard'
|
||||
export { default as WishlistButton } from './WishlistButton'
|
||||
|
@ -1,12 +1,22 @@
|
||||
{
|
||||
"title": "ACME Storefront | Powered by Next.js Commerce",
|
||||
"titleTemplate": "%s - ACME Storefront",
|
||||
"description": "Next.js Commerce -> https://www.nextjs.org/commerce",
|
||||
"description": "Next.js Commerce - https://www.nextjs.org/commerce",
|
||||
"openGraph": {
|
||||
"title": "ACME Storefront | Powered by Next.js Commerce",
|
||||
"description": "Next.js Commerce - https://www.nextjs.org/commerce",
|
||||
"type": "website",
|
||||
"locale": "en_IE",
|
||||
"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": {
|
||||
"handle": "@nextjs",
|
||||
|
6
framework/bigcommerce/.env.template
Normal file
6
framework/bigcommerce/.env.template
Normal file
@ -0,0 +1,6 @@
|
||||
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,27 +1,25 @@
|
||||
# Table of Contents
|
||||
|
||||
Table of Contents
|
||||
=================
|
||||
|
||||
* [BigCommerce Storefront Data Hooks](#bigcommerce-storefront-data-hooks)
|
||||
* [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](#bigcommerce-storefront-data-hooks)
|
||||
- [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
|
||||
|
||||
@ -49,6 +47,7 @@ BIGCOMMERCE_STOREFRONT_API_TOKEN=<>
|
||||
BIGCOMMERCE_STORE_API_URL=<>
|
||||
BIGCOMMERCE_STORE_API_TOKEN=<>
|
||||
BIGCOMMERCE_STORE_API_CLIENT_ID=<>
|
||||
BIGCOMMERCE_CHANNEL_ID=<>
|
||||
```
|
||||
|
||||
## General Usage
|
||||
@ -193,13 +192,11 @@ Returns the current cart data for use
|
||||
...
|
||||
import useCart from '@bigcommerce/storefront-data-hooks/cart/use-cart'
|
||||
|
||||
const countItem = (count: number, item: any) => count + item.quantity
|
||||
const countItems = (count: number, items: any[]) =>
|
||||
items.reduce(countItem, count)
|
||||
const countItem = (count: number, item: LineItem) => count + item.quantity
|
||||
|
||||
const CartNumber = () => {
|
||||
const { data } = useCart()
|
||||
const itemsCount = Object.values(data?.line_items ?? {}).reduce(countItems, 0)
|
||||
const itemsCount = data?.lineItems.reduce(countItem, 0) ?? 0
|
||||
|
||||
return itemsCount > 0 ? <span>{itemsCount}</span> : null
|
||||
}
|
||||
@ -235,7 +232,7 @@ import useUpdateItem from '@bigcommerce/storefront-data-hooks/cart/use-update-it
|
||||
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 })
|
||||
@ -264,7 +261,7 @@ import useRemoveItem from '@bigcommerce/storefront-data-hooks/cart/use-remove-it
|
||||
|
||||
const RemoveButton = ({ item }) => {
|
||||
const removeItem = useRemoveItem()
|
||||
|
||||
|
||||
const handleRemove = async () => {
|
||||
await removeItem({ id: item.id })
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import { parseCartItem } from '../../utils/parse-item'
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
import type { CartHandlers } from '..'
|
||||
|
||||
// Return current cart info
|
||||
const addItem: CartHandlers['addItem'] = async ({
|
||||
res,
|
||||
body: { cartId, item },
|
||||
@ -26,8 +25,14 @@ const addItem: CartHandlers['addItem'] = async ({
|
||||
}),
|
||||
}
|
||||
const { data } = cartId
|
||||
? await config.storeApiFetch(`/v3/carts/${cartId}/items?include=line_items.physical_items.options`, options)
|
||||
: await config.storeApiFetch('/v3/carts?include=line_items.physical_items.options', options)
|
||||
? await config.storeApiFetch(
|
||||
`/v3/carts/${cartId}/items?include=line_items.physical_items.options`,
|
||||
options
|
||||
)
|
||||
: await config.storeApiFetch(
|
||||
'/v3/carts?include=line_items.physical_items.options',
|
||||
options
|
||||
)
|
||||
|
||||
// Create or update the cart cookie
|
||||
res.setHeader(
|
||||
|
@ -1,6 +1,7 @@
|
||||
import type { BigcommerceCart } from '../../../types'
|
||||
import { BigcommerceApiError } from '../../utils/errors'
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
import type { Cart, CartHandlers } from '..'
|
||||
import type { CartHandlers } from '../'
|
||||
|
||||
// Return current cart info
|
||||
const getCart: CartHandlers['getCart'] = async ({
|
||||
@ -8,11 +9,13 @@ const getCart: CartHandlers['getCart'] = async ({
|
||||
body: { cartId },
|
||||
config,
|
||||
}) => {
|
||||
let result: { data?: Cart } = {}
|
||||
let result: { data?: BigcommerceCart } = {}
|
||||
|
||||
if (cartId) {
|
||||
try {
|
||||
result = await config.storeApiFetch(`/v3/carts/${cartId}?include=line_items.physical_items.options`)
|
||||
result = await config.storeApiFetch(
|
||||
`/v3/carts/${cartId}?include=line_items.physical_items.options`
|
||||
)
|
||||
} catch (error) {
|
||||
if (error instanceof BigcommerceApiError && error.status === 404) {
|
||||
// Remove the cookie if it exists but the cart wasn't found
|
||||
|
@ -1,7 +1,6 @@
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
import type { CartHandlers } from '..'
|
||||
|
||||
// Return current cart info
|
||||
const removeItem: CartHandlers['removeItem'] = async ({
|
||||
res,
|
||||
body: { cartId, itemId },
|
||||
|
@ -2,7 +2,6 @@ import { parseCartItem } from '../../utils/parse-item'
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
import type { CartHandlers } from '..'
|
||||
|
||||
// Return current cart info
|
||||
const updateItem: CartHandlers['updateItem'] = async ({
|
||||
res,
|
||||
body: { cartId, itemId, item },
|
||||
|
@ -8,63 +8,25 @@ import getCart from './handlers/get-cart'
|
||||
import addItem from './handlers/add-item'
|
||||
import updateItem from './handlers/update-item'
|
||||
import removeItem from './handlers/remove-item'
|
||||
|
||||
type OptionSelections = {
|
||||
option_id: Number
|
||||
option_value: Number|String
|
||||
}
|
||||
|
||||
export type ItemBody = {
|
||||
productId: number
|
||||
variantId: number
|
||||
quantity?: number
|
||||
optionSelections?: OptionSelections
|
||||
}
|
||||
|
||||
export type AddItemBody = { item: ItemBody }
|
||||
|
||||
export type UpdateItemBody = { itemId: string; item: ItemBody }
|
||||
|
||||
export type RemoveItemBody = { itemId: string }
|
||||
|
||||
// TODO: this type should match:
|
||||
// https://developer.bigcommerce.com/api-reference/cart-checkout/server-server-cart-api/cart/getacart#responses
|
||||
export type Cart = {
|
||||
id: string
|
||||
parent_id?: string
|
||||
customer_id: number
|
||||
email: string
|
||||
currency: { code: string }
|
||||
tax_included: boolean
|
||||
base_amount: number
|
||||
discount_amount: number
|
||||
cart_amount: number
|
||||
line_items: {
|
||||
custom_items: any[]
|
||||
digital_items: any[]
|
||||
gift_certificates: any[]
|
||||
physical_items: any[]
|
||||
}
|
||||
// TODO: add missing fields
|
||||
}
|
||||
import type {
|
||||
BigcommerceCart,
|
||||
GetCartHandlerBody,
|
||||
AddCartItemHandlerBody,
|
||||
UpdateCartItemHandlerBody,
|
||||
RemoveCartItemHandlerBody,
|
||||
} from '../../types'
|
||||
|
||||
export type CartHandlers = {
|
||||
getCart: BigcommerceHandler<Cart, { cartId?: string }>
|
||||
addItem: BigcommerceHandler<Cart, { cartId?: string } & Partial<AddItemBody>>
|
||||
updateItem: BigcommerceHandler<
|
||||
Cart,
|
||||
{ cartId?: string } & Partial<UpdateItemBody>
|
||||
>
|
||||
removeItem: BigcommerceHandler<
|
||||
Cart,
|
||||
{ cartId?: string } & Partial<RemoveItemBody>
|
||||
>
|
||||
getCart: BigcommerceHandler<BigcommerceCart, GetCartHandlerBody>
|
||||
addItem: BigcommerceHandler<BigcommerceCart, AddCartItemHandlerBody>
|
||||
updateItem: BigcommerceHandler<BigcommerceCart, UpdateCartItemHandlerBody>
|
||||
removeItem: BigcommerceHandler<BigcommerceCart, RemoveCartItemHandlerBody>
|
||||
}
|
||||
|
||||
const METHODS = ['GET', 'POST', 'PUT', 'DELETE']
|
||||
|
||||
// TODO: a complete implementation should have schema validation for `req.body`
|
||||
const cartApi: BigcommerceApiHandler<Cart, CartHandlers> = async (
|
||||
const cartApi: BigcommerceApiHandler<BigcommerceCart, CartHandlers> = async (
|
||||
req,
|
||||
res,
|
||||
config,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import getAllProducts, { ProductEdge } from '../../operations/get-all-products'
|
||||
import { Product } from '@commerce/types'
|
||||
import getAllProducts, { ProductEdge } from '../../../product/get-all-products'
|
||||
import type { ProductsHandlers } from '../products'
|
||||
|
||||
const SORT: { [key: string]: string | undefined } = {
|
||||
@ -6,6 +7,7 @@ const SORT: { [key: string]: string | undefined } = {
|
||||
trending: 'total_sold',
|
||||
price: 'price',
|
||||
}
|
||||
|
||||
const LIMIT = 12
|
||||
|
||||
// Return current cart info
|
||||
@ -44,21 +46,25 @@ const getProducts: ProductsHandlers['getProducts'] = async ({
|
||||
const { data } = await config.storeApiFetch<{ data: { id: number }[] }>(
|
||||
url.pathname + url.search
|
||||
)
|
||||
|
||||
const entityIds = data.map((p) => p.id)
|
||||
const found = entityIds.length > 0
|
||||
|
||||
// We want the GraphQL version of each product
|
||||
const graphqlData = await getAllProducts({
|
||||
variables: { first: LIMIT, entityIds },
|
||||
config,
|
||||
})
|
||||
|
||||
// Put the products in an object that we can use to get them by id
|
||||
const productsById = graphqlData.products.reduce<{
|
||||
[k: number]: ProductEdge
|
||||
[k: number]: Product
|
||||
}>((prods, p) => {
|
||||
prods[p.node.entityId] = p
|
||||
prods[Number(p.id)] = p
|
||||
return prods
|
||||
}, {})
|
||||
const products: ProductEdge[] = found ? [] : graphqlData.products
|
||||
|
||||
const products: Product[] = found ? [] : graphqlData.products
|
||||
|
||||
// Populate the products array with the graphql products, in the order
|
||||
// assigned by the list of entity ids
|
||||
|
@ -1,27 +1,27 @@
|
||||
import type { Product } from '@commerce/types'
|
||||
import isAllowedMethod from '../utils/is-allowed-method'
|
||||
import createApiHandler, {
|
||||
BigcommerceApiHandler,
|
||||
BigcommerceHandler,
|
||||
} from '../utils/create-api-handler'
|
||||
import { BigcommerceApiError } from '../utils/errors'
|
||||
import type { ProductEdge } from '../operations/get-all-products'
|
||||
import getProducts from './handlers/get-products'
|
||||
|
||||
export type SearchProductsData = {
|
||||
products: ProductEdge[]
|
||||
products: Product[]
|
||||
found: boolean
|
||||
}
|
||||
|
||||
export type ProductsHandlers = {
|
||||
getProducts: BigcommerceHandler<
|
||||
SearchProductsData,
|
||||
{ search?: 'string'; category?: string; brand?: string; sort?: string }
|
||||
{ search?: string; category?: string; brand?: string; sort?: string }
|
||||
>
|
||||
}
|
||||
|
||||
const METHODS = ['GET']
|
||||
|
||||
// TODO: a complete implementation should have schema validation for `req.body`
|
||||
// TODO(lf): a complete implementation should have schema validation for `req.body`
|
||||
const productsApi: BigcommerceApiHandler<
|
||||
SearchProductsData,
|
||||
ProductsHandlers
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { FetcherError } from '@commerce/utils/errors'
|
||||
import login from '../../operations/login'
|
||||
import login from '../../../auth/login'
|
||||
import type { LoginHandlers } from '../login'
|
||||
|
||||
const invalidCredentials = /invalid credentials/i
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { BigcommerceApiError } from '../../utils/errors'
|
||||
import login from '../../operations/login'
|
||||
import login from '../../../auth/login'
|
||||
import { SignupHandlers } from '../signup'
|
||||
|
||||
const signup: SignupHandlers['signup'] = async ({
|
||||
|
@ -1,14 +1,28 @@
|
||||
import type { ItemBody as WishlistItemBody } from '../wishlist'
|
||||
import type { ItemBody } from '../cart'
|
||||
import type { CartItemBody, OptionSelections } from '../../types'
|
||||
|
||||
export const parseWishlistItem = (item: WishlistItemBody) => ({
|
||||
product_id: item.productId,
|
||||
variant_id: item.variantId,
|
||||
type BCWishlistItemBody = {
|
||||
product_id: number
|
||||
variant_id: number
|
||||
}
|
||||
|
||||
type BCCartItemBody = {
|
||||
product_id: number
|
||||
variant_id: number
|
||||
quantity?: number
|
||||
option_selections?: OptionSelections
|
||||
}
|
||||
|
||||
export const parseWishlistItem = (
|
||||
item: WishlistItemBody
|
||||
): BCWishlistItemBody => ({
|
||||
product_id: Number(item.productId),
|
||||
variant_id: Number(item.variantId),
|
||||
})
|
||||
|
||||
export const parseCartItem = (item: ItemBody) => ({
|
||||
export const parseCartItem = (item: CartItemBody): BCCartItemBody => ({
|
||||
quantity: item.quantity,
|
||||
product_id: item.productId,
|
||||
variant_id: item.variantId,
|
||||
option_selections: item.optionSelections
|
||||
product_id: Number(item.productId),
|
||||
variant_id: Number(item.variantId),
|
||||
option_selections: item.optionSelections,
|
||||
})
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { ProductNode } from '../operations/get-all-products'
|
||||
import type { ProductNode } from '../../product/get-all-products'
|
||||
import type { RecursivePartial } from './types'
|
||||
|
||||
export default function setProductLocaleMeta(
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { WishlistHandlers } from '..'
|
||||
import getCustomerId from '../../operations/get-customer-id'
|
||||
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
||||
import getCustomerId from '../../../customer/get-customer-id'
|
||||
import getCustomerWishlist from '../../../customer/get-customer-wishlist'
|
||||
import { parseWishlistItem } from '../../utils/parse-item'
|
||||
|
||||
// Returns the wishlist of the signed customer
|
||||
|
@ -1,5 +1,5 @@
|
||||
import getCustomerId from '../../operations/get-customer-id'
|
||||
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
||||
import getCustomerId from '../../../customer/get-customer-id'
|
||||
import getCustomerWishlist from '../../../customer/get-customer-wishlist'
|
||||
import type { Wishlist, WishlistHandlers } from '..'
|
||||
|
||||
// Return wishlist info
|
||||
|
@ -1,7 +1,7 @@
|
||||
import getCustomerId from '../../operations/get-customer-id'
|
||||
import getCustomerId from '../../../customer/get-customer-id'
|
||||
import getCustomerWishlist, {
|
||||
Wishlist,
|
||||
} from '../../operations/get-customer-wishlist'
|
||||
} from '../../../customer/get-customer-wishlist'
|
||||
import type { WishlistHandlers } from '..'
|
||||
|
||||
// Return current wishlist info
|
||||
|
@ -7,24 +7,25 @@ import { BigcommerceApiError } from '../utils/errors'
|
||||
import type {
|
||||
Wishlist,
|
||||
WishlistItem,
|
||||
} from '../operations/get-customer-wishlist'
|
||||
} from '../../customer/get-customer-wishlist'
|
||||
import getWishlist from './handlers/get-wishlist'
|
||||
import addItem from './handlers/add-item'
|
||||
import removeItem from './handlers/remove-item'
|
||||
import type { Product, ProductVariant, Customer } from '@commerce/types'
|
||||
|
||||
export type { Wishlist, WishlistItem }
|
||||
|
||||
export type ItemBody = {
|
||||
productId: number
|
||||
variantId: number
|
||||
productId: Product['id']
|
||||
variantId: ProductVariant['id']
|
||||
}
|
||||
|
||||
export type AddItemBody = { item: ItemBody }
|
||||
|
||||
export type RemoveItemBody = { itemId: string }
|
||||
export type RemoveItemBody = { itemId: Product['id'] }
|
||||
|
||||
export type WishlistBody = {
|
||||
customer_id: number
|
||||
customer_id: Customer['entityId']
|
||||
is_public: number
|
||||
name: string
|
||||
items: any[]
|
||||
|
3
framework/bigcommerce/auth/index.ts
Normal file
3
framework/bigcommerce/auth/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { default as useLogin } from './use-login'
|
||||
export { default as useLogout } from './use-logout'
|
||||
export { default as useSignup } from './use-signup'
|
@ -1,8 +1,8 @@
|
||||
import type { ServerResponse } from 'http'
|
||||
import type { LoginMutation, LoginMutationVariables } from '../../schema'
|
||||
import type { RecursivePartial } from '../utils/types'
|
||||
import concatHeader from '../utils/concat-cookie'
|
||||
import { BigcommerceConfig, getConfig } from '..'
|
||||
import type { LoginMutation, LoginMutationVariables } from '../schema'
|
||||
import type { RecursivePartial } from '../api/utils/types'
|
||||
import concatHeader from '../api/utils/concat-cookie'
|
||||
import { BigcommerceConfig, getConfig } from '../api'
|
||||
|
||||
export const loginMutation = /* GraphQL */ `
|
||||
mutation login($email: String!, $password: String!) {
|
||||
@ -54,7 +54,7 @@ async function login({
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
cookie = cookie.replace('; Secure', '')
|
||||
// SameSite=none can't be set unless the cookie is Secure
|
||||
// bc seems to sometimes send back SameSite=None rather than none so make
|
||||
// bc seems to sometimes send back SameSite=None rather than none so make
|
||||
// this case insensitive
|
||||
cookie = cookie.replace(/; SameSite=none/gi, '; SameSite=lax')
|
||||
}
|
40
framework/bigcommerce/auth/use-login.tsx
Normal file
40
framework/bigcommerce/auth/use-login.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { MutationHook } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useLogin, { UseLogin } from '@commerce/auth/use-login'
|
||||
import type { LoginBody } from '../api/customers/login'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
export default useLogin as UseLogin<typeof handler>
|
||||
|
||||
export const handler: MutationHook<null, {}, LoginBody> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/customers/login',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({ input: { email, password }, options, fetch }) {
|
||||
if (!(email && password)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'A first name, last name, email and password are required to login',
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...options,
|
||||
body: { email, password },
|
||||
})
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { revalidate } = useCustomer()
|
||||
|
||||
return useCallback(
|
||||
async function login(input) {
|
||||
const data = await fetch({ input })
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fetch, revalidate]
|
||||
)
|
||||
},
|
||||
}
|
25
framework/bigcommerce/auth/use-logout.tsx
Normal file
25
framework/bigcommerce/auth/use-logout.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { MutationHook } from '@commerce/utils/types'
|
||||
import useLogout, { UseLogout } from '@commerce/auth/use-logout'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
export default useLogout as UseLogout<typeof handler>
|
||||
|
||||
export const handler: MutationHook<null> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/customers/logout',
|
||||
method: 'GET',
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { mutate } = useCustomer()
|
||||
|
||||
return useCallback(
|
||||
async function logout() {
|
||||
const data = await fetch()
|
||||
await mutate(null, false)
|
||||
return data
|
||||
},
|
||||
[fetch, mutate]
|
||||
)
|
||||
},
|
||||
}
|
44
framework/bigcommerce/auth/use-signup.tsx
Normal file
44
framework/bigcommerce/auth/use-signup.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { MutationHook } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useSignup, { UseSignup } from '@commerce/auth/use-signup'
|
||||
import type { SignupBody } from '../api/customers/signup'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
export default useSignup as UseSignup<typeof handler>
|
||||
|
||||
export const handler: MutationHook<null, {}, SignupBody, SignupBody> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/customers/signup',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({
|
||||
input: { firstName, lastName, email, password },
|
||||
options,
|
||||
fetch,
|
||||
}) {
|
||||
if (!(firstName && lastName && email && password)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'A first name, last name, email and password are required to signup',
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...options,
|
||||
body: { firstName, lastName, email, password },
|
||||
})
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { revalidate } = useCustomer()
|
||||
|
||||
return useCallback(
|
||||
async function signup(input) {
|
||||
const data = await fetch({ input })
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fetch, revalidate]
|
||||
)
|
||||
},
|
||||
}
|
4
framework/bigcommerce/cart/index.ts
Normal file
4
framework/bigcommerce/cart/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export { default as useCart } from './use-cart'
|
||||
export { default as useAddItem } from './use-add-item'
|
||||
export { default as useRemoveItem } from './use-remove-item'
|
||||
export { default as useUpdateItem } from './use-update-item'
|
@ -1,56 +1,50 @@
|
||||
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 useCartAddItem from '@commerce/cart/use-add-item'
|
||||
import type { ItemBody, AddItemBody } from '../api/cart'
|
||||
import useCart, { Cart } from './use-cart'
|
||||
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'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'POST',
|
||||
}
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export type AddItemInput = ItemBody
|
||||
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',
|
||||
})
|
||||
}
|
||||
|
||||
export const fetcher: HookFetcher<Cart, AddItemBody> = (
|
||||
options,
|
||||
{ item },
|
||||
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 fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { item },
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useAddItem = () => {
|
||||
return normalizeCart(data)
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { mutate } = useCart()
|
||||
const fn = useCartAddItem(defaultOpts, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
async function addItem(input: AddItemInput) {
|
||||
const data = await fn({ item: input })
|
||||
async function addItem(input) {
|
||||
const data = await fetch({ input })
|
||||
await mutate(data, false)
|
||||
return data
|
||||
},
|
||||
[fn, mutate]
|
||||
[fetch, mutate]
|
||||
)
|
||||
}
|
||||
|
||||
useAddItem.extend = extendHook
|
||||
|
||||
return useAddItem
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@ -1,13 +0,0 @@
|
||||
import useAddItem from './use-add-item'
|
||||
import useRemoveItem from './use-remove-item'
|
||||
import useUpdateItem from './use-update-item'
|
||||
|
||||
// This hook is probably not going to be used, but it's here
|
||||
// to show how a commerce should be structuring it
|
||||
export default function useCartActions() {
|
||||
const addItem = useAddItem()
|
||||
const updateItem = useUpdateItem()
|
||||
const removeItem = useRemoveItem()
|
||||
|
||||
return { addItem, updateItem, removeItem }
|
||||
}
|
@ -1,50 +1,41 @@
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import type { SwrOptions } from '@commerce/utils/use-data'
|
||||
import useCommerceCart, { CartInput } from '@commerce/cart/use-cart'
|
||||
import type { Cart } from '../api/cart'
|
||||
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'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'GET',
|
||||
}
|
||||
export default useCart as UseCart<typeof handler>
|
||||
|
||||
export type { Cart }
|
||||
|
||||
export const fetcher: HookFetcher<Cart | null, CartInput> = (
|
||||
options,
|
||||
{ cartId },
|
||||
fetch
|
||||
) => {
|
||||
return cartId ? fetch({ ...defaultOpts, ...options }) : null
|
||||
}
|
||||
|
||||
export function extendHook(
|
||||
customFetcher: typeof fetcher,
|
||||
swrOptions?: SwrOptions<Cart | null, CartInput>
|
||||
) {
|
||||
const useCart = () => {
|
||||
const response = useCommerceCart(defaultOpts, [], customFetcher, {
|
||||
revalidateOnFocus: false,
|
||||
...swrOptions,
|
||||
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 },
|
||||
})
|
||||
|
||||
// Uses a getter to only calculate the prop when required
|
||||
// response.data is also a getter and it's better to not trigger it early
|
||||
Object.defineProperty(response, 'isEmpty', {
|
||||
get() {
|
||||
return Object.values(response.data?.line_items ?? {}).every(
|
||||
(items) => !items.length
|
||||
)
|
||||
},
|
||||
set: (x) => x,
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
useCart.extend = extendHook
|
||||
|
||||
return useCart
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.create(response, {
|
||||
isEmpty: {
|
||||
get() {
|
||||
return (response.data?.lineItems.length ?? 0) <= 0
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
}),
|
||||
[response]
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@ -1,51 +1,71 @@
|
||||
import { useCallback } from 'react'
|
||||
import { HookFetcher } from '@commerce/utils/types'
|
||||
import useCartRemoveItem from '@commerce/cart/use-remove-item'
|
||||
import type { RemoveItemBody } from '../api/cart'
|
||||
import useCart, { Cart } from './use-cart'
|
||||
import type {
|
||||
MutationHookContext,
|
||||
HookFetcherContext,
|
||||
} from '@commerce/utils/types'
|
||||
import { ValidationError } from '@commerce/utils/errors'
|
||||
import useRemoveItem, {
|
||||
RemoveItemInput as RemoveItemInputBase,
|
||||
UseRemoveItem,
|
||||
} from '@commerce/cart/use-remove-item'
|
||||
import { normalizeCart } from '../lib/normalize'
|
||||
import type {
|
||||
RemoveCartItemBody,
|
||||
Cart,
|
||||
BigcommerceCart,
|
||||
LineItem,
|
||||
} from '../types'
|
||||
import useCart from './use-cart'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'DELETE',
|
||||
}
|
||||
export type RemoveItemFn<T = any> = T extends LineItem
|
||||
? (input?: RemoveItemInput<T>) => Promise<Cart | null>
|
||||
: (input: RemoveItemInput<T>) => Promise<Cart | null>
|
||||
|
||||
export type RemoveItemInput = {
|
||||
id: string
|
||||
}
|
||||
export type RemoveItemInput<T = any> = T extends LineItem
|
||||
? Partial<RemoveItemInputBase>
|
||||
: RemoveItemInputBase
|
||||
|
||||
export const fetcher: HookFetcher<Cart | null, RemoveItemBody> = (
|
||||
options,
|
||||
{ itemId },
|
||||
fetch
|
||||
) => {
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { itemId },
|
||||
})
|
||||
}
|
||||
export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useRemoveItem = (item?: any) => {
|
||||
export const handler = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'DELETE',
|
||||
},
|
||||
async fetcher({
|
||||
input: { itemId },
|
||||
options,
|
||||
fetch,
|
||||
}: HookFetcherContext<RemoveCartItemBody>) {
|
||||
const data = await fetch<BigcommerceCart>({
|
||||
...options,
|
||||
body: { itemId },
|
||||
})
|
||||
return normalizeCart(data)
|
||||
},
|
||||
useHook: ({
|
||||
fetch,
|
||||
}: MutationHookContext<Cart | null, RemoveCartItemBody>) => <
|
||||
T extends LineItem | undefined = undefined
|
||||
>(
|
||||
ctx: { item?: T } = {}
|
||||
) => {
|
||||
const { item } = ctx
|
||||
const { mutate } = useCart()
|
||||
const fn = useCartRemoveItem<Cart | null, RemoveItemBody>(
|
||||
defaultOpts,
|
||||
customFetcher
|
||||
)
|
||||
const removeItem: RemoveItemFn<LineItem> = async (input) => {
|
||||
const itemId = input?.id ?? item?.id
|
||||
|
||||
return useCallback(
|
||||
async function removeItem(input: RemoveItemInput) {
|
||||
const data = await fn({ itemId: input.id ?? item?.id })
|
||||
await mutate(data, false)
|
||||
return data
|
||||
},
|
||||
[fn, mutate]
|
||||
)
|
||||
}
|
||||
if (!itemId) {
|
||||
throw new ValidationError({
|
||||
message: 'Invalid input used for this operation',
|
||||
})
|
||||
}
|
||||
|
||||
useRemoveItem.extend = extendHook
|
||||
const data = await fetch({ input: { itemId } })
|
||||
await mutate(data, false)
|
||||
return data
|
||||
}
|
||||
|
||||
return useRemoveItem
|
||||
return useCallback(removeItem as RemoveItemFn<T>, [fetch, mutate])
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@ -1,70 +1,97 @@
|
||||
import { useCallback } from 'react'
|
||||
import debounce from 'lodash.debounce'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useCartUpdateItem from '@commerce/cart/use-update-item'
|
||||
import type { ItemBody, UpdateItemBody } from '../api/cart'
|
||||
import { fetcher as removeFetcher } from './use-remove-item'
|
||||
import useCart, { Cart } from './use-cart'
|
||||
import type {
|
||||
MutationHookContext,
|
||||
HookFetcherContext,
|
||||
} from '@commerce/utils/types'
|
||||
import { ValidationError } from '@commerce/utils/errors'
|
||||
import useUpdateItem, {
|
||||
UpdateItemInput as UpdateItemInputBase,
|
||||
UseUpdateItem,
|
||||
} from '@commerce/cart/use-update-item'
|
||||
import { normalizeCart } from '../lib/normalize'
|
||||
import type {
|
||||
UpdateCartItemBody,
|
||||
Cart,
|
||||
BigcommerceCart,
|
||||
LineItem,
|
||||
} from '../types'
|
||||
import { handler as removeItemHandler } from './use-remove-item'
|
||||
import useCart from './use-cart'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'PUT',
|
||||
}
|
||||
export type UpdateItemInput<T = any> = T extends LineItem
|
||||
? Partial<UpdateItemInputBase<LineItem>>
|
||||
: UpdateItemInputBase<LineItem>
|
||||
|
||||
export type UpdateItemInput = Partial<{ id: string } & ItemBody>
|
||||
export default useUpdateItem as UseUpdateItem<typeof handler>
|
||||
|
||||
export const fetcher: HookFetcher<Cart | null, UpdateItemBody> = (
|
||||
options,
|
||||
{ itemId, item },
|
||||
fetch
|
||||
) => {
|
||||
if (Number.isInteger(item.quantity)) {
|
||||
// Also allow the update hook to remove an item if the quantity is lower than 1
|
||||
if (item.quantity! < 1) {
|
||||
return removeFetcher(null, { itemId }, fetch)
|
||||
export const handler = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'PUT',
|
||||
},
|
||||
async fetcher({
|
||||
input: { itemId, item },
|
||||
options,
|
||||
fetch,
|
||||
}: HookFetcherContext<UpdateCartItemBody>) {
|
||||
if (Number.isInteger(item.quantity)) {
|
||||
// Also allow the update hook to remove an item if the quantity is lower than 1
|
||||
if (item.quantity! < 1) {
|
||||
return removeItemHandler.fetcher({
|
||||
options: removeItemHandler.fetchOptions,
|
||||
input: { itemId },
|
||||
fetch,
|
||||
})
|
||||
}
|
||||
} else if (item.quantity) {
|
||||
throw new ValidationError({
|
||||
message: 'The item quantity has to be a valid integer',
|
||||
})
|
||||
}
|
||||
} else if (item.quantity) {
|
||||
throw new CommerceError({
|
||||
message: 'The item quantity has to be a valid integer',
|
||||
|
||||
const data = await fetch<BigcommerceCart, UpdateCartItemBody>({
|
||||
...options,
|
||||
body: { itemId, item },
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { itemId, item },
|
||||
})
|
||||
}
|
||||
|
||||
function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) {
|
||||
const useUpdateItem = (item?: any) => {
|
||||
const { mutate } = useCart()
|
||||
const fn = useCartUpdateItem<Cart | null, UpdateItemBody>(
|
||||
defaultOpts,
|
||||
customFetcher
|
||||
)
|
||||
return normalizeCart(data)
|
||||
},
|
||||
useHook: ({
|
||||
fetch,
|
||||
}: MutationHookContext<Cart | null, UpdateCartItemBody>) => <
|
||||
T extends LineItem | undefined = undefined
|
||||
>(
|
||||
ctx: {
|
||||
item?: T
|
||||
wait?: number
|
||||
} = {}
|
||||
) => {
|
||||
const { item } = ctx
|
||||
const { mutate } = useCart() as any
|
||||
|
||||
return useCallback(
|
||||
debounce(async (input: UpdateItemInput) => {
|
||||
const data = await fn({
|
||||
itemId: input.id ?? item?.id,
|
||||
item: {
|
||||
productId: input.productId ?? item?.product_id,
|
||||
variantId: input.productId ?? item?.variant_id,
|
||||
quantity: input.quantity,
|
||||
debounce(async (input: UpdateItemInput<T>) => {
|
||||
const itemId = input.id ?? item?.id
|
||||
const productId = input.productId ?? item?.productId
|
||||
const variantId = input.productId ?? item?.variantId
|
||||
|
||||
if (!itemId || !productId || !variantId) {
|
||||
throw new ValidationError({
|
||||
message: 'Invalid input used for this operation',
|
||||
})
|
||||
}
|
||||
|
||||
const data = await fetch({
|
||||
input: {
|
||||
itemId,
|
||||
item: { productId, variantId, quantity: input.quantity },
|
||||
},
|
||||
})
|
||||
await mutate(data, false)
|
||||
return data
|
||||
}, cfg?.wait ?? 500),
|
||||
[fn, mutate]
|
||||
}, ctx.wait ?? 500),
|
||||
[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
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||
import { BigcommerceConfig, getConfig } from '..'
|
||||
import { definitions } from '../definitions/store-content'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
||||
import { BigcommerceConfig, getConfig } from '../api'
|
||||
import { definitions } from '../api/definitions/store-content'
|
||||
|
||||
export type Page = definitions['page_Full']
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||
import { BigcommerceConfig, getConfig } from '..'
|
||||
import { definitions } from '../definitions/store-content'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
||||
import { BigcommerceConfig, getConfig } from '../api'
|
||||
import { definitions } from '../api/definitions/store-content'
|
||||
|
||||
export type Page = definitions['page_Full']
|
||||
|
||||
@ -38,9 +38,9 @@ async function getPage({
|
||||
config = getConfig(config)
|
||||
// RecursivePartial forces the method to check for every prop in the data, which is
|
||||
// required in case there's a custom `url`
|
||||
const { data } = await config.storeApiFetch<RecursivePartial<{ data: Page[] }>>(
|
||||
url || `/v3/content/pages?id=${variables.id}&include=body`
|
||||
)
|
||||
const { data } = await config.storeApiFetch<
|
||||
RecursivePartial<{ data: Page[] }>
|
||||
>(url || `/v3/content/pages?id=${variables.id}&include=body`)
|
||||
const firstPage = data?.[0]
|
||||
const page = firstPage as RecursiveRequired<typeof firstPage>
|
||||
|
@ -1,8 +1,8 @@
|
||||
import type { GetSiteInfoQuery, GetSiteInfoQueryVariables } from '../../schema'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||
import filterEdges from '../utils/filter-edges'
|
||||
import { BigcommerceConfig, getConfig } from '..'
|
||||
import { categoryTreeItemFragment } from '../fragments/category-tree'
|
||||
import type { GetSiteInfoQuery, GetSiteInfoQueryVariables } from '../schema'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
||||
import filterEdges from '../api/utils/filter-edges'
|
||||
import { BigcommerceConfig, getConfig } from '../api'
|
||||
import { categoryTreeItemFragment } from '../api/fragments/category-tree'
|
||||
|
||||
// Get 3 levels of categories
|
||||
export const getSiteInfoQuery = /* GraphQL */ `
|
@ -1,5 +1,5 @@
|
||||
import { GetCustomerIdQuery } from '../../schema'
|
||||
import { BigcommerceConfig, getConfig } from '..'
|
||||
import { GetCustomerIdQuery } from '../schema'
|
||||
import { BigcommerceConfig, getConfig } from '../api'
|
||||
|
||||
export const getCustomerIdQuery = /* GraphQL */ `
|
||||
query getCustomerId {
|
@ -1,7 +1,7 @@
|
||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||
import { definitions } from '../definitions/wishlist'
|
||||
import { BigcommerceConfig, getConfig } from '..'
|
||||
import getAllProducts, { ProductEdge } from './get-all-products'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
||||
import { definitions } from '../api/definitions/wishlist'
|
||||
import { BigcommerceConfig, getConfig } from '../api'
|
||||
import getAllProducts, { ProductEdge } from '../product/get-all-products'
|
||||
|
||||
export type Wishlist = Omit<definitions['wishlist_Full'], 'items'> & {
|
||||
items?: WishlistItem[]
|
||||
@ -68,14 +68,15 @@ async function getCustomerWishlist({
|
||||
const productsById = graphqlData.products.reduce<{
|
||||
[k: number]: ProductEdge
|
||||
}>((prods, p) => {
|
||||
prods[p.node.entityId] = p
|
||||
prods[Number(p.id)] = p as any
|
||||
return prods
|
||||
}, {})
|
||||
// Populate the wishlist items with the graphql products
|
||||
wishlist.items.forEach((item) => {
|
||||
const product = item && productsById[item.product_id!]
|
||||
if (item && product) {
|
||||
item.product = product.node
|
||||
// @ts-ignore Fix this type when the wishlist type is properly defined
|
||||
item.product = product
|
||||
}
|
||||
})
|
||||
}
|
1
framework/bigcommerce/customer/index.ts
Normal file
1
framework/bigcommerce/customer/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as useCustomer } from './use-customer'
|
24
framework/bigcommerce/customer/use-customer.tsx
Normal file
24
framework/bigcommerce/customer/use-customer.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { SWRHook } from '@commerce/utils/types'
|
||||
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
|
||||
import type { Customer, CustomerData } from '../api/customers'
|
||||
|
||||
export default useCustomer as UseCustomer<typeof handler>
|
||||
|
||||
export const handler: SWRHook<Customer | null> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/customers',
|
||||
method: 'GET',
|
||||
},
|
||||
async fetcher({ options, fetch }) {
|
||||
const data = await fetch<CustomerData | null>(options)
|
||||
return data?.customer ?? null
|
||||
},
|
||||
useHook: ({ useData }) => (input) => {
|
||||
return useData({
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...input?.swrOptions,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
41
framework/bigcommerce/fetcher.ts
Normal file
41
framework/bigcommerce/fetcher.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { FetcherError } from '@commerce/utils/errors'
|
||||
import type { Fetcher } from '@commerce/utils/types'
|
||||
|
||||
async function getText(res: Response) {
|
||||
try {
|
||||
return (await res.text()) || res.statusText
|
||||
} catch (error) {
|
||||
return res.statusText
|
||||
}
|
||||
}
|
||||
|
||||
async function getError(res: Response) {
|
||||
if (res.headers.get('Content-Type')?.includes('application/json')) {
|
||||
const data = await res.json()
|
||||
return new FetcherError({ errors: data.errors, status: res.status })
|
||||
}
|
||||
return new FetcherError({ message: await getText(res), status: res.status })
|
||||
}
|
||||
|
||||
const fetcher: Fetcher = async ({
|
||||
url,
|
||||
method = 'GET',
|
||||
variables,
|
||||
body: bodyObj,
|
||||
}) => {
|
||||
const hasBody = Boolean(variables || bodyObj)
|
||||
const body = hasBody
|
||||
? JSON.stringify(variables ? { variables } : bodyObj)
|
||||
: undefined
|
||||
const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined
|
||||
const res = await fetch(url!, { method, body, headers })
|
||||
|
||||
if (res.ok) {
|
||||
const { data } = await res.json()
|
||||
return data
|
||||
}
|
||||
|
||||
throw await getError(res)
|
||||
}
|
||||
|
||||
export default fetcher
|
@ -1,46 +1,17 @@
|
||||
import { ReactNode } from 'react'
|
||||
import * as React from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import {
|
||||
CommerceConfig,
|
||||
CommerceProvider as CoreCommerceProvider,
|
||||
useCommerce as useCoreCommerce,
|
||||
} from '@commerce'
|
||||
import { FetcherError } from '@commerce/utils/errors'
|
||||
import { bigcommerceProvider, BigcommerceProvider } from './provider'
|
||||
|
||||
async function getText(res: Response) {
|
||||
try {
|
||||
return (await res.text()) || res.statusText
|
||||
} catch (error) {
|
||||
return res.statusText
|
||||
}
|
||||
}
|
||||
|
||||
async function getError(res: Response) {
|
||||
if (res.headers.get('Content-Type')?.includes('application/json')) {
|
||||
const data = await res.json()
|
||||
return new FetcherError({ errors: data.errors, status: res.status })
|
||||
}
|
||||
return new FetcherError({ message: await getText(res), status: res.status })
|
||||
}
|
||||
export { bigcommerceProvider }
|
||||
export type { BigcommerceProvider }
|
||||
|
||||
export const bigcommerceConfig: CommerceConfig = {
|
||||
locale: 'en-us',
|
||||
cartCookie: 'bc_cartId',
|
||||
async fetcher({ url, method = 'GET', variables, body: bodyObj }) {
|
||||
const hasBody = Boolean(variables || bodyObj)
|
||||
const body = hasBody
|
||||
? JSON.stringify(variables ? { variables } : bodyObj)
|
||||
: undefined
|
||||
const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined
|
||||
const res = await fetch(url!, { method, body, headers })
|
||||
|
||||
if (res.ok) {
|
||||
const { data } = await res.json()
|
||||
return data
|
||||
}
|
||||
|
||||
throw await getError(res)
|
||||
},
|
||||
}
|
||||
|
||||
export type BigcommerceConfig = Partial<CommerceConfig>
|
||||
@ -52,10 +23,13 @@ export type BigcommerceProps = {
|
||||
|
||||
export function CommerceProvider({ children, ...config }: BigcommerceProps) {
|
||||
return (
|
||||
<CoreCommerceProvider config={{ ...bigcommerceConfig, ...config }}>
|
||||
<CoreCommerceProvider
|
||||
provider={bigcommerceProvider}
|
||||
config={{ ...bigcommerceConfig, ...config }}
|
||||
>
|
||||
{children}
|
||||
</CoreCommerceProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useCommerce = () => useCoreCommerce()
|
||||
export const useCommerce = () => useCoreCommerce<BigcommerceProvider>()
|
||||
|
13
framework/bigcommerce/lib/immutability.ts
Normal file
13
framework/bigcommerce/lib/immutability.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import update, { Context } from 'immutability-helper'
|
||||
|
||||
const c = new Context()
|
||||
|
||||
c.extend('$auto', function (value, object) {
|
||||
return object ? c.update(object, value) : c.update({}, value)
|
||||
})
|
||||
|
||||
c.extend('$autoArray', function (value, object) {
|
||||
return object ? c.update(object, value) : c.update([], value)
|
||||
})
|
||||
|
||||
export default c.update
|
113
framework/bigcommerce/lib/normalize.ts
Normal file
113
framework/bigcommerce/lib/normalize.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import type { Product } from '@commerce/types'
|
||||
import type { Cart, BigcommerceCart, LineItem } from '../types'
|
||||
import update from './immutability'
|
||||
|
||||
function normalizeProductOption(productOption: any) {
|
||||
const {
|
||||
node: {
|
||||
entityId,
|
||||
values: { edges },
|
||||
...rest
|
||||
},
|
||||
} = productOption
|
||||
|
||||
return {
|
||||
id: entityId,
|
||||
values: edges?.map(({ node }: any) => node),
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeProduct(productNode: any): Product {
|
||||
const {
|
||||
entityId: id,
|
||||
productOptions,
|
||||
prices,
|
||||
path,
|
||||
id: _,
|
||||
options: _0,
|
||||
} = productNode
|
||||
|
||||
return update(productNode, {
|
||||
id: { $set: String(id) },
|
||||
images: {
|
||||
$apply: ({ edges }: any) =>
|
||||
edges?.map(({ node: { urlOriginal, altText, ...rest } }: any) => ({
|
||||
url: urlOriginal,
|
||||
alt: altText,
|
||||
...rest,
|
||||
})),
|
||||
},
|
||||
variants: {
|
||||
$apply: ({ edges }: any) =>
|
||||
edges?.map(({ node: { entityId, productOptions, ...rest } }: any) => ({
|
||||
id: entityId,
|
||||
options: productOptions?.edges
|
||||
? productOptions.edges.map(normalizeProductOption)
|
||||
: [],
|
||||
...rest,
|
||||
})),
|
||||
},
|
||||
options: {
|
||||
$set: productOptions.edges
|
||||
? productOptions?.edges.map(normalizeProductOption)
|
||||
: [],
|
||||
},
|
||||
brand: {
|
||||
$apply: (brand: any) => (brand?.entityId ? brand?.entityId : null),
|
||||
},
|
||||
slug: {
|
||||
$set: path?.replace(/^\/+|\/+$/g, ''),
|
||||
},
|
||||
price: {
|
||||
$set: {
|
||||
value: prices?.price.value,
|
||||
currencyCode: prices?.price.currencyCode,
|
||||
},
|
||||
},
|
||||
$unset: ['entityId'],
|
||||
})
|
||||
}
|
||||
|
||||
export function normalizeCart(data: BigcommerceCart): Cart {
|
||||
return {
|
||||
id: data.id,
|
||||
customerId: String(data.customer_id),
|
||||
email: data.email,
|
||||
createdAt: data.created_time,
|
||||
currency: data.currency,
|
||||
taxesIncluded: data.tax_included,
|
||||
lineItems: data.line_items.physical_items.map(normalizeLineItem),
|
||||
lineItemsSubtotalPrice: data.base_amount,
|
||||
subtotalPrice: data.base_amount + data.discount_amount,
|
||||
totalPrice: data.cart_amount,
|
||||
discounts: data.discounts?.map((discount) => ({
|
||||
value: discount.discounted_amount,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLineItem(item: any): LineItem {
|
||||
return {
|
||||
id: item.id,
|
||||
variantId: String(item.variant_id),
|
||||
productId: String(item.product_id),
|
||||
name: item.name,
|
||||
quantity: item.quantity,
|
||||
variant: {
|
||||
id: String(item.variant_id),
|
||||
sku: item.sku,
|
||||
name: item.name,
|
||||
image: {
|
||||
url: item.image_url,
|
||||
},
|
||||
requiresShipping: item.is_require_shipping,
|
||||
price: item.sale_price,
|
||||
listPrice: item.list_price,
|
||||
},
|
||||
path: item.url.split('/')[3],
|
||||
discounts: item.discounts.map((discount: any) => ({
|
||||
value: discount.discounted_amount,
|
||||
})),
|
||||
}
|
||||
}
|
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'],
|
||||
},
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import type {
|
||||
GetAllProductPathsQuery,
|
||||
GetAllProductPathsQueryVariables,
|
||||
} from '../../schema'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||
import filterEdges from '../utils/filter-edges'
|
||||
import { BigcommerceConfig, getConfig } from '..'
|
||||
} from '../schema'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
||||
import filterEdges from '../api/utils/filter-edges'
|
||||
import { BigcommerceConfig, getConfig } from '../api'
|
||||
|
||||
export const getAllProductPathsQuery = /* GraphQL */ `
|
||||
query getAllProductPaths($first: Int = 100) {
|
@ -1,12 +1,14 @@
|
||||
import type {
|
||||
GetAllProductsQuery,
|
||||
GetAllProductsQueryVariables,
|
||||
} from '../../schema'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||
import filterEdges from '../utils/filter-edges'
|
||||
import setProductLocaleMeta from '../utils/set-product-locale-meta'
|
||||
import { productConnectionFragment } from '../fragments/product'
|
||||
import { BigcommerceConfig, getConfig } from '..'
|
||||
} from '../schema'
|
||||
import type { Product } from '@commerce/types'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
||||
import filterEdges from '../api/utils/filter-edges'
|
||||
import setProductLocaleMeta from '../api/utils/set-product-locale-meta'
|
||||
import { productConnectionFragment } from '../api/fragments/product'
|
||||
import { BigcommerceConfig, getConfig } from '../api'
|
||||
import { normalizeProduct } from '../lib/normalize'
|
||||
|
||||
export const getAllProductsQuery = /* GraphQL */ `
|
||||
query getAllProducts(
|
||||
@ -72,7 +74,7 @@ async function getAllProducts(opts?: {
|
||||
variables?: ProductVariables
|
||||
config?: BigcommerceConfig
|
||||
preview?: boolean
|
||||
}): Promise<GetAllProductsResult>
|
||||
}): Promise<{ products: Product[] }>
|
||||
|
||||
async function getAllProducts<
|
||||
T extends Record<keyof GetAllProductsResult, any[]>,
|
||||
@ -93,7 +95,8 @@ async function getAllProducts({
|
||||
variables?: ProductVariables
|
||||
config?: BigcommerceConfig
|
||||
preview?: boolean
|
||||
} = {}): Promise<GetAllProductsResult> {
|
||||
// TODO: fix the product type here
|
||||
} = {}): Promise<{ products: Product[] | any[] }> {
|
||||
config = getConfig(config)
|
||||
|
||||
const locale = vars.locale || config.locale
|
||||
@ -126,7 +129,7 @@ async function getAllProducts({
|
||||
})
|
||||
}
|
||||
|
||||
return { products }
|
||||
return { products: products.map(({ node }) => normalizeProduct(node as any)) }
|
||||
}
|
||||
|
||||
export default getAllProducts
|
@ -1,7 +1,9 @@
|
||||
import type { GetProductQuery, GetProductQueryVariables } from '../../schema'
|
||||
import setProductLocaleMeta from '../utils/set-product-locale-meta'
|
||||
import { productInfoFragment } from '../fragments/product'
|
||||
import { BigcommerceConfig, getConfig } from '..'
|
||||
import type { GetProductQuery, GetProductQueryVariables } from '../schema'
|
||||
import setProductLocaleMeta from '../api/utils/set-product-locale-meta'
|
||||
import { productInfoFragment } from '../api/fragments/product'
|
||||
import { BigcommerceConfig, getConfig } from '../api'
|
||||
import { normalizeProduct } from '../lib/normalize'
|
||||
import type { Product } from '@commerce/types'
|
||||
|
||||
export const getProductQuery = /* GraphQL */ `
|
||||
query getProduct(
|
||||
@ -92,7 +94,7 @@ async function getProduct({
|
||||
variables: ProductVariables
|
||||
config?: BigcommerceConfig
|
||||
preview?: boolean
|
||||
}): Promise<GetProductResult> {
|
||||
}): Promise<Product | {} | any> {
|
||||
config = getConfig(config)
|
||||
|
||||
const locale = vars.locale || config.locale
|
||||
@ -109,7 +111,8 @@ async function getProduct({
|
||||
if (locale && config.applyLocale) {
|
||||
setProductLocaleMeta(product)
|
||||
}
|
||||
return { product }
|
||||
|
||||
return { product: normalizeProduct(product as any) }
|
||||
}
|
||||
|
||||
return {}
|
4
framework/bigcommerce/product/index.ts
Normal file
4
framework/bigcommerce/product/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export { default as usePrice } from './use-price'
|
||||
export { default as useSearch } from './use-search'
|
||||
export { default as getProduct } from './get-product'
|
||||
export { default as getAllProducts } from './get-all-products'
|
2
framework/bigcommerce/product/use-price.tsx
Normal file
2
framework/bigcommerce/product/use-price.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
export * from '@commerce/product/use-price'
|
||||
export { default } from '@commerce/product/use-price'
|
53
framework/bigcommerce/product/use-search.tsx
Normal file
53
framework/bigcommerce/product/use-search.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { SWRHook } from '@commerce/utils/types'
|
||||
import useSearch, { UseSearch } from '@commerce/product/use-search'
|
||||
import type { SearchProductsData } from '../api/catalog/products'
|
||||
|
||||
export default useSearch as UseSearch<typeof handler>
|
||||
|
||||
export type SearchProductsInput = {
|
||||
search?: string
|
||||
categoryId?: number
|
||||
brandId?: number
|
||||
sort?: string
|
||||
}
|
||||
|
||||
export const handler: SWRHook<
|
||||
SearchProductsData,
|
||||
SearchProductsInput,
|
||||
SearchProductsInput
|
||||
> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/catalog/products',
|
||||
method: 'GET',
|
||||
},
|
||||
fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) {
|
||||
// Use a dummy base as we only care about the relative path
|
||||
const url = new URL(options.url!, 'http://a')
|
||||
|
||||
if (search) url.searchParams.set('search', search)
|
||||
if (Number.isInteger(categoryId))
|
||||
url.searchParams.set('category', String(categoryId))
|
||||
if (Number.isInteger(brandId))
|
||||
url.searchParams.set('brand', String(brandId))
|
||||
if (sort) url.searchParams.set('sort', sort)
|
||||
|
||||
return fetch({
|
||||
url: url.pathname + url.search,
|
||||
method: options.method,
|
||||
})
|
||||
},
|
||||
useHook: ({ useData }) => (input = {}) => {
|
||||
return useData({
|
||||
input: [
|
||||
['search', input.search],
|
||||
['categoryId', input.categoryId],
|
||||
['brandId', input.brandId],
|
||||
['sort', input.sort],
|
||||
],
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...input.swrOptions,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import type { SwrOptions } from '@commerce/utils/use-data'
|
||||
import useCommerceSearch from '@commerce/products/use-search'
|
||||
import type { SearchProductsData } from '../api/catalog/products'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/catalog/products',
|
||||
method: 'GET',
|
||||
}
|
||||
|
||||
export type SearchProductsInput = {
|
||||
search?: string
|
||||
categoryId?: number
|
||||
brandId?: number
|
||||
sort?: string
|
||||
}
|
||||
|
||||
export const fetcher: HookFetcher<SearchProductsData, SearchProductsInput> = (
|
||||
options,
|
||||
{ search, categoryId, brandId, sort },
|
||||
fetch
|
||||
) => {
|
||||
// Use a dummy base as we only care about the relative path
|
||||
const url = new URL(options?.url ?? defaultOpts.url, 'http://a')
|
||||
|
||||
if (search) url.searchParams.set('search', search)
|
||||
if (Number.isInteger(categoryId))
|
||||
url.searchParams.set('category', String(categoryId))
|
||||
if (Number.isInteger(brandId)) url.searchParams.set('brand', String(brandId))
|
||||
if (sort) url.searchParams.set('sort', sort)
|
||||
|
||||
return fetch({
|
||||
url: url.pathname + url.search,
|
||||
method: options?.method ?? defaultOpts.method,
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(
|
||||
customFetcher: typeof fetcher,
|
||||
swrOptions?: SwrOptions<SearchProductsData, SearchProductsInput>
|
||||
) {
|
||||
const useSearch = (input: SearchProductsInput = {}) => {
|
||||
const response = useCommerceSearch(
|
||||
defaultOpts,
|
||||
[
|
||||
['search', input.search],
|
||||
['categoryId', input.categoryId],
|
||||
['brandId', input.brandId],
|
||||
['sort', input.sort],
|
||||
],
|
||||
customFetcher,
|
||||
{ revalidateOnFocus: false, ...swrOptions }
|
||||
)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
useSearch.extend = extendHook
|
||||
|
||||
return useSearch
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
34
framework/bigcommerce/provider.ts
Normal file
34
framework/bigcommerce/provider.ts
Normal file
@ -0,0 +1,34 @@
|
||||
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
|
58
framework/bigcommerce/types.ts
Normal file
58
framework/bigcommerce/types.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import * as Core from '@commerce/types'
|
||||
|
||||
// TODO: this type should match:
|
||||
// https://developer.bigcommerce.com/api-reference/cart-checkout/server-server-cart-api/cart/getacart#responses
|
||||
export type BigcommerceCart = {
|
||||
id: string
|
||||
parent_id?: string
|
||||
customer_id: number
|
||||
email: string
|
||||
currency: { code: string }
|
||||
tax_included: boolean
|
||||
base_amount: number
|
||||
discount_amount: number
|
||||
cart_amount: number
|
||||
line_items: {
|
||||
custom_items: any[]
|
||||
digital_items: any[]
|
||||
gift_certificates: any[]
|
||||
physical_items: any[]
|
||||
}
|
||||
created_time: string
|
||||
discounts?: { id: number; discounted_amount: number }[]
|
||||
// TODO: add missing fields
|
||||
}
|
||||
|
||||
export type Cart = Core.Cart & {
|
||||
lineItems: LineItem[]
|
||||
}
|
||||
|
||||
export type LineItem = Core.LineItem
|
||||
|
||||
/**
|
||||
* Cart mutations
|
||||
*/
|
||||
|
||||
export type OptionSelections = {
|
||||
option_id: number
|
||||
option_value: number | string
|
||||
}
|
||||
|
||||
export type CartItemBody = Core.CartItemBody & {
|
||||
productId: string // The product id is always required for BC
|
||||
optionSelections?: OptionSelections
|
||||
}
|
||||
|
||||
export type GetCartHandlerBody = Core.GetCartHandlerBody
|
||||
|
||||
export type AddCartItemBody = Core.AddCartItemBody<CartItemBody>
|
||||
|
||||
export type AddCartItemHandlerBody = Core.AddCartItemHandlerBody<CartItemBody>
|
||||
|
||||
export type UpdateCartItemBody = Core.UpdateCartItemBody<CartItemBody>
|
||||
|
||||
export type UpdateCartItemHandlerBody = Core.UpdateCartItemHandlerBody<CartItemBody>
|
||||
|
||||
export type RemoveCartItemBody = Core.RemoveCartItemBody
|
||||
|
||||
export type RemoveCartItemHandlerBody = Core.RemoveCartItemHandlerBody
|
@ -1,38 +0,0 @@
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import type { SwrOptions } from '@commerce/utils/use-data'
|
||||
import useCommerceCustomer from '@commerce/use-customer'
|
||||
import type { Customer, CustomerData } from './api/customers'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/customers',
|
||||
method: 'GET',
|
||||
}
|
||||
|
||||
export type { Customer }
|
||||
|
||||
export const fetcher: HookFetcher<Customer | null> = async (
|
||||
options,
|
||||
_,
|
||||
fetch
|
||||
) => {
|
||||
const data = await fetch<CustomerData | null>({ ...defaultOpts, ...options })
|
||||
return data?.customer ?? null
|
||||
}
|
||||
|
||||
export function extendHook(
|
||||
customFetcher: typeof fetcher,
|
||||
swrOptions?: SwrOptions<Customer | null>
|
||||
) {
|
||||
const useCustomer = () => {
|
||||
return useCommerceCustomer(defaultOpts, [], customFetcher, {
|
||||
revalidateOnFocus: false,
|
||||
...swrOptions,
|
||||
})
|
||||
}
|
||||
|
||||
useCustomer.extend = extendHook
|
||||
|
||||
return useCustomer
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
@ -1,54 +0,0 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useCommerceLogin from '@commerce/use-login'
|
||||
import type { LoginBody } from './api/customers/login'
|
||||
import useCustomer from './use-customer'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/customers/login',
|
||||
method: 'POST',
|
||||
}
|
||||
|
||||
export type LoginInput = LoginBody
|
||||
|
||||
export const fetcher: HookFetcher<null, LoginBody> = (
|
||||
options,
|
||||
{ email, password },
|
||||
fetch
|
||||
) => {
|
||||
if (!(email && password)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'A first name, last name, email and password are required to login',
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { email, password },
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useLogin = () => {
|
||||
const { revalidate } = useCustomer()
|
||||
const fn = useCommerceLogin<null, LoginInput>(defaultOpts, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
async function login(input: LoginInput) {
|
||||
const data = await fn(input)
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fn]
|
||||
)
|
||||
}
|
||||
|
||||
useLogin.extend = extendHook
|
||||
|
||||
return useLogin
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
@ -1,38 +0,0 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import useCommerceLogout from '@commerce/use-logout'
|
||||
import useCustomer from './use-customer'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/customers/logout',
|
||||
method: 'GET',
|
||||
}
|
||||
|
||||
export const fetcher: HookFetcher<null> = (options, _, fetch) => {
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useLogout = () => {
|
||||
const { mutate } = useCustomer()
|
||||
const fn = useCommerceLogout<null>(defaultOpts, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
async function login() {
|
||||
const data = await fn(null)
|
||||
await mutate(null, false)
|
||||
return data
|
||||
},
|
||||
[fn]
|
||||
)
|
||||
}
|
||||
|
||||
useLogout.extend = extendHook
|
||||
|
||||
return useLogout
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
@ -1,2 +0,0 @@
|
||||
export * from '@commerce/use-price'
|
||||
export { default } from '@commerce/use-price'
|
@ -1,54 +0,0 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useCommerceSignup from '@commerce/use-signup'
|
||||
import type { SignupBody } from './api/customers/signup'
|
||||
import useCustomer from './use-customer'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/customers/signup',
|
||||
method: 'POST',
|
||||
}
|
||||
|
||||
export type SignupInput = SignupBody
|
||||
|
||||
export const fetcher: HookFetcher<null, SignupBody> = (
|
||||
options,
|
||||
{ firstName, lastName, email, password },
|
||||
fetch
|
||||
) => {
|
||||
if (!(firstName && lastName && email && password)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'A first name, last name, email and password are required to signup',
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { firstName, lastName, email, password },
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useSignup = () => {
|
||||
const { revalidate } = useCustomer()
|
||||
const fn = useCommerceSignup<null, SignupInput>(defaultOpts, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
async function signup(input: SignupInput) {
|
||||
const data = await fn(input)
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fn]
|
||||
)
|
||||
}
|
||||
|
||||
useSignup.extend = extendHook
|
||||
|
||||
return useSignup
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
3
framework/bigcommerce/wishlist/index.ts
Normal file
3
framework/bigcommerce/wishlist/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { default as useAddItem } from './use-add-item'
|
||||
export { default as useWishlist } from './use-wishlist'
|
||||
export { default as useRemoveItem } from './use-remove-item'
|
@ -1,39 +1,24 @@
|
||||
import { useCallback } from 'react'
|
||||
import { HookFetcher } from '@commerce/utils/types'
|
||||
import type { MutationHook } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useWishlistAddItem from '@commerce/wishlist/use-add-item'
|
||||
import useAddItem, { UseAddItem } from '@commerce/wishlist/use-add-item'
|
||||
import type { ItemBody, AddItemBody } from '../api/wishlist'
|
||||
import useCustomer from '../use-customer'
|
||||
import useWishlist, { UseWishlistOptions, Wishlist } from './use-wishlist'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import useWishlist from './use-wishlist'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'POST',
|
||||
}
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export type AddItemInput = ItemBody
|
||||
|
||||
export const fetcher: HookFetcher<Wishlist, 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?: UseWishlistOptions) => {
|
||||
export const handler: MutationHook<any, {}, ItemBody, AddItemBody> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'POST',
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { data: customer } = useCustomer()
|
||||
const { revalidate } = useWishlist(opts)
|
||||
const fn = useWishlistAddItem(defaultOpts, customFetcher)
|
||||
const { revalidate } = useWishlist()
|
||||
|
||||
return useCallback(
|
||||
async function addItem(input: AddItemInput) {
|
||||
async function addItem(item) {
|
||||
if (!customer) {
|
||||
// A signed customer is required in order to have a wishlist
|
||||
throw new CommerceError({
|
||||
@ -41,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()
|
||||
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 { HookFetcher } from '@commerce/utils/types'
|
||||
import type { MutationHook } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useWishlistRemoveItem from '@commerce/wishlist/use-remove-item'
|
||||
import type { RemoveItemBody } from '../api/wishlist'
|
||||
import useCustomer from '../use-customer'
|
||||
import useWishlist, { UseWishlistOptions, Wishlist } from './use-wishlist'
|
||||
import useRemoveItem, {
|
||||
RemoveItemInput,
|
||||
UseRemoveItem,
|
||||
} from '@commerce/wishlist/use-remove-item'
|
||||
import type { RemoveItemBody, Wishlist } from '../api/wishlist'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import useWishlist, { UseWishlistInput } from './use-wishlist'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'DELETE',
|
||||
}
|
||||
export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||
|
||||
export type RemoveItemInput = {
|
||||
id: string | number
|
||||
}
|
||||
|
||||
export const fetcher: HookFetcher<Wishlist | null, RemoveItemBody> = (
|
||||
options,
|
||||
{ itemId },
|
||||
fetch
|
||||
) => {
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { itemId },
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useRemoveItem = (opts?: UseWishlistOptions) => {
|
||||
export const handler: MutationHook<
|
||||
Wishlist | null,
|
||||
{ wishlist?: UseWishlistInput },
|
||||
RemoveItemInput,
|
||||
RemoveItemBody
|
||||
> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'DELETE',
|
||||
},
|
||||
useHook: ({ fetch }) => ({ wishlist } = {}) => {
|
||||
const { data: customer } = useCustomer()
|
||||
const { revalidate } = useWishlist(opts)
|
||||
const fn = useWishlistRemoveItem<Wishlist | null, RemoveItemBody>(
|
||||
defaultOpts,
|
||||
customFetcher
|
||||
)
|
||||
const { revalidate } = useWishlist(wishlist)
|
||||
|
||||
return useCallback(
|
||||
async function removeItem(input: RemoveItemInput) {
|
||||
async function removeItem(input) {
|
||||
if (!customer) {
|
||||
// A signed customer is required in order to have a wishlist
|
||||
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()
|
||||
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,76 +1,60 @@
|
||||
import { HookFetcher } from '@commerce/utils/types'
|
||||
import { SwrOptions } from '@commerce/utils/use-data'
|
||||
import useCommerceWishlist from '@commerce/wishlist/use-wishlist'
|
||||
import { useMemo } from 'react'
|
||||
import { SWRHook } from '@commerce/utils/types'
|
||||
import useWishlist, { UseWishlist } from '@commerce/wishlist/use-wishlist'
|
||||
import type { Wishlist } from '../api/wishlist'
|
||||
import useCustomer from '../use-customer'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'GET',
|
||||
}
|
||||
export type UseWishlistInput = { includeProducts?: boolean }
|
||||
|
||||
export type { Wishlist }
|
||||
export default useWishlist as UseWishlist<typeof handler>
|
||||
|
||||
export interface UseWishlistOptions {
|
||||
includeProducts?: boolean
|
||||
}
|
||||
export const handler: SWRHook<
|
||||
Wishlist | null,
|
||||
UseWishlistInput,
|
||||
{ customerId?: number } & UseWishlistInput,
|
||||
{ isEmpty?: boolean }
|
||||
> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'GET',
|
||||
},
|
||||
async fetcher({ input: { customerId, includeProducts }, options, fetch }) {
|
||||
if (!customerId) return null
|
||||
|
||||
export interface UseWishlistInput extends UseWishlistOptions {
|
||||
customerId?: number
|
||||
}
|
||||
// Use a dummy base as we only care about the relative path
|
||||
const url = new URL(options.url!, 'http://a')
|
||||
|
||||
export const fetcher: HookFetcher<Wishlist | null, UseWishlistInput> = (
|
||||
options,
|
||||
{ customerId, includeProducts },
|
||||
fetch
|
||||
) => {
|
||||
if (!customerId) return null
|
||||
if (includeProducts) url.searchParams.set('products', '1')
|
||||
|
||||
// Use a dummy base as we only care about the relative path
|
||||
const url = new URL(options?.url ?? defaultOpts.url, 'http://a')
|
||||
|
||||
if (includeProducts) url.searchParams.set('products', '1')
|
||||
|
||||
return fetch({
|
||||
url: url.pathname + url.search,
|
||||
method: options?.method ?? defaultOpts.method,
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(
|
||||
customFetcher: typeof fetcher,
|
||||
swrOptions?: SwrOptions<Wishlist | null, UseWishlistInput>
|
||||
) {
|
||||
const useWishlist = ({ includeProducts }: UseWishlistOptions = {}) => {
|
||||
return fetch({
|
||||
url: url.pathname + url.search,
|
||||
method: options.method,
|
||||
})
|
||||
},
|
||||
useHook: ({ useData }) => (input) => {
|
||||
const { data: customer } = useCustomer()
|
||||
const response = useCommerceWishlist(
|
||||
defaultOpts,
|
||||
[
|
||||
const response = useData({
|
||||
input: [
|
||||
['customerId', customer?.entityId],
|
||||
['includeProducts', includeProducts],
|
||||
['includeProducts', input?.includeProducts],
|
||||
],
|
||||
customFetcher,
|
||||
{
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...swrOptions,
|
||||
}
|
||||
)
|
||||
|
||||
// Uses a getter to only calculate the prop when required
|
||||
// response.data is also a getter and it's better to not trigger it early
|
||||
Object.defineProperty(response, 'isEmpty', {
|
||||
get() {
|
||||
return (response.data?.items?.length || 0) <= 0
|
||||
...input?.swrOptions,
|
||||
},
|
||||
set: (x) => x,
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
useWishlist.extend = extendHook
|
||||
|
||||
return useWishlist
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.create(response, {
|
||||
isEmpty: {
|
||||
get() {
|
||||
return (response.data?.items?.length || 0) <= 0
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
}),
|
||||
[response]
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@ -32,5 +32,3 @@ export interface CommerceAPIFetchOptions<Variables> {
|
||||
variables?: Variables
|
||||
preview?: boolean
|
||||
}
|
||||
|
||||
// TODO: define interfaces for all the available operations and API endpoints
|
||||
|
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
|
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