mirror of
https://github.com/vercel/commerce.git
synced 2025-03-31 17:25:53 +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
@ -3,3 +3,7 @@ BIGCOMMERCE_STOREFRONT_API_TOKEN=
|
|||||||
BIGCOMMERCE_STORE_API_URL=
|
BIGCOMMERCE_STORE_API_URL=
|
||||||
BIGCOMMERCE_STORE_API_TOKEN=
|
BIGCOMMERCE_STORE_API_TOKEN=
|
||||||
BIGCOMMERCE_STORE_API_CLIENT_ID=
|
BIGCOMMERCE_STORE_API_CLIENT_ID=
|
||||||
|
BIGCOMMERCE_CHANNEL_ID=
|
||||||
|
|
||||||
|
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/)
|
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
|
## Features
|
||||||
|
|
||||||
@ -21,26 +22,122 @@ This project is currently <b>under development</b>.
|
|||||||
- Integrations - Integrate seamlessly with the most common ecommerce platforms.
|
- Integrations - Integrate seamlessly with the most common ecommerce platforms.
|
||||||
- Dark Mode Support
|
- Dark Mode Support
|
||||||
|
|
||||||
|
## 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
|
## 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)
|
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.
|
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
|
## Troubleshoot
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@ -57,6 +154,7 @@ BIGCOMMERCE_STOREFRONT_API_TOKEN=<>
|
|||||||
BIGCOMMERCE_STORE_API_URL=<>
|
BIGCOMMERCE_STORE_API_URL=<>
|
||||||
BIGCOMMERCE_STORE_API_TOKEN=<>
|
BIGCOMMERCE_STORE_API_TOKEN=<>
|
||||||
BIGCOMMERCE_STORE_API_CLIENT_ID=<>
|
BIGCOMMERCE_STORE_API_CLIENT_ID=<>
|
||||||
|
BIGCOMMERCE_CHANNEL_ID=<>
|
||||||
```
|
```
|
||||||
|
|
||||||
If your project was started with a "Deploy with Vercel" button, you can use Vercel's CLI to retrieve these credentials.
|
If your project was started with a "Deploy with Vercel" button, you can use Vercel's CLI to retrieve these credentials.
|
||||||
@ -77,22 +175,3 @@ After Email confirmation, Checkout should be manually enabled through BigCommerc
|
|||||||
<br>
|
<br>
|
||||||
BigCommerce team has been notified and they plan to add more detailed about this subject.
|
BigCommerce team has been notified and they plan to add more detailed about this subject.
|
||||||
</details>
|
</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 { FC, useEffect, useState, useCallback } from 'react'
|
||||||
import { Logo, Button, Input } from '@components/ui'
|
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 { useUI } from '@components/ui/context'
|
||||||
import { validate } from 'email-validator'
|
import { validate } from 'email-validator'
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { validate } from 'email-validator'
|
|||||||
import { Info } from '@components/icons'
|
import { Info } from '@components/icons'
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
import { Logo, Button, Input } from '@components/ui'
|
import { Logo, Button, Input } from '@components/ui'
|
||||||
import useSignup from '@framework/use-signup'
|
import useSignup from '@framework/auth/use-signup'
|
||||||
|
|
||||||
interface Props {}
|
interface Props {}
|
||||||
|
|
||||||
|
@ -2,43 +2,51 @@ import { ChangeEvent, useEffect, useState } from 'react'
|
|||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import s from './CartItem.module.css'
|
||||||
import { Trash, Plus, Minus } from '@components/icons'
|
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 useUpdateItem from '@framework/cart/use-update-item'
|
||||||
import useRemoveItem from '@framework/cart/use-remove-item'
|
import useRemoveItem from '@framework/cart/use-remove-item'
|
||||||
import s from './CartItem.module.css'
|
|
||||||
|
|
||||||
type ItemOption = {
|
type ItemOption = {
|
||||||
name: string,
|
name: string
|
||||||
nameId: number,
|
nameId: number
|
||||||
value: string,
|
value: string
|
||||||
valueId: number
|
valueId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const CartItem = ({
|
const CartItem = ({
|
||||||
item,
|
item,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
|
...rest
|
||||||
}: {
|
}: {
|
||||||
item: any
|
item: LineItem
|
||||||
currencyCode: string
|
currencyCode: string
|
||||||
}) => {
|
}) => {
|
||||||
|
const { closeSidebarIfPresent } = useUI()
|
||||||
|
|
||||||
const { price } = usePrice({
|
const { price } = usePrice({
|
||||||
amount: item.extended_sale_price,
|
amount: item.variant.price * item.quantity,
|
||||||
baseAmount: item.extended_list_price,
|
baseAmount: item.variant.listPrice * item.quantity,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
})
|
})
|
||||||
const updateItem = useUpdateItem(item)
|
|
||||||
|
const updateItem = useUpdateItem({ item })
|
||||||
const removeItem = useRemoveItem()
|
const removeItem = useRemoveItem()
|
||||||
const [quantity, setQuantity] = useState(item.quantity)
|
const [quantity, setQuantity] = useState(item.quantity)
|
||||||
const [removing, setRemoving] = useState(false)
|
const [removing, setRemoving] = useState(false)
|
||||||
|
|
||||||
const updateQuantity = async (val: number) => {
|
const updateQuantity = async (val: number) => {
|
||||||
await updateItem({ quantity: val })
|
await updateItem({ quantity: val })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleQuantity = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleQuantity = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const val = Number(e.target.value)
|
const val = Number(e.target.value)
|
||||||
|
|
||||||
if (Number.isInteger(val) && val >= 0) {
|
if (Number.isInteger(val) && val >= 0) {
|
||||||
setQuantity(e.target.value)
|
setQuantity(Number(e.target.value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
@ -62,11 +70,13 @@ const CartItem = ({
|
|||||||
try {
|
try {
|
||||||
// If this action succeeds then there's no need to do `setRemoving(true)`
|
// If this action succeeds then there's no need to do `setRemoving(true)`
|
||||||
// because the component will be removed from the view
|
// because the component will be removed from the view
|
||||||
await removeItem({ id: item.id })
|
await removeItem(item)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setRemoving(false)
|
setRemoving(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// TODO: Add a type for this
|
||||||
|
const options = (item as any).options
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Reset the quantity state if the item quantity changes
|
// 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', {
|
className={cn('flex flex-row space-x-8 py-8', {
|
||||||
'opacity-75 pointer-events-none': removing,
|
'opacity-75 pointer-events-none': removing,
|
||||||
})}
|
})}
|
||||||
|
{...rest}
|
||||||
>
|
>
|
||||||
<div className="w-16 h-16 bg-violet relative overflow-hidden">
|
<div className="w-16 h-16 bg-violet relative overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
className={s.productImage}
|
className={s.productImage}
|
||||||
src={item.image_url}
|
|
||||||
width={150}
|
width={150}
|
||||||
height={150}
|
height={150}
|
||||||
alt="Product Image"
|
src={item.variant.image!.url}
|
||||||
// The cart item image is already optimized and very small in size
|
alt={item.variant.image!.altText}
|
||||||
unoptimized
|
unoptimized
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex flex-col text-base">
|
<div className="flex-1 flex flex-col text-base">
|
||||||
{/** TODO: Replace this. No `path` found at Cart */}
|
<Link href={`/product/${item.path}`}>
|
||||||
<Link href={`/product/${item.url.split('/')[3]}`}>
|
<span
|
||||||
<span className="font-bold text-lg cursor-pointer leading-6">
|
className="font-bold text-lg cursor-pointer leading-6"
|
||||||
|
onClick={() => closeSidebarIfPresent()}
|
||||||
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
{item.options && item.options.length > 0 ? (
|
{options && options.length > 0 ? (
|
||||||
<div className="">
|
<div className="">
|
||||||
{item.options.map((option:ItemOption, i: number) =>
|
{options.map((option: ItemOption, i: number) => (
|
||||||
<span key={`${item.id}-${option.name}`} className="text-sm font-semibold text-accents-7">
|
<span
|
||||||
{option.value}{ i === item.options.length -1 ? "" : ", " }
|
key={`${item.id}-${option.name}`}
|
||||||
|
className="text-sm font-semibold text-accents-7"
|
||||||
|
>
|
||||||
|
{option.value}
|
||||||
|
{i === options.length - 1 ? '' : ', '}
|
||||||
</span>
|
</span>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="flex items-center mt-3">
|
<div className="flex items-center mt-3">
|
||||||
@ -130,7 +146,10 @@ const CartItem = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col justify-between space-y-2 text-base">
|
<div className="flex flex-col justify-between space-y-2 text-base">
|
||||||
<span>{price}</span>
|
<span>{price}</span>
|
||||||
<button className="flex justify-end" onClick={handleRemove}>
|
<button
|
||||||
|
className="flex justify-end outline-none"
|
||||||
|
onClick={handleRemove}
|
||||||
|
>
|
||||||
<Trash />
|
<Trash />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,42 +1,40 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import { UserNav } from '@components/common'
|
import Link from 'next/link'
|
||||||
import { Button } from '@components/ui'
|
|
||||||
import { Bag, Cross, Check } from '@components/icons'
|
|
||||||
import { useUI } from '@components/ui/context'
|
|
||||||
import useCart from '@framework/cart/use-cart'
|
|
||||||
import usePrice from '@framework/use-price'
|
|
||||||
import CartItem from '../CartItem'
|
import CartItem from '../CartItem'
|
||||||
import s from './CartSidebarView.module.css'
|
import s from './CartSidebarView.module.css'
|
||||||
|
import { Button } from '@components/ui'
|
||||||
|
import { UserNav } from '@components/common'
|
||||||
|
import { useUI } from '@components/ui/context'
|
||||||
|
import { Bag, Cross, Check } from '@components/icons'
|
||||||
|
import useCart from '@framework/cart/use-cart'
|
||||||
|
import usePrice from '@framework/product/use-price'
|
||||||
|
|
||||||
const CartSidebarView: FC = () => {
|
const CartSidebarView: FC = () => {
|
||||||
const { closeSidebar } = useUI()
|
const { closeSidebar } = useUI()
|
||||||
const { data, isEmpty } = useCart()
|
const { data, isLoading, isEmpty } = useCart()
|
||||||
|
|
||||||
const { price: subTotal } = usePrice(
|
const { price: subTotal } = usePrice(
|
||||||
data && {
|
data && {
|
||||||
amount: data.base_amount,
|
amount: Number(data.subtotalPrice),
|
||||||
currencyCode: data.currency.code,
|
currencyCode: data.currency.code,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
const { price: total } = usePrice(
|
const { price: total } = usePrice(
|
||||||
data && {
|
data && {
|
||||||
amount: data.cart_amount,
|
amount: Number(data.totalPrice),
|
||||||
currencyCode: data.currency.code,
|
currencyCode: data.currency.code,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
const handleClose = () => closeSidebar()
|
const handleClose = () => closeSidebar()
|
||||||
|
|
||||||
const items = data?.line_items.physical_items ?? []
|
|
||||||
|
|
||||||
const error = null
|
const error = null
|
||||||
const success = null
|
const success = null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(s.root, {
|
className={cn(s.root, {
|
||||||
[s.empty]: error,
|
[s.empty]: error || success || isLoading || isEmpty,
|
||||||
[s.empty]: success,
|
|
||||||
[s.empty]: isEmpty,
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<header className="px-4 pt-6 pb-4 sm:px-6">
|
<header className="px-4 pt-6 pb-4 sm:px-6">
|
||||||
@ -51,12 +49,12 @@ const CartSidebarView: FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<UserNav className="" />
|
<UserNav />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{isEmpty ? (
|
{isLoading || isEmpty ? (
|
||||||
<div className="flex-1 px-4 flex flex-col justify-center items-center">
|
<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">
|
<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" />
|
<Bag className="absolute" />
|
||||||
@ -90,15 +88,20 @@ const CartSidebarView: FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="px-4 sm:px-6 flex-1">
|
<div className="px-4 sm:px-6 flex-1">
|
||||||
<h2 className="pt-1 pb-4 text-2xl leading-7 font-bold text-base tracking-wide">
|
<Link href="/cart">
|
||||||
My Cart
|
<h2
|
||||||
</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">
|
<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
|
<CartItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
currencyCode={data?.currency.code!}
|
currencyCode={data!.currency.code}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -2,7 +2,7 @@ import { FC } from 'react'
|
|||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/router'
|
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 getSlug from '@lib/get-slug'
|
||||||
import { Github, Vercel } from '@components/icons'
|
import { Github, Vercel } from '@components/icons'
|
||||||
import { Logo, Container } from '@components/ui'
|
import { Logo, Container } from '@components/ui'
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import type { Product } from '@commerce/types'
|
||||||
import { Grid } from '@components/ui'
|
import { Grid } from '@components/ui'
|
||||||
import { ProductCard } from '@components/product'
|
import { ProductCard } from '@components/product'
|
||||||
import s from './HomeAllProductsGrid.module.css'
|
import s from './HomeAllProductsGrid.module.css'
|
||||||
@ -8,10 +9,14 @@ import { getCategoryPath, getDesignerPath } from '@lib/search'
|
|||||||
interface Props {
|
interface Props {
|
||||||
categories?: any
|
categories?: any
|
||||||
brands?: any
|
brands?: any
|
||||||
newestProducts?: any
|
products?: Product[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const Head: FC<Props> = ({ categories, brands, newestProducts }) => {
|
const HomeAllProductsGrid: FC<Props> = ({
|
||||||
|
categories,
|
||||||
|
brands,
|
||||||
|
products = [],
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={s.root}>
|
<div className={s.root}>
|
||||||
<div className={s.asideWrapper}>
|
<div className={s.asideWrapper}>
|
||||||
@ -48,13 +53,15 @@ const Head: FC<Props> = ({ categories, brands, newestProducts }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Grid layout="normal">
|
<Grid layout="normal">
|
||||||
{newestProducts.map(({ node }: any) => (
|
{products.map((product) => (
|
||||||
<ProductCard
|
<ProductCard
|
||||||
key={node.path}
|
key={product.path}
|
||||||
product={node}
|
product={product}
|
||||||
variant="simple"
|
variant="simple"
|
||||||
imgWidth={480}
|
imgProps={{
|
||||||
imgHeight={480}
|
width: 480,
|
||||||
|
height: 480,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</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
|
const currentLocale = locale || defaultLocale
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClickOutside active={display} onClick={() => setDisplay(false)} >
|
<ClickOutside active={display} onClick={() => setDisplay(false)}>
|
||||||
<nav className={s.root}>
|
<nav className={s.root}>
|
||||||
<div
|
<div
|
||||||
className="flex items-center relative"
|
className="flex items-center relative"
|
||||||
|
@ -7,12 +7,11 @@ import { useUI } from '@components/ui/context'
|
|||||||
import { Navbar, Footer } from '@components/common'
|
import { Navbar, Footer } from '@components/common'
|
||||||
import { useAcceptCookies } from '@lib/hooks/useAcceptCookies'
|
import { useAcceptCookies } from '@lib/hooks/useAcceptCookies'
|
||||||
import { Sidebar, Button, Modal, LoadingDots } from '@components/ui'
|
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 LoginView from '@components/auth/LoginView'
|
||||||
import { CommerceProvider } from '@framework'
|
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 = () => (
|
const Loading = () => (
|
||||||
<div className="w-80 h-80 flex items-center text-center justify-center p-3">
|
<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'),
|
() => import('@components/auth/SignUpView'),
|
||||||
dynamicProps
|
dynamicProps
|
||||||
)
|
)
|
||||||
|
|
||||||
const ForgotPassword = dynamic(
|
const ForgotPassword = dynamic(
|
||||||
() => import('@components/auth/ForgotPassword'),
|
() => import('@components/auth/ForgotPassword'),
|
||||||
dynamicProps
|
dynamicProps
|
||||||
)
|
)
|
||||||
|
|
||||||
const FeatureBar = dynamic(
|
const FeatureBar = dynamic(
|
||||||
() => import('@components/common/FeatureBar'),
|
() => import('@components/common/FeatureBar'),
|
||||||
dynamicProps
|
dynamicProps
|
||||||
@ -40,10 +41,14 @@ const FeatureBar = dynamic(
|
|||||||
interface Props {
|
interface Props {
|
||||||
pageProps: {
|
pageProps: {
|
||||||
pages?: Page[]
|
pages?: Page[]
|
||||||
|
commerceFeatures: Record<string, boolean>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Layout: FC<Props> = ({ children, pageProps }) => {
|
const Layout: FC<Props> = ({
|
||||||
|
children,
|
||||||
|
pageProps: { commerceFeatures, ...pageProps },
|
||||||
|
}) => {
|
||||||
const {
|
const {
|
||||||
displaySidebar,
|
displaySidebar,
|
||||||
displayModal,
|
displayModal,
|
||||||
@ -53,7 +58,6 @@ const Layout: FC<Props> = ({ children, pageProps }) => {
|
|||||||
} = useUI()
|
} = useUI()
|
||||||
const { acceptedCookies, onAcceptCookies } = useAcceptCookies()
|
const { acceptedCookies, onAcceptCookies } = useAcceptCookies()
|
||||||
const { locale = 'en-US' } = useRouter()
|
const { locale = 'en-US' } = useRouter()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommerceProvider locale={locale}>
|
<CommerceProvider locale={locale}>
|
||||||
<div className={cn(s.root)}>
|
<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 Link from 'next/link'
|
||||||
import s from './Navbar.module.css'
|
|
||||||
import { Logo, Container } from '@components/ui'
|
import { Logo, Container } from '@components/ui'
|
||||||
import { Searchbar, UserNav } from '@components/common'
|
import { Searchbar, UserNav } from '@components/common'
|
||||||
import cn from 'classnames'
|
import NavbarRoot from './NavbarRoot'
|
||||||
import throttle from 'lodash.throttle'
|
import s from './Navbar.module.css'
|
||||||
|
|
||||||
const Navbar: FC = () => {
|
const Navbar: FC = () => (
|
||||||
const [hasScrolled, setHasScrolled] = useState(false)
|
<NavbarRoot>
|
||||||
|
<Container>
|
||||||
useEffect(() => {
|
<div className="relative flex flex-row justify-between py-4 align-center md:py-6">
|
||||||
const handleScroll = throttle(() => {
|
<div className="flex items-center flex-1">
|
||||||
const offset = 0
|
<Link href="/">
|
||||||
const { scrollTop } = document.documentElement
|
<a className={s.logo} aria-label="Logo">
|
||||||
const scrolled = scrollTop > offset
|
<Logo />
|
||||||
setHasScrolled(scrolled)
|
</a>
|
||||||
}, 200)
|
</Link>
|
||||||
|
<nav className="hidden ml-6 space-x-4 lg:block">
|
||||||
document.addEventListener('scroll', handleScroll)
|
<Link href="/search">
|
||||||
return () => {
|
<a className={s.link}>All</a>
|
||||||
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>
|
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="hidden ml-6 space-x-4 lg:block">
|
<Link href="/search?q=clothes">
|
||||||
<Link href="/search">
|
<a className={s.link}>Clothes</a>
|
||||||
<a className={s.link}>All</a>
|
</Link>
|
||||||
</Link>
|
<Link href="/search?q=accessories">
|
||||||
<Link href="/search?q=clothes">
|
<a className={s.link}>Accessories</a>
|
||||||
<a className={s.link}>Clothes</a>
|
</Link>
|
||||||
</Link>
|
<Link href="/search?q=shoes">
|
||||||
<Link href="/search?q=accessories">
|
<a className={s.link}>Shoes</a>
|
||||||
<a className={s.link}>Accessories</a>
|
</Link>
|
||||||
</Link>
|
</nav>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex pb-4 lg:px-6 lg:hidden">
|
<div className="justify-center flex-1 hidden lg:flex">
|
||||||
<Searchbar id="mobile-search" />
|
<Searchbar />
|
||||||
</div>
|
</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
|
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 { Moon, Sun } from '@components/icons'
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
import ClickOutside from '@lib/click-outside'
|
import ClickOutside from '@lib/click-outside'
|
||||||
|
import useLogout from '@framework/auth/use-logout'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
disableBodyScroll,
|
disableBodyScroll,
|
||||||
@ -15,8 +16,6 @@ import {
|
|||||||
clearAllBodyScrollLocks,
|
clearAllBodyScrollLocks,
|
||||||
} from 'body-scroll-lock'
|
} from 'body-scroll-lock'
|
||||||
|
|
||||||
import useLogout from '@framework/use-logout'
|
|
||||||
|
|
||||||
interface DropdownMenuProps {
|
interface DropdownMenuProps {
|
||||||
open?: boolean
|
open?: boolean
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,26 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
|
import type { LineItem } from '@framework/types'
|
||||||
import useCart from '@framework/cart/use-cart'
|
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 { Heart, Bag } from '@components/icons'
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
import DropdownMenu from './DropdownMenu'
|
import DropdownMenu from './DropdownMenu'
|
||||||
import s from './UserNav.module.css'
|
import s from './UserNav.module.css'
|
||||||
import { Avatar } from '@components/common'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const countItem = (count: number, item: any) => count + item.quantity
|
const countItem = (count: number, item: LineItem) => count + item.quantity
|
||||||
const countItems = (count: number, items: any[]) =>
|
|
||||||
items.reduce(countItem, count)
|
|
||||||
|
|
||||||
const UserNav: FC<Props> = ({ className, children, ...props }) => {
|
const UserNav: FC<Props> = ({ className }) => {
|
||||||
const { data } = useCart()
|
const { data } = useCart()
|
||||||
const { data: customer } = useCustomer()
|
const { data: customer } = useCustomer()
|
||||||
const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI()
|
const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI()
|
||||||
const itemsCount = Object.values(data?.line_items ?? {}).reduce(countItems, 0)
|
const itemsCount = data?.lineItems.reduce(countItem, 0) ?? 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={cn(s.root, className)}>
|
<nav className={cn(s.root, className)}>
|
||||||
@ -31,13 +30,15 @@ const UserNav: FC<Props> = ({ className, children, ...props }) => {
|
|||||||
<Bag />
|
<Bag />
|
||||||
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
|
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
|
||||||
</li>
|
</li>
|
||||||
<li className={s.item}>
|
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
||||||
<Link href="/wishlist">
|
<li className={s.item}>
|
||||||
<a onClick={closeSidebarIfPresent} aria-label="Wishlist">
|
<Link href="/wishlist">
|
||||||
<Heart />
|
<a onClick={closeSidebarIfPresent} aria-label="Wishlist">
|
||||||
</a>
|
<Heart />
|
||||||
</Link>
|
</a>
|
||||||
</li>
|
</Link>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
<li className={s.item}>
|
<li className={s.item}>
|
||||||
{customer ? (
|
{customer ? (
|
||||||
<DropdownMenu />
|
<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 }) => {
|
const Vercel = ({ ...props }) => {
|
||||||
return (
|
return (
|
||||||
<svg width="89" height="20" viewBox="0 0 89 20" fill="none" xmlns="http://www.w3.org/2000/svg" { ...props }>
|
<svg
|
||||||
<path d="M11.5625 0L23.125 20H0L11.5625 0Z" fill="currentColor"/>
|
width="89"
|
||||||
<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"/>
|
height="20"
|
||||||
<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"/>
|
viewBox="0 0 89 20"
|
||||||
<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"/>
|
fill="none"
|
||||||
<path d="M88.2188 1.75H85.7188V15.8125H88.2188V1.75Z" fill="currentColor"/>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path d="M40.1563 1.75H37.2813L31.7813 11.25L26.2813 1.75H23.375L31.7813 16.25L40.1563 1.75Z" fill="currentColor"/>
|
{...props}
|
||||||
<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>
|
<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 Info } from './Info'
|
||||||
export { default as ChevronUp } from './ChevronUp'
|
export { default as ChevronUp } from './ChevronUp'
|
||||||
export { default as Vercel } from './Vercel'
|
export { default as Vercel } from './Vercel'
|
||||||
|
export { default as MapPin } from './MapPin'
|
||||||
|
export { default as CreditCard } from './CreditCard'
|
||||||
|
@ -1,103 +1,88 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Image from 'next/image'
|
import type { Product } from '@commerce/types'
|
||||||
import type { FC } from 'react'
|
|
||||||
import s from './ProductCard.module.css'
|
import s from './ProductCard.module.css'
|
||||||
|
import Image, { ImageProps } from 'next/image'
|
||||||
import WishlistButton from '@components/wishlist/WishlistButton'
|
import WishlistButton from '@components/wishlist/WishlistButton'
|
||||||
|
|
||||||
import usePrice from '@framework/use-price'
|
|
||||||
import type { ProductNode } from '@framework/api/operations/get-all-products'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
product: ProductNode
|
product: Product
|
||||||
variant?: 'slim' | 'simple'
|
variant?: 'slim' | 'simple'
|
||||||
imgWidth: number | string
|
imgProps?: Omit<ImageProps, 'src'>
|
||||||
imgHeight: number | string
|
|
||||||
imgLayout?: 'fixed' | 'intrinsic' | 'responsive' | undefined
|
|
||||||
imgPriority?: boolean
|
|
||||||
imgLoading?: 'eager' | 'lazy'
|
|
||||||
imgSizes?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const placeholderImg = '/product-img-placeholder.svg'
|
||||||
|
|
||||||
const ProductCard: FC<Props> = ({
|
const ProductCard: FC<Props> = ({
|
||||||
className,
|
className,
|
||||||
product: p,
|
product,
|
||||||
variant,
|
variant,
|
||||||
imgWidth,
|
imgProps,
|
||||||
imgHeight,
|
...props
|
||||||
imgPriority,
|
}) => (
|
||||||
imgLoading,
|
<Link href={`/product/${product.slug}`} {...props}>
|
||||||
imgSizes,
|
<a className={cn(s.root, { [s.simple]: variant === 'simple' }, className)}>
|
||||||
imgLayout = 'responsive',
|
{variant === 'slim' ? (
|
||||||
}) => {
|
<div className="relative overflow-hidden box-border">
|
||||||
const src = p.images.edges?.[0]?.node?.urlOriginal!
|
<div className="absolute inset-0 flex items-center justify-end mr-8 z-20">
|
||||||
const placeholderImg = '/product-img-placeholder.svg';
|
<span className="bg-black text-white inline-block p-3 font-bold text-xl break-words">
|
||||||
const { price } = usePrice({
|
{product.name}
|
||||||
amount: p.prices?.price?.value,
|
</span>
|
||||||
baseAmount: p.prices?.retailPrice?.value,
|
</div>
|
||||||
currencyCode: p.prices?.price?.currencyCode!,
|
{product?.images && (
|
||||||
})
|
|
||||||
|
|
||||||
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>
|
|
||||||
<Image
|
<Image
|
||||||
quality="85"
|
quality="85"
|
||||||
width={imgWidth}
|
src={product.images[0].url || placeholderImg}
|
||||||
sizes={imgSizes}
|
alt={product.name || 'Product Image'}
|
||||||
height={imgHeight}
|
height={320}
|
||||||
layout={imgLayout}
|
width={320}
|
||||||
loading={imgLoading}
|
layout="fixed"
|
||||||
priority={imgPriority}
|
{...imgProps}
|
||||||
src={p.images.edges?.[0]?.node.urlOriginal! || placeholderImg}
|
|
||||||
alt={p.images.edges?.[0]?.node.altText || 'Product Image'}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
) : (
|
</div>
|
||||||
<>
|
) : (
|
||||||
<div className={s.squareBg} />
|
<>
|
||||||
<div className="flex flex-row justify-between box-border w-full z-20 absolute">
|
<div className={s.squareBg} />
|
||||||
<div className="absolute top-0 left-0 pr-16 max-w-full">
|
<div className="flex flex-row justify-between box-border w-full z-20 absolute">
|
||||||
<h3 className={s.productTitle}>
|
<div className="absolute top-0 left-0 pr-16 max-w-full">
|
||||||
<span>{p.name}</span>
|
<h3 className={s.productTitle}>
|
||||||
</h3>
|
<span>{product.name}</span>
|
||||||
<span className={s.productPrice}>{price}</span>
|
</h3>
|
||||||
</div>
|
<span className={s.productPrice}>
|
||||||
|
{product.price.value}
|
||||||
|
|
||||||
|
{product.price.currencyCode}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
||||||
<WishlistButton
|
<WishlistButton
|
||||||
className={s.wishlistButton}
|
className={s.wishlistButton}
|
||||||
productId={p.entityId}
|
productId={product.id}
|
||||||
variant={p.variants.edges?.[0]!}
|
variant={product.variants[0] as any}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
<div className={s.imageContainer}>
|
</div>
|
||||||
|
<div className={s.imageContainer}>
|
||||||
|
{product?.images && (
|
||||||
<Image
|
<Image
|
||||||
quality="85"
|
alt={product.name || 'Product Image'}
|
||||||
src={src || placeholderImg}
|
|
||||||
alt={p.name}
|
|
||||||
className={s.productImage}
|
className={s.productImage}
|
||||||
width={imgWidth}
|
src={product.images[0].url || placeholderImg}
|
||||||
sizes={imgSizes}
|
height={540}
|
||||||
height={imgHeight}
|
width={540}
|
||||||
layout={imgLayout}
|
quality="85"
|
||||||
loading={imgLoading}
|
layout="responsive"
|
||||||
priority={imgPriority}
|
{...imgProps}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
</>
|
</div>
|
||||||
)}
|
</>
|
||||||
</a>
|
)}
|
||||||
</Link>
|
</a>
|
||||||
)
|
</Link>
|
||||||
}
|
)
|
||||||
|
|
||||||
export default ProductCard
|
export default ProductCard
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
import { useKeenSlider } from 'keen-slider/react'
|
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 cn from 'classnames'
|
||||||
|
|
||||||
import s from './ProductSlider.module.css'
|
import s from './ProductSlider.module.css'
|
||||||
@ -33,15 +40,22 @@ const ProductSlider: FC = ({ children }) => {
|
|||||||
if (
|
if (
|
||||||
touchXPosition - touchXRadius < 10 ||
|
touchXPosition - touchXRadius < 10 ||
|
||||||
touchXPosition + touchXRadius > window.innerWidth - 10
|
touchXPosition + touchXRadius > window.innerWidth - 10
|
||||||
) event.preventDefault()
|
)
|
||||||
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
sliderContainerRef.current!
|
sliderContainerRef.current!.addEventListener(
|
||||||
.addEventListener('touchstart', preventNavigation)
|
'touchstart',
|
||||||
|
preventNavigation
|
||||||
|
)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
sliderContainerRef.current!
|
if (sliderContainerRef.current) {
|
||||||
.removeEventListener('touchstart', preventNavigation)
|
sliderContainerRef.current!.removeEventListener(
|
||||||
|
'touchstart',
|
||||||
|
preventNavigation
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.productDisplay {
|
.productDisplay {
|
||||||
@apply relative flex px-0 pb-0 relative box-border col-span-1 bg-violet;
|
@apply relative flex px-0 pb-0 box-border col-span-1 bg-violet;
|
||||||
min-height: 600px;
|
min-height: 600px;
|
||||||
|
|
||||||
@screen md {
|
@screen md {
|
||||||
|
@ -1,51 +1,48 @@
|
|||||||
import { FC, useState } from 'react'
|
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { NextSeo } from 'next-seo'
|
import { NextSeo } from 'next-seo'
|
||||||
|
import { FC, useState } from 'react'
|
||||||
import s from './ProductView.module.css'
|
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 { Swatch, ProductSlider } from '@components/product'
|
||||||
import useAddItem from '@framework/cart/use-add-item'
|
import { Button, Container, Text, useUI } from '@components/ui'
|
||||||
import type { ProductNode } from '@framework/api/operations/get-product'
|
|
||||||
import {
|
import type { Product } from '@commerce/types'
|
||||||
getCurrentVariant,
|
import usePrice from '@framework/product/use-price'
|
||||||
getProductOptions,
|
import { useAddItem } from '@framework/cart'
|
||||||
SelectedOptions,
|
|
||||||
} from '../helpers'
|
import { getVariant, SelectedOptions } from '../helpers'
|
||||||
import WishlistButton from '@components/wishlist/WishlistButton'
|
import WishlistButton from '@components/wishlist/WishlistButton'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
children?: any
|
children?: any
|
||||||
product: ProductNode
|
product: Product
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductView: FC<Props> = ({ product }) => {
|
const ProductView: FC<Props> = ({ product }) => {
|
||||||
const addItem = useAddItem()
|
const addItem = useAddItem()
|
||||||
const { price } = usePrice({
|
const { price } = usePrice({
|
||||||
amount: product.prices?.price?.value,
|
amount: product.price.value,
|
||||||
baseAmount: product.prices?.retailPrice?.value,
|
baseAmount: product.price.retailPrice,
|
||||||
currencyCode: product.prices?.price?.currencyCode!,
|
currencyCode: product.price.currencyCode!,
|
||||||
})
|
})
|
||||||
const { openSidebar } = useUI()
|
const { openSidebar } = useUI()
|
||||||
const options = getProductOptions(product)
|
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [choices, setChoices] = useState<SelectedOptions>({
|
const [choices, setChoices] = useState<SelectedOptions>({
|
||||||
size: null,
|
size: null,
|
||||||
color: null,
|
color: null,
|
||||||
})
|
})
|
||||||
const variant = getCurrentVariant(product, choices)
|
|
||||||
|
// Select the correct variant based on choices
|
||||||
|
const variant = getVariant(product, choices)
|
||||||
|
|
||||||
const addToCart = async () => {
|
const addToCart = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
await addItem({
|
await addItem({
|
||||||
productId: product.entityId,
|
productId: String(product.id),
|
||||||
variantId: variant?.node.entityId!,
|
variantId: String(variant ? variant.id : product.variants[0].id),
|
||||||
})
|
})
|
||||||
openSidebar()
|
openSidebar()
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@ -65,7 +62,7 @@ const ProductView: FC<Props> = ({ product }) => {
|
|||||||
description: product.description,
|
description: product.description,
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url: product.images.edges?.[0]?.node.urlOriginal!,
|
url: product.images[0]?.url!,
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 600,
|
height: 600,
|
||||||
alt: product.name,
|
alt: product.name,
|
||||||
@ -80,18 +77,18 @@ const ProductView: FC<Props> = ({ product }) => {
|
|||||||
<div className={s.price}>
|
<div className={s.price}>
|
||||||
{price}
|
{price}
|
||||||
{` `}
|
{` `}
|
||||||
{product.prices?.price.currencyCode}
|
{product.price?.currencyCode}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={s.sliderContainer}>
|
<div className={s.sliderContainer}>
|
||||||
<ProductSlider key={product.entityId}>
|
<ProductSlider key={product.id}>
|
||||||
{product.images.edges?.map((image, i) => (
|
{product.images.map((image, i) => (
|
||||||
<div key={image?.node.urlOriginal} className={s.imageContainer}>
|
<div key={image.url} className={s.imageContainer}>
|
||||||
<Image
|
<Image
|
||||||
className={s.img}
|
className={s.img}
|
||||||
src={image?.node.urlOriginal!}
|
src={image.url!}
|
||||||
alt={image?.node.altText || 'Product Image'}
|
alt={image.alt || 'Product Image'}
|
||||||
width={1050}
|
width={1050}
|
||||||
height={1050}
|
height={1050}
|
||||||
priority={i === 0}
|
priority={i === 0}
|
||||||
@ -102,20 +99,21 @@ const ProductView: FC<Props> = ({ product }) => {
|
|||||||
</ProductSlider>
|
</ProductSlider>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={s.sidebar}>
|
<div className={s.sidebar}>
|
||||||
<section>
|
<section>
|
||||||
{options?.map((opt: any) => (
|
{product.options?.map((opt) => (
|
||||||
<div className="pb-4" key={opt.displayName}>
|
<div className="pb-4" key={opt.displayName}>
|
||||||
<h2 className="uppercase font-medium">{opt.displayName}</h2>
|
<h2 className="uppercase font-medium">{opt.displayName}</h2>
|
||||||
<div className="flex flex-row py-4">
|
<div className="flex flex-row py-4">
|
||||||
{opt.values.map((v: any, i: number) => {
|
{opt.values.map((v, i: number) => {
|
||||||
const active = (choices as any)[opt.displayName]
|
const active = (choices as any)[
|
||||||
|
opt.displayName.toLowerCase()
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Swatch
|
<Swatch
|
||||||
key={`${v.entityId}-${i}`}
|
key={`${opt.id}-${i}`}
|
||||||
active={v.label === active}
|
active={v.label.toLowerCase() === active}
|
||||||
variant={opt.displayName}
|
variant={opt.displayName}
|
||||||
color={v.hexColors ? v.hexColors[0] : ''}
|
color={v.hexColors ? v.hexColors[0] : ''}
|
||||||
label={v.label}
|
label={v.label}
|
||||||
@ -123,7 +121,7 @@ const ProductView: FC<Props> = ({ product }) => {
|
|||||||
setChoices((choices) => {
|
setChoices((choices) => {
|
||||||
return {
|
return {
|
||||||
...choices,
|
...choices,
|
||||||
[opt.displayName]: v.label,
|
[opt.displayName.toLowerCase()]: v.label.toLowerCase(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
@ -145,18 +143,19 @@ const ProductView: FC<Props> = ({ product }) => {
|
|||||||
className={s.button}
|
className={s.button}
|
||||||
onClick={addToCart}
|
onClick={addToCart}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={!variant}
|
disabled={!variant && product.options.length > 0}
|
||||||
>
|
>
|
||||||
Add to Cart
|
Add to Cart
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
||||||
<WishlistButton
|
<WishlistButton
|
||||||
className={s.wishlistButton}
|
className={s.wishlistButton}
|
||||||
productId={product.entityId}
|
productId={product.id}
|
||||||
variant={variant!}
|
variant={product.variants[0]! as any}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
.root {
|
.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
|
@apply h-12 w-12 bg-primary text-primary rounded-full mr-3 inline-flex
|
||||||
items-center justify-center cursor-pointer transition duration-150 ease-in-out
|
items-center justify-center cursor-pointer transition duration-150 ease-in-out
|
||||||
p-0 shadow-none border-gray-200 border box-border;
|
p-0 shadow-none border-gray-200 border box-border;
|
||||||
|
@ -13,7 +13,7 @@ interface Props {
|
|||||||
color?: string
|
color?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const Swatch: FC<Props & ButtonProps> = ({
|
const Swatch: FC<Omit<ButtonProps, 'variant'> & Props> = ({
|
||||||
className,
|
className,
|
||||||
color = '',
|
color = '',
|
||||||
label,
|
label,
|
||||||
|
@ -1,56 +1,22 @@
|
|||||||
import type { ProductNode } from '@framework/api/operations/get-product'
|
import type { Product } from '@commerce/types'
|
||||||
|
|
||||||
export type SelectedOptions = {
|
export type SelectedOptions = {
|
||||||
size: string | null
|
size: string | null
|
||||||
color: string | null
|
color: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProductOption = {
|
export function getVariant(product: Product, opts: SelectedOptions) {
|
||||||
displayName: string
|
const variant = product.variants.find((variant) => {
|
||||||
values: any
|
return Object.entries(opts).every(([key, value]) =>
|
||||||
}
|
variant.options.find((option) => {
|
||||||
|
|
||||||
// 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) => {
|
|
||||||
if (
|
if (
|
||||||
edge?.node.__typename === 'MultipleChoiceOption' &&
|
option.__typename === 'MultipleChoiceOption' &&
|
||||||
edge.node.displayName.toLowerCase() === key
|
option.displayName.toLowerCase() === key.toLowerCase()
|
||||||
) {
|
) {
|
||||||
return edge.node.values.edges?.find(
|
return option.values.find((v) => v.label.toLowerCase() === value)
|
||||||
(valueEdge) => valueEdge?.node.label === value
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
)
|
||||||
return numberOfDefinedOpts === numberOfEdges ?
|
|
||||||
Object.entries(opts).every(isEdgeEqualToOption)
|
|
||||||
: Object.entries(opts).some(isEdgeEqualToOption)
|
|
||||||
})
|
})
|
||||||
|
return variant
|
||||||
return variant ?? product.variants.edges?.[0]
|
|
||||||
}
|
}
|
||||||
|
@ -13,9 +13,9 @@ const Container: FC<Props> = ({ children, className, el = 'div', clean }) => {
|
|||||||
'mx-auto max-w-8xl px-6': !clean,
|
'mx-auto max-w-8xl px-6': !clean,
|
||||||
})
|
})
|
||||||
|
|
||||||
let Component: React.ComponentType<React.HTMLAttributes<
|
let Component: React.ComponentType<
|
||||||
HTMLDivElement
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>> = el as any
|
> = el as any
|
||||||
|
|
||||||
return <Component className={rootClassName}>{children}</Component>
|
return <Component className={rootClassName}>{children}</Component>
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply w-full;
|
@apply w-full relative;
|
||||||
|
height: 320px;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@apply flex flex-row items-center;
|
@apply flex flex-row items-center;
|
||||||
|
}
|
||||||
|
|
||||||
& > * {
|
.container > * {
|
||||||
@apply flex-1 px-16 py-4;
|
@apply relative flex-1 px-16 py-4 h-full;
|
||||||
width: 430px;
|
min-height: 320px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary {
|
.primary {
|
||||||
|
@ -8,6 +8,7 @@ export interface State {
|
|||||||
displayToast: boolean
|
displayToast: boolean
|
||||||
modalView: string
|
modalView: string
|
||||||
toastText: string
|
toastText: string
|
||||||
|
userAvatar: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
@ -17,6 +18,7 @@ const initialState = {
|
|||||||
modalView: 'LOGIN_VIEW',
|
modalView: 'LOGIN_VIEW',
|
||||||
displayToast: false,
|
displayToast: false,
|
||||||
toastText: '',
|
toastText: '',
|
||||||
|
userAvatar: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
@ -55,9 +57,14 @@ type Action =
|
|||||||
| {
|
| {
|
||||||
type: 'SET_USER_AVATAR'
|
type: 'SET_USER_AVATAR'
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type MODAL_VIEWS = 'SIGNUP_VIEW' | 'LOGIN_VIEW' | 'FORGOT_VIEW'
|
type MODAL_VIEWS =
|
||||||
|
| 'SIGNUP_VIEW'
|
||||||
|
| 'LOGIN_VIEW'
|
||||||
|
| 'FORGOT_VIEW'
|
||||||
|
| 'NEW_SHIPPING_ADDRESS'
|
||||||
|
| 'NEW_PAYMENT_METHOD'
|
||||||
type ToastText = string
|
type ToastText = string
|
||||||
|
|
||||||
export const UIContext = React.createContext<State | any>(initialState)
|
export const UIContext = React.createContext<State | any>(initialState)
|
||||||
@ -157,7 +164,8 @@ export const UIProvider: FC = (props) => {
|
|||||||
const openToast = () => dispatch({ type: 'OPEN_TOAST' })
|
const openToast = () => dispatch({ type: 'OPEN_TOAST' })
|
||||||
const closeToast = () => dispatch({ type: 'CLOSE_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) =>
|
const setModalView = (view: MODAL_VIEWS) =>
|
||||||
dispatch({ type: 'SET_MODAL_VIEW', view })
|
dispatch({ type: 'SET_MODAL_VIEW', view })
|
||||||
@ -176,7 +184,7 @@ export const UIProvider: FC = (props) => {
|
|||||||
setModalView,
|
setModalView,
|
||||||
openToast,
|
openToast,
|
||||||
closeToast,
|
closeToast,
|
||||||
setUserAvatar
|
setUserAvatar,
|
||||||
}),
|
}),
|
||||||
[state]
|
[state]
|
||||||
)
|
)
|
||||||
|
@ -10,3 +10,4 @@ export { default as Skeleton } from './Skeleton'
|
|||||||
export { default as Modal } from './Modal'
|
export { default as Modal } from './Modal'
|
||||||
export { default as Text } from './Text'
|
export { default as Text } from './Text'
|
||||||
export { default as Input } from './Input'
|
export { default as Input } from './Input'
|
||||||
|
export { useUI } from './context'
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import React, { FC, useState } from 'react'
|
import React, { FC, useState } from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import type { ProductNode } from '@framework/api/operations/get-all-products'
|
import { useUI } from '@components/ui'
|
||||||
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 { Heart } from '@components/icons'
|
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 = {
|
type Props = {
|
||||||
productId: number
|
productId: Product['id']
|
||||||
variant: NonNullable<ProductNode['variants']['edges']>[0]
|
variant: ProductVariant
|
||||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>
|
} & React.ButtonHTMLAttributes<HTMLButtonElement>
|
||||||
|
|
||||||
const WishlistButton: FC<Props> = ({
|
const WishlistButton: FC<Props> = ({
|
||||||
@ -19,16 +19,19 @@ const WishlistButton: FC<Props> = ({
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
|
const { data } = useWishlist()
|
||||||
const addItem = useAddItem()
|
const addItem = useAddItem()
|
||||||
const removeItem = useRemoveItem()
|
const removeItem = useRemoveItem()
|
||||||
const { data } = useWishlist()
|
|
||||||
const { data: customer } = useCustomer()
|
const { data: customer } = useCustomer()
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const { openModal, setModalView } = useUI()
|
const { openModal, setModalView } = useUI()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
// @ts-ignore Wishlist is not always enabled
|
||||||
const itemInWishlist = data?.items?.find(
|
const itemInWishlist = data?.items?.find(
|
||||||
|
// @ts-ignore Wishlist is not always enabled
|
||||||
(item) =>
|
(item) =>
|
||||||
item.product_id === productId &&
|
item.product_id === Number(productId) &&
|
||||||
item.variant_id === variant?.node.entityId
|
(item.variant_id as any) === Number(variant.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleWishlistChange = async (e: any) => {
|
const handleWishlistChange = async (e: any) => {
|
||||||
@ -50,7 +53,7 @@ const WishlistButton: FC<Props> = ({
|
|||||||
} else {
|
} else {
|
||||||
await addItem({
|
await addItem({
|
||||||
productId,
|
productId,
|
||||||
variantId: variant?.node.entityId!,
|
variantId: variant?.id!,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,27 +2,28 @@ import { FC, useState } from 'react'
|
|||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Image from 'next/image'
|
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 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 {
|
interface Props {
|
||||||
item: WishlistItem
|
product: Product
|
||||||
}
|
}
|
||||||
|
|
||||||
const WishlistCard: FC<Props> = ({ item }) => {
|
const WishlistCard: FC<Props> = ({ product }) => {
|
||||||
const product = item.product!
|
|
||||||
const { price } = usePrice({
|
const { price } = usePrice({
|
||||||
amount: product.prices?.price?.value,
|
amount: product.prices?.price?.value,
|
||||||
baseAmount: product.prices?.retailPrice?.value,
|
baseAmount: product.prices?.retailPrice?.value,
|
||||||
currencyCode: product.prices?.price?.currencyCode!,
|
currencyCode: product.prices?.price?.currencyCode!,
|
||||||
})
|
})
|
||||||
const removeItem = useRemoveItem({ includeProducts: true })
|
// @ts-ignore Wishlist is not always enabled
|
||||||
|
const removeItem = useRemoveItem({ wishlist: { includeProducts: true } })
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [removing, setRemoving] = useState(false)
|
const [removing, setRemoving] = useState(false)
|
||||||
const addItem = useAddItem()
|
const addItem = useAddItem()
|
||||||
@ -34,7 +35,7 @@ const WishlistCard: FC<Props> = ({ item }) => {
|
|||||||
try {
|
try {
|
||||||
// If this action succeeds then there's no need to do `setRemoving(true)`
|
// If this action succeeds then there's no need to do `setRemoving(true)`
|
||||||
// because the component will be removed from the view
|
// because the component will be removed from the view
|
||||||
await removeItem({ id: item.id! })
|
await removeItem({ id: product.id! })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setRemoving(false)
|
setRemoving(false)
|
||||||
}
|
}
|
||||||
@ -43,8 +44,8 @@ const WishlistCard: FC<Props> = ({ item }) => {
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
await addItem({
|
await addItem({
|
||||||
productId: product.entityId,
|
productId: String(product.id),
|
||||||
variantId: product.variants.edges?.[0]?.node.entityId!,
|
variantId: String(product.variants[0].id),
|
||||||
})
|
})
|
||||||
openSidebar()
|
openSidebar()
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@ -57,10 +58,10 @@ const WishlistCard: FC<Props> = ({ item }) => {
|
|||||||
<div className={cn(s.root, { 'opacity-75 pointer-events-none': removing })}>
|
<div className={cn(s.root, { 'opacity-75 pointer-events-none': removing })}>
|
||||||
<div className={`col-span-3 ${s.productBg}`}>
|
<div className={`col-span-3 ${s.productBg}`}>
|
||||||
<Image
|
<Image
|
||||||
src={product.images.edges?.[0]?.node.urlOriginal!}
|
src={product.images[0].url}
|
||||||
width={400}
|
width={400}
|
||||||
height={400}
|
height={400}
|
||||||
alt={product.images.edges?.[0]?.node.altText || 'Product Image'}
|
alt={product.images[0].alt || 'Product Image'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export { default as WishlistCard } from './WishlistCard'
|
export { default as WishlistCard } from './WishlistCard'
|
||||||
|
export { default as WishlistButton } from './WishlistButton'
|
||||||
|
@ -1,12 +1,22 @@
|
|||||||
{
|
{
|
||||||
"title": "ACME Storefront | Powered by Next.js Commerce",
|
"title": "ACME Storefront | Powered by Next.js Commerce",
|
||||||
"titleTemplate": "%s - ACME Storefront",
|
"titleTemplate": "%s - ACME Storefront",
|
||||||
"description": "Next.js Commerce -> https://www.nextjs.org/commerce",
|
"description": "Next.js Commerce - https://www.nextjs.org/commerce",
|
||||||
"openGraph": {
|
"openGraph": {
|
||||||
|
"title": "ACME Storefront | Powered by Next.js Commerce",
|
||||||
|
"description": "Next.js Commerce - https://www.nextjs.org/commerce",
|
||||||
"type": "website",
|
"type": "website",
|
||||||
"locale": "en_IE",
|
"locale": "en_IE",
|
||||||
"url": "https://nextjs.org/commerce",
|
"url": "https://nextjs.org/commerce",
|
||||||
"site_name": "Next.js Commerce"
|
"site_name": "Next.js Commerce",
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"url": "/card.png",
|
||||||
|
"width": 800,
|
||||||
|
"height": 600,
|
||||||
|
"alt": "Next.js Commerce"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"twitter": {
|
"twitter": {
|
||||||
"handle": "@nextjs",
|
"handle": "@nextjs",
|
||||||
|
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)
|
||||||
* [BigCommerce Storefront Data Hooks](#bigcommerce-storefront-data-hooks)
|
- [CommerceProvider](#commerceprovider)
|
||||||
* [Installation](#installation)
|
- [useLogin hook](#uselogin-hook)
|
||||||
* [General Usage](#general-usage)
|
- [useLogout](#uselogout)
|
||||||
* [CommerceProvider](#commerceprovider)
|
- [useCustomer](#usecustomer)
|
||||||
* [useLogin hook](#uselogin-hook)
|
- [useSignup](#usesignup)
|
||||||
* [useLogout](#uselogout)
|
- [usePrice](#useprice)
|
||||||
* [useCustomer](#usecustomer)
|
- [Cart Hooks](#cart-hooks)
|
||||||
* [useSignup](#usesignup)
|
- [useCart](#usecart)
|
||||||
* [usePrice](#useprice)
|
- [useAddItem](#useadditem)
|
||||||
* [Cart Hooks](#cart-hooks)
|
- [useUpdateItem](#useupdateitem)
|
||||||
* [useCart](#usecart)
|
- [useRemoveItem](#useremoveitem)
|
||||||
* [useAddItem](#useadditem)
|
- [Wishlist Hooks](#wishlist-hooks)
|
||||||
* [useUpdateItem](#useupdateitem)
|
- [Product Hooks and API](#product-hooks-and-api)
|
||||||
* [useRemoveItem](#useremoveitem)
|
- [useSearch](#usesearch)
|
||||||
* [Wishlist Hooks](#wishlist-hooks)
|
- [getAllProducts](#getallproducts)
|
||||||
* [Product Hooks and API](#product-hooks-and-api)
|
- [getProduct](#getproduct)
|
||||||
* [useSearch](#usesearch)
|
- [More](#more)
|
||||||
* [getAllProducts](#getallproducts)
|
|
||||||
* [getProduct](#getproduct)
|
|
||||||
* [More](#more)
|
|
||||||
|
|
||||||
# BigCommerce Storefront Data Hooks
|
# BigCommerce Storefront Data Hooks
|
||||||
|
|
||||||
@ -49,6 +47,7 @@ BIGCOMMERCE_STOREFRONT_API_TOKEN=<>
|
|||||||
BIGCOMMERCE_STORE_API_URL=<>
|
BIGCOMMERCE_STORE_API_URL=<>
|
||||||
BIGCOMMERCE_STORE_API_TOKEN=<>
|
BIGCOMMERCE_STORE_API_TOKEN=<>
|
||||||
BIGCOMMERCE_STORE_API_CLIENT_ID=<>
|
BIGCOMMERCE_STORE_API_CLIENT_ID=<>
|
||||||
|
BIGCOMMERCE_CHANNEL_ID=<>
|
||||||
```
|
```
|
||||||
|
|
||||||
## General Usage
|
## General Usage
|
||||||
@ -193,13 +192,11 @@ Returns the current cart data for use
|
|||||||
...
|
...
|
||||||
import useCart from '@bigcommerce/storefront-data-hooks/cart/use-cart'
|
import useCart from '@bigcommerce/storefront-data-hooks/cart/use-cart'
|
||||||
|
|
||||||
const countItem = (count: number, item: any) => count + item.quantity
|
const countItem = (count: number, item: LineItem) => count + item.quantity
|
||||||
const countItems = (count: number, items: any[]) =>
|
|
||||||
items.reduce(countItem, count)
|
|
||||||
|
|
||||||
const CartNumber = () => {
|
const CartNumber = () => {
|
||||||
const { data } = useCart()
|
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
|
return itemsCount > 0 ? <span>{itemsCount}</span> : null
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import { parseCartItem } from '../../utils/parse-item'
|
|||||||
import getCartCookie from '../../utils/get-cart-cookie'
|
import getCartCookie from '../../utils/get-cart-cookie'
|
||||||
import type { CartHandlers } from '..'
|
import type { CartHandlers } from '..'
|
||||||
|
|
||||||
// Return current cart info
|
|
||||||
const addItem: CartHandlers['addItem'] = async ({
|
const addItem: CartHandlers['addItem'] = async ({
|
||||||
res,
|
res,
|
||||||
body: { cartId, item },
|
body: { cartId, item },
|
||||||
@ -26,8 +25,14 @@ const addItem: CartHandlers['addItem'] = async ({
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
const { data } = cartId
|
const { data } = cartId
|
||||||
? await config.storeApiFetch(`/v3/carts/${cartId}/items?include=line_items.physical_items.options`, options)
|
? await config.storeApiFetch(
|
||||||
: await config.storeApiFetch('/v3/carts?include=line_items.physical_items.options', options)
|
`/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
|
// Create or update the cart cookie
|
||||||
res.setHeader(
|
res.setHeader(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import type { BigcommerceCart } from '../../../types'
|
||||||
import { BigcommerceApiError } from '../../utils/errors'
|
import { BigcommerceApiError } from '../../utils/errors'
|
||||||
import getCartCookie from '../../utils/get-cart-cookie'
|
import getCartCookie from '../../utils/get-cart-cookie'
|
||||||
import type { Cart, CartHandlers } from '..'
|
import type { CartHandlers } from '../'
|
||||||
|
|
||||||
// Return current cart info
|
// Return current cart info
|
||||||
const getCart: CartHandlers['getCart'] = async ({
|
const getCart: CartHandlers['getCart'] = async ({
|
||||||
@ -8,11 +9,13 @@ const getCart: CartHandlers['getCart'] = async ({
|
|||||||
body: { cartId },
|
body: { cartId },
|
||||||
config,
|
config,
|
||||||
}) => {
|
}) => {
|
||||||
let result: { data?: Cart } = {}
|
let result: { data?: BigcommerceCart } = {}
|
||||||
|
|
||||||
if (cartId) {
|
if (cartId) {
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
if (error instanceof BigcommerceApiError && error.status === 404) {
|
if (error instanceof BigcommerceApiError && error.status === 404) {
|
||||||
// Remove the cookie if it exists but the cart wasn't found
|
// Remove the cookie if it exists but the cart wasn't found
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import getCartCookie from '../../utils/get-cart-cookie'
|
import getCartCookie from '../../utils/get-cart-cookie'
|
||||||
import type { CartHandlers } from '..'
|
import type { CartHandlers } from '..'
|
||||||
|
|
||||||
// Return current cart info
|
|
||||||
const removeItem: CartHandlers['removeItem'] = async ({
|
const removeItem: CartHandlers['removeItem'] = async ({
|
||||||
res,
|
res,
|
||||||
body: { cartId, itemId },
|
body: { cartId, itemId },
|
||||||
|
@ -2,7 +2,6 @@ import { parseCartItem } from '../../utils/parse-item'
|
|||||||
import getCartCookie from '../../utils/get-cart-cookie'
|
import getCartCookie from '../../utils/get-cart-cookie'
|
||||||
import type { CartHandlers } from '..'
|
import type { CartHandlers } from '..'
|
||||||
|
|
||||||
// Return current cart info
|
|
||||||
const updateItem: CartHandlers['updateItem'] = async ({
|
const updateItem: CartHandlers['updateItem'] = async ({
|
||||||
res,
|
res,
|
||||||
body: { cartId, itemId, item },
|
body: { cartId, itemId, item },
|
||||||
|
@ -8,63 +8,25 @@ import getCart from './handlers/get-cart'
|
|||||||
import addItem from './handlers/add-item'
|
import addItem from './handlers/add-item'
|
||||||
import updateItem from './handlers/update-item'
|
import updateItem from './handlers/update-item'
|
||||||
import removeItem from './handlers/remove-item'
|
import removeItem from './handlers/remove-item'
|
||||||
|
import type {
|
||||||
type OptionSelections = {
|
BigcommerceCart,
|
||||||
option_id: Number
|
GetCartHandlerBody,
|
||||||
option_value: Number|String
|
AddCartItemHandlerBody,
|
||||||
}
|
UpdateCartItemHandlerBody,
|
||||||
|
RemoveCartItemHandlerBody,
|
||||||
export type ItemBody = {
|
} from '../../types'
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CartHandlers = {
|
export type CartHandlers = {
|
||||||
getCart: BigcommerceHandler<Cart, { cartId?: string }>
|
getCart: BigcommerceHandler<BigcommerceCart, GetCartHandlerBody>
|
||||||
addItem: BigcommerceHandler<Cart, { cartId?: string } & Partial<AddItemBody>>
|
addItem: BigcommerceHandler<BigcommerceCart, AddCartItemHandlerBody>
|
||||||
updateItem: BigcommerceHandler<
|
updateItem: BigcommerceHandler<BigcommerceCart, UpdateCartItemHandlerBody>
|
||||||
Cart,
|
removeItem: BigcommerceHandler<BigcommerceCart, RemoveCartItemHandlerBody>
|
||||||
{ cartId?: string } & Partial<UpdateItemBody>
|
|
||||||
>
|
|
||||||
removeItem: BigcommerceHandler<
|
|
||||||
Cart,
|
|
||||||
{ cartId?: string } & Partial<RemoveItemBody>
|
|
||||||
>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const METHODS = ['GET', 'POST', 'PUT', 'DELETE']
|
const METHODS = ['GET', 'POST', 'PUT', 'DELETE']
|
||||||
|
|
||||||
// TODO: a complete implementation should have schema validation for `req.body`
|
// TODO: a complete implementation should have schema validation for `req.body`
|
||||||
const cartApi: BigcommerceApiHandler<Cart, CartHandlers> = async (
|
const cartApi: BigcommerceApiHandler<BigcommerceCart, CartHandlers> = async (
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
config,
|
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'
|
import type { ProductsHandlers } from '../products'
|
||||||
|
|
||||||
const SORT: { [key: string]: string | undefined } = {
|
const SORT: { [key: string]: string | undefined } = {
|
||||||
@ -6,6 +7,7 @@ const SORT: { [key: string]: string | undefined } = {
|
|||||||
trending: 'total_sold',
|
trending: 'total_sold',
|
||||||
price: 'price',
|
price: 'price',
|
||||||
}
|
}
|
||||||
|
|
||||||
const LIMIT = 12
|
const LIMIT = 12
|
||||||
|
|
||||||
// Return current cart info
|
// Return current cart info
|
||||||
@ -44,21 +46,25 @@ const getProducts: ProductsHandlers['getProducts'] = async ({
|
|||||||
const { data } = await config.storeApiFetch<{ data: { id: number }[] }>(
|
const { data } = await config.storeApiFetch<{ data: { id: number }[] }>(
|
||||||
url.pathname + url.search
|
url.pathname + url.search
|
||||||
)
|
)
|
||||||
|
|
||||||
const entityIds = data.map((p) => p.id)
|
const entityIds = data.map((p) => p.id)
|
||||||
const found = entityIds.length > 0
|
const found = entityIds.length > 0
|
||||||
|
|
||||||
// We want the GraphQL version of each product
|
// We want the GraphQL version of each product
|
||||||
const graphqlData = await getAllProducts({
|
const graphqlData = await getAllProducts({
|
||||||
variables: { first: LIMIT, entityIds },
|
variables: { first: LIMIT, entityIds },
|
||||||
config,
|
config,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Put the products in an object that we can use to get them by id
|
// Put the products in an object that we can use to get them by id
|
||||||
const productsById = graphqlData.products.reduce<{
|
const productsById = graphqlData.products.reduce<{
|
||||||
[k: number]: ProductEdge
|
[k: number]: Product
|
||||||
}>((prods, p) => {
|
}>((prods, p) => {
|
||||||
prods[p.node.entityId] = p
|
prods[Number(p.id)] = p
|
||||||
return prods
|
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
|
// Populate the products array with the graphql products, in the order
|
||||||
// assigned by the list of entity ids
|
// 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 isAllowedMethod from '../utils/is-allowed-method'
|
||||||
import createApiHandler, {
|
import createApiHandler, {
|
||||||
BigcommerceApiHandler,
|
BigcommerceApiHandler,
|
||||||
BigcommerceHandler,
|
BigcommerceHandler,
|
||||||
} from '../utils/create-api-handler'
|
} from '../utils/create-api-handler'
|
||||||
import { BigcommerceApiError } from '../utils/errors'
|
import { BigcommerceApiError } from '../utils/errors'
|
||||||
import type { ProductEdge } from '../operations/get-all-products'
|
|
||||||
import getProducts from './handlers/get-products'
|
import getProducts from './handlers/get-products'
|
||||||
|
|
||||||
export type SearchProductsData = {
|
export type SearchProductsData = {
|
||||||
products: ProductEdge[]
|
products: Product[]
|
||||||
found: boolean
|
found: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProductsHandlers = {
|
export type ProductsHandlers = {
|
||||||
getProducts: BigcommerceHandler<
|
getProducts: BigcommerceHandler<
|
||||||
SearchProductsData,
|
SearchProductsData,
|
||||||
{ search?: 'string'; category?: string; brand?: string; sort?: string }
|
{ search?: string; category?: string; brand?: string; sort?: string }
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
|
||||||
const METHODS = ['GET']
|
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<
|
const productsApi: BigcommerceApiHandler<
|
||||||
SearchProductsData,
|
SearchProductsData,
|
||||||
ProductsHandlers
|
ProductsHandlers
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { FetcherError } from '@commerce/utils/errors'
|
import { FetcherError } from '@commerce/utils/errors'
|
||||||
import login from '../../operations/login'
|
import login from '../../../auth/login'
|
||||||
import type { LoginHandlers } from '../login'
|
import type { LoginHandlers } from '../login'
|
||||||
|
|
||||||
const invalidCredentials = /invalid credentials/i
|
const invalidCredentials = /invalid credentials/i
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { BigcommerceApiError } from '../../utils/errors'
|
import { BigcommerceApiError } from '../../utils/errors'
|
||||||
import login from '../../operations/login'
|
import login from '../../../auth/login'
|
||||||
import { SignupHandlers } from '../signup'
|
import { SignupHandlers } from '../signup'
|
||||||
|
|
||||||
const signup: SignupHandlers['signup'] = async ({
|
const signup: SignupHandlers['signup'] = async ({
|
||||||
|
@ -1,14 +1,28 @@
|
|||||||
import type { ItemBody as WishlistItemBody } from '../wishlist'
|
import type { ItemBody as WishlistItemBody } from '../wishlist'
|
||||||
import type { ItemBody } from '../cart'
|
import type { CartItemBody, OptionSelections } from '../../types'
|
||||||
|
|
||||||
export const parseWishlistItem = (item: WishlistItemBody) => ({
|
type BCWishlistItemBody = {
|
||||||
product_id: item.productId,
|
product_id: number
|
||||||
variant_id: item.variantId,
|
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,
|
quantity: item.quantity,
|
||||||
product_id: item.productId,
|
product_id: Number(item.productId),
|
||||||
variant_id: item.variantId,
|
variant_id: Number(item.variantId),
|
||||||
option_selections: item.optionSelections
|
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'
|
import type { RecursivePartial } from './types'
|
||||||
|
|
||||||
export default function setProductLocaleMeta(
|
export default function setProductLocaleMeta(
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { WishlistHandlers } from '..'
|
import type { WishlistHandlers } from '..'
|
||||||
import getCustomerId from '../../operations/get-customer-id'
|
import getCustomerId from '../../../customer/get-customer-id'
|
||||||
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
import getCustomerWishlist from '../../../customer/get-customer-wishlist'
|
||||||
import { parseWishlistItem } from '../../utils/parse-item'
|
import { parseWishlistItem } from '../../utils/parse-item'
|
||||||
|
|
||||||
// Returns the wishlist of the signed customer
|
// Returns the wishlist of the signed customer
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import getCustomerId from '../../operations/get-customer-id'
|
import getCustomerId from '../../../customer/get-customer-id'
|
||||||
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
import getCustomerWishlist from '../../../customer/get-customer-wishlist'
|
||||||
import type { Wishlist, WishlistHandlers } from '..'
|
import type { Wishlist, WishlistHandlers } from '..'
|
||||||
|
|
||||||
// Return wishlist info
|
// Return wishlist info
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import getCustomerId from '../../operations/get-customer-id'
|
import getCustomerId from '../../../customer/get-customer-id'
|
||||||
import getCustomerWishlist, {
|
import getCustomerWishlist, {
|
||||||
Wishlist,
|
Wishlist,
|
||||||
} from '../../operations/get-customer-wishlist'
|
} from '../../../customer/get-customer-wishlist'
|
||||||
import type { WishlistHandlers } from '..'
|
import type { WishlistHandlers } from '..'
|
||||||
|
|
||||||
// Return current wishlist info
|
// Return current wishlist info
|
||||||
|
@ -7,24 +7,25 @@ import { BigcommerceApiError } from '../utils/errors'
|
|||||||
import type {
|
import type {
|
||||||
Wishlist,
|
Wishlist,
|
||||||
WishlistItem,
|
WishlistItem,
|
||||||
} from '../operations/get-customer-wishlist'
|
} from '../../customer/get-customer-wishlist'
|
||||||
import getWishlist from './handlers/get-wishlist'
|
import getWishlist from './handlers/get-wishlist'
|
||||||
import addItem from './handlers/add-item'
|
import addItem from './handlers/add-item'
|
||||||
import removeItem from './handlers/remove-item'
|
import removeItem from './handlers/remove-item'
|
||||||
|
import type { Product, ProductVariant, Customer } from '@commerce/types'
|
||||||
|
|
||||||
export type { Wishlist, WishlistItem }
|
export type { Wishlist, WishlistItem }
|
||||||
|
|
||||||
export type ItemBody = {
|
export type ItemBody = {
|
||||||
productId: number
|
productId: Product['id']
|
||||||
variantId: number
|
variantId: ProductVariant['id']
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AddItemBody = { item: ItemBody }
|
export type AddItemBody = { item: ItemBody }
|
||||||
|
|
||||||
export type RemoveItemBody = { itemId: string }
|
export type RemoveItemBody = { itemId: Product['id'] }
|
||||||
|
|
||||||
export type WishlistBody = {
|
export type WishlistBody = {
|
||||||
customer_id: number
|
customer_id: Customer['entityId']
|
||||||
is_public: number
|
is_public: number
|
||||||
name: string
|
name: string
|
||||||
items: any[]
|
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 { ServerResponse } from 'http'
|
||||||
import type { LoginMutation, LoginMutationVariables } from '../../schema'
|
import type { LoginMutation, LoginMutationVariables } from '../schema'
|
||||||
import type { RecursivePartial } from '../utils/types'
|
import type { RecursivePartial } from '../api/utils/types'
|
||||||
import concatHeader from '../utils/concat-cookie'
|
import concatHeader from '../api/utils/concat-cookie'
|
||||||
import { BigcommerceConfig, getConfig } from '..'
|
import { BigcommerceConfig, getConfig } from '../api'
|
||||||
|
|
||||||
export const loginMutation = /* GraphQL */ `
|
export const loginMutation = /* GraphQL */ `
|
||||||
mutation login($email: String!, $password: String!) {
|
mutation login($email: String!, $password: String!) {
|
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 { useCallback } from 'react'
|
||||||
import type { HookFetcher } from '@commerce/utils/types'
|
import type { MutationHook } from '@commerce/utils/types'
|
||||||
import { CommerceError } from '@commerce/utils/errors'
|
import { CommerceError } from '@commerce/utils/errors'
|
||||||
import useCartAddItem from '@commerce/cart/use-add-item'
|
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
|
||||||
import type { ItemBody, AddItemBody } from '../api/cart'
|
import { normalizeCart } from '../lib/normalize'
|
||||||
import useCart, { Cart } from './use-cart'
|
import type {
|
||||||
|
Cart,
|
||||||
|
BigcommerceCart,
|
||||||
|
CartItemBody,
|
||||||
|
AddCartItemBody,
|
||||||
|
} from '../types'
|
||||||
|
import useCart from './use-cart'
|
||||||
|
|
||||||
const defaultOpts = {
|
export default useAddItem as UseAddItem<typeof handler>
|
||||||
url: '/api/bigcommerce/cart',
|
|
||||||
method: 'POST',
|
|
||||||
}
|
|
||||||
|
|
||||||
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> = (
|
const data = await fetch<BigcommerceCart, AddCartItemBody>({
|
||||||
options,
|
...options,
|
||||||
{ item },
|
body: { 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',
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
return fetch({
|
return normalizeCart(data)
|
||||||
...defaultOpts,
|
},
|
||||||
...options,
|
useHook: ({ fetch }) => () => {
|
||||||
body: { item },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extendHook(customFetcher: typeof fetcher) {
|
|
||||||
const useAddItem = () => {
|
|
||||||
const { mutate } = useCart()
|
const { mutate } = useCart()
|
||||||
const fn = useCartAddItem(defaultOpts, customFetcher)
|
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async function addItem(input: AddItemInput) {
|
async function addItem(input) {
|
||||||
const data = await fn({ item: input })
|
const data = await fetch({ input })
|
||||||
await mutate(data, false)
|
await mutate(data, false)
|
||||||
return data
|
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 { useMemo } from 'react'
|
||||||
import type { SwrOptions } from '@commerce/utils/use-data'
|
import { SWRHook } from '@commerce/utils/types'
|
||||||
import useCommerceCart, { CartInput } from '@commerce/cart/use-cart'
|
import useCart, { UseCart, FetchCartInput } from '@commerce/cart/use-cart'
|
||||||
import type { Cart } from '../api/cart'
|
import { normalizeCart } from '../lib/normalize'
|
||||||
|
import type { Cart } from '../types'
|
||||||
|
|
||||||
const defaultOpts = {
|
export default useCart as UseCart<typeof handler>
|
||||||
url: '/api/bigcommerce/cart',
|
|
||||||
method: 'GET',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { Cart }
|
export const handler: SWRHook<
|
||||||
|
Cart | null,
|
||||||
export const fetcher: HookFetcher<Cart | null, CartInput> = (
|
{},
|
||||||
options,
|
FetchCartInput,
|
||||||
{ cartId },
|
{ isEmpty?: boolean }
|
||||||
fetch
|
> = {
|
||||||
) => {
|
fetchOptions: {
|
||||||
return cartId ? fetch({ ...defaultOpts, ...options }) : null
|
url: '/api/bigcommerce/cart',
|
||||||
}
|
method: 'GET',
|
||||||
|
},
|
||||||
export function extendHook(
|
async fetcher({ input: { cartId }, options, fetch }) {
|
||||||
customFetcher: typeof fetcher,
|
const data = cartId ? await fetch(options) : null
|
||||||
swrOptions?: SwrOptions<Cart | null, CartInput>
|
return data && normalizeCart(data)
|
||||||
) {
|
},
|
||||||
const useCart = () => {
|
useHook: ({ useData }) => (input) => {
|
||||||
const response = useCommerceCart(defaultOpts, [], customFetcher, {
|
const response = useData({
|
||||||
revalidateOnFocus: false,
|
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
|
||||||
...swrOptions,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Uses a getter to only calculate the prop when required
|
return useMemo(
|
||||||
// response.data is also a getter and it's better to not trigger it early
|
() =>
|
||||||
Object.defineProperty(response, 'isEmpty', {
|
Object.create(response, {
|
||||||
get() {
|
isEmpty: {
|
||||||
return Object.values(response.data?.line_items ?? {}).every(
|
get() {
|
||||||
(items) => !items.length
|
return (response.data?.lineItems.length ?? 0) <= 0
|
||||||
)
|
},
|
||||||
},
|
enumerable: true,
|
||||||
set: (x) => x,
|
},
|
||||||
})
|
}),
|
||||||
|
[response]
|
||||||
return response
|
)
|
||||||
}
|
},
|
||||||
|
|
||||||
useCart.extend = extendHook
|
|
||||||
|
|
||||||
return useCart
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default extendHook(fetcher)
|
|
||||||
|
@ -1,51 +1,71 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { HookFetcher } from '@commerce/utils/types'
|
import type {
|
||||||
import useCartRemoveItem from '@commerce/cart/use-remove-item'
|
MutationHookContext,
|
||||||
import type { RemoveItemBody } from '../api/cart'
|
HookFetcherContext,
|
||||||
import useCart, { Cart } from './use-cart'
|
} 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 = {
|
export type RemoveItemFn<T = any> = T extends LineItem
|
||||||
url: '/api/bigcommerce/cart',
|
? (input?: RemoveItemInput<T>) => Promise<Cart | null>
|
||||||
method: 'DELETE',
|
: (input: RemoveItemInput<T>) => Promise<Cart | null>
|
||||||
}
|
|
||||||
|
|
||||||
export type RemoveItemInput = {
|
export type RemoveItemInput<T = any> = T extends LineItem
|
||||||
id: string
|
? Partial<RemoveItemInputBase>
|
||||||
}
|
: RemoveItemInputBase
|
||||||
|
|
||||||
export const fetcher: HookFetcher<Cart | null, RemoveItemBody> = (
|
export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||||
options,
|
|
||||||
{ itemId },
|
|
||||||
fetch
|
|
||||||
) => {
|
|
||||||
return fetch({
|
|
||||||
...defaultOpts,
|
|
||||||
...options,
|
|
||||||
body: { itemId },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extendHook(customFetcher: typeof fetcher) {
|
export const handler = {
|
||||||
const useRemoveItem = (item?: any) => {
|
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 { mutate } = useCart()
|
||||||
const fn = useCartRemoveItem<Cart | null, RemoveItemBody>(
|
const removeItem: RemoveItemFn<LineItem> = async (input) => {
|
||||||
defaultOpts,
|
const itemId = input?.id ?? item?.id
|
||||||
customFetcher
|
|
||||||
)
|
|
||||||
|
|
||||||
return useCallback(
|
if (!itemId) {
|
||||||
async function removeItem(input: RemoveItemInput) {
|
throw new ValidationError({
|
||||||
const data = await fn({ itemId: input.id ?? item?.id })
|
message: 'Invalid input used for this operation',
|
||||||
await mutate(data, false)
|
})
|
||||||
return data
|
}
|
||||||
},
|
|
||||||
[fn, mutate]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 { useCallback } from 'react'
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
import type { HookFetcher } from '@commerce/utils/types'
|
import type {
|
||||||
import { CommerceError } from '@commerce/utils/errors'
|
MutationHookContext,
|
||||||
import useCartUpdateItem from '@commerce/cart/use-update-item'
|
HookFetcherContext,
|
||||||
import type { ItemBody, UpdateItemBody } from '../api/cart'
|
} from '@commerce/utils/types'
|
||||||
import { fetcher as removeFetcher } from './use-remove-item'
|
import { ValidationError } from '@commerce/utils/errors'
|
||||||
import useCart, { Cart } from './use-cart'
|
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 = {
|
export type UpdateItemInput<T = any> = T extends LineItem
|
||||||
url: '/api/bigcommerce/cart',
|
? Partial<UpdateItemInputBase<LineItem>>
|
||||||
method: 'PUT',
|
: UpdateItemInputBase<LineItem>
|
||||||
}
|
|
||||||
|
|
||||||
export type UpdateItemInput = Partial<{ id: string } & ItemBody>
|
export default useUpdateItem as UseUpdateItem<typeof handler>
|
||||||
|
|
||||||
export const fetcher: HookFetcher<Cart | null, UpdateItemBody> = (
|
export const handler = {
|
||||||
options,
|
fetchOptions: {
|
||||||
{ itemId, item },
|
url: '/api/bigcommerce/cart',
|
||||||
fetch
|
method: 'PUT',
|
||||||
) => {
|
},
|
||||||
if (Number.isInteger(item.quantity)) {
|
async fetcher({
|
||||||
// Also allow the update hook to remove an item if the quantity is lower than 1
|
input: { itemId, item },
|
||||||
if (item.quantity! < 1) {
|
options,
|
||||||
return removeFetcher(null, { itemId }, fetch)
|
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({
|
const data = await fetch<BigcommerceCart, UpdateCartItemBody>({
|
||||||
message: 'The item quantity has to be a valid integer',
|
...options,
|
||||||
|
body: { itemId, item },
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
return fetch({
|
return normalizeCart(data)
|
||||||
...defaultOpts,
|
},
|
||||||
...options,
|
useHook: ({
|
||||||
body: { itemId, item },
|
fetch,
|
||||||
})
|
}: MutationHookContext<Cart | null, UpdateCartItemBody>) => <
|
||||||
}
|
T extends LineItem | undefined = undefined
|
||||||
|
>(
|
||||||
function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) {
|
ctx: {
|
||||||
const useUpdateItem = (item?: any) => {
|
item?: T
|
||||||
const { mutate } = useCart()
|
wait?: number
|
||||||
const fn = useCartUpdateItem<Cart | null, UpdateItemBody>(
|
} = {}
|
||||||
defaultOpts,
|
) => {
|
||||||
customFetcher
|
const { item } = ctx
|
||||||
)
|
const { mutate } = useCart() as any
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
debounce(async (input: UpdateItemInput) => {
|
debounce(async (input: UpdateItemInput<T>) => {
|
||||||
const data = await fn({
|
const itemId = input.id ?? item?.id
|
||||||
itemId: input.id ?? item?.id,
|
const productId = input.productId ?? item?.productId
|
||||||
item: {
|
const variantId = input.productId ?? item?.variantId
|
||||||
productId: input.productId ?? item?.product_id,
|
|
||||||
variantId: input.productId ?? item?.variant_id,
|
if (!itemId || !productId || !variantId) {
|
||||||
quantity: input.quantity,
|
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)
|
await mutate(data, false)
|
||||||
return data
|
return data
|
||||||
}, cfg?.wait ?? 500),
|
}, ctx.wait ?? 500),
|
||||||
[fn, mutate]
|
[fetch, mutate]
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
|
||||||
useUpdateItem.extend = extendHook
|
|
||||||
|
|
||||||
return useUpdateItem
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default extendHook(fetcher)
|
|
||||||
|
6
framework/bigcommerce/commerce.config.json
Normal file
6
framework/bigcommerce/commerce.config.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"provider": "bigcommerce",
|
||||||
|
"features": {
|
||||||
|
"wishlist": true
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
||||||
import { BigcommerceConfig, getConfig } from '..'
|
import { BigcommerceConfig, getConfig } from '../api'
|
||||||
import { definitions } from '../definitions/store-content'
|
import { definitions } from '../api/definitions/store-content'
|
||||||
|
|
||||||
export type Page = definitions['page_Full']
|
export type Page = definitions['page_Full']
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
||||||
import { BigcommerceConfig, getConfig } from '..'
|
import { BigcommerceConfig, getConfig } from '../api'
|
||||||
import { definitions } from '../definitions/store-content'
|
import { definitions } from '../api/definitions/store-content'
|
||||||
|
|
||||||
export type Page = definitions['page_Full']
|
export type Page = definitions['page_Full']
|
||||||
|
|
||||||
@ -38,9 +38,9 @@ async function getPage({
|
|||||||
config = getConfig(config)
|
config = getConfig(config)
|
||||||
// RecursivePartial forces the method to check for every prop in the data, which is
|
// RecursivePartial forces the method to check for every prop in the data, which is
|
||||||
// required in case there's a custom `url`
|
// required in case there's a custom `url`
|
||||||
const { data } = await config.storeApiFetch<RecursivePartial<{ data: Page[] }>>(
|
const { data } = await config.storeApiFetch<
|
||||||
url || `/v3/content/pages?id=${variables.id}&include=body`
|
RecursivePartial<{ data: Page[] }>
|
||||||
)
|
>(url || `/v3/content/pages?id=${variables.id}&include=body`)
|
||||||
const firstPage = data?.[0]
|
const firstPage = data?.[0]
|
||||||
const page = firstPage as RecursiveRequired<typeof firstPage>
|
const page = firstPage as RecursiveRequired<typeof firstPage>
|
||||||
|
|
@ -1,8 +1,8 @@
|
|||||||
import type { GetSiteInfoQuery, GetSiteInfoQueryVariables } from '../../schema'
|
import type { GetSiteInfoQuery, GetSiteInfoQueryVariables } from '../schema'
|
||||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
||||||
import filterEdges from '../utils/filter-edges'
|
import filterEdges from '../api/utils/filter-edges'
|
||||||
import { BigcommerceConfig, getConfig } from '..'
|
import { BigcommerceConfig, getConfig } from '../api'
|
||||||
import { categoryTreeItemFragment } from '../fragments/category-tree'
|
import { categoryTreeItemFragment } from '../api/fragments/category-tree'
|
||||||
|
|
||||||
// Get 3 levels of categories
|
// Get 3 levels of categories
|
||||||
export const getSiteInfoQuery = /* GraphQL */ `
|
export const getSiteInfoQuery = /* GraphQL */ `
|
@ -1,5 +1,5 @@
|
|||||||
import { GetCustomerIdQuery } from '../../schema'
|
import { GetCustomerIdQuery } from '../schema'
|
||||||
import { BigcommerceConfig, getConfig } from '..'
|
import { BigcommerceConfig, getConfig } from '../api'
|
||||||
|
|
||||||
export const getCustomerIdQuery = /* GraphQL */ `
|
export const getCustomerIdQuery = /* GraphQL */ `
|
||||||
query getCustomerId {
|
query getCustomerId {
|
@ -1,7 +1,7 @@
|
|||||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
||||||
import { definitions } from '../definitions/wishlist'
|
import { definitions } from '../api/definitions/wishlist'
|
||||||
import { BigcommerceConfig, getConfig } from '..'
|
import { BigcommerceConfig, getConfig } from '../api'
|
||||||
import getAllProducts, { ProductEdge } from './get-all-products'
|
import getAllProducts, { ProductEdge } from '../product/get-all-products'
|
||||||
|
|
||||||
export type Wishlist = Omit<definitions['wishlist_Full'], 'items'> & {
|
export type Wishlist = Omit<definitions['wishlist_Full'], 'items'> & {
|
||||||
items?: WishlistItem[]
|
items?: WishlistItem[]
|
||||||
@ -68,14 +68,15 @@ async function getCustomerWishlist({
|
|||||||
const productsById = graphqlData.products.reduce<{
|
const productsById = graphqlData.products.reduce<{
|
||||||
[k: number]: ProductEdge
|
[k: number]: ProductEdge
|
||||||
}>((prods, p) => {
|
}>((prods, p) => {
|
||||||
prods[p.node.entityId] = p
|
prods[Number(p.id)] = p as any
|
||||||
return prods
|
return prods
|
||||||
}, {})
|
}, {})
|
||||||
// Populate the wishlist items with the graphql products
|
// Populate the wishlist items with the graphql products
|
||||||
wishlist.items.forEach((item) => {
|
wishlist.items.forEach((item) => {
|
||||||
const product = item && productsById[item.product_id!]
|
const product = item && productsById[item.product_id!]
|
||||||
if (item && product) {
|
if (item && product) {
|
||||||
item.product = product.node
|
// @ts-ignore Fix this type when the wishlist type is properly defined
|
||||||
|
item.product = product
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
1
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 type { ReactNode } from 'react'
|
||||||
import * as React from 'react'
|
|
||||||
import {
|
import {
|
||||||
CommerceConfig,
|
CommerceConfig,
|
||||||
CommerceProvider as CoreCommerceProvider,
|
CommerceProvider as CoreCommerceProvider,
|
||||||
useCommerce as useCoreCommerce,
|
useCommerce as useCoreCommerce,
|
||||||
} from '@commerce'
|
} from '@commerce'
|
||||||
import { FetcherError } from '@commerce/utils/errors'
|
import { bigcommerceProvider, BigcommerceProvider } from './provider'
|
||||||
|
|
||||||
async function getText(res: Response) {
|
export { bigcommerceProvider }
|
||||||
try {
|
export type { BigcommerceProvider }
|
||||||
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 const bigcommerceConfig: CommerceConfig = {
|
export const bigcommerceConfig: CommerceConfig = {
|
||||||
locale: 'en-us',
|
locale: 'en-us',
|
||||||
cartCookie: 'bc_cartId',
|
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>
|
export type BigcommerceConfig = Partial<CommerceConfig>
|
||||||
@ -52,10 +23,13 @@ export type BigcommerceProps = {
|
|||||||
|
|
||||||
export function CommerceProvider({ children, ...config }: BigcommerceProps) {
|
export function CommerceProvider({ children, ...config }: BigcommerceProps) {
|
||||||
return (
|
return (
|
||||||
<CoreCommerceProvider config={{ ...bigcommerceConfig, ...config }}>
|
<CoreCommerceProvider
|
||||||
|
provider={bigcommerceProvider}
|
||||||
|
config={{ ...bigcommerceConfig, ...config }}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</CoreCommerceProvider>
|
</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 {
|
import type {
|
||||||
GetAllProductPathsQuery,
|
GetAllProductPathsQuery,
|
||||||
GetAllProductPathsQueryVariables,
|
GetAllProductPathsQueryVariables,
|
||||||
} from '../../schema'
|
} from '../schema'
|
||||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
||||||
import filterEdges from '../utils/filter-edges'
|
import filterEdges from '../api/utils/filter-edges'
|
||||||
import { BigcommerceConfig, getConfig } from '..'
|
import { BigcommerceConfig, getConfig } from '../api'
|
||||||
|
|
||||||
export const getAllProductPathsQuery = /* GraphQL */ `
|
export const getAllProductPathsQuery = /* GraphQL */ `
|
||||||
query getAllProductPaths($first: Int = 100) {
|
query getAllProductPaths($first: Int = 100) {
|
@ -1,12 +1,14 @@
|
|||||||
import type {
|
import type {
|
||||||
GetAllProductsQuery,
|
GetAllProductsQuery,
|
||||||
GetAllProductsQueryVariables,
|
GetAllProductsQueryVariables,
|
||||||
} from '../../schema'
|
} from '../schema'
|
||||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
import type { Product } from '@commerce/types'
|
||||||
import filterEdges from '../utils/filter-edges'
|
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
||||||
import setProductLocaleMeta from '../utils/set-product-locale-meta'
|
import filterEdges from '../api/utils/filter-edges'
|
||||||
import { productConnectionFragment } from '../fragments/product'
|
import setProductLocaleMeta from '../api/utils/set-product-locale-meta'
|
||||||
import { BigcommerceConfig, getConfig } from '..'
|
import { productConnectionFragment } from '../api/fragments/product'
|
||||||
|
import { BigcommerceConfig, getConfig } from '../api'
|
||||||
|
import { normalizeProduct } from '../lib/normalize'
|
||||||
|
|
||||||
export const getAllProductsQuery = /* GraphQL */ `
|
export const getAllProductsQuery = /* GraphQL */ `
|
||||||
query getAllProducts(
|
query getAllProducts(
|
||||||
@ -72,7 +74,7 @@ async function getAllProducts(opts?: {
|
|||||||
variables?: ProductVariables
|
variables?: ProductVariables
|
||||||
config?: BigcommerceConfig
|
config?: BigcommerceConfig
|
||||||
preview?: boolean
|
preview?: boolean
|
||||||
}): Promise<GetAllProductsResult>
|
}): Promise<{ products: Product[] }>
|
||||||
|
|
||||||
async function getAllProducts<
|
async function getAllProducts<
|
||||||
T extends Record<keyof GetAllProductsResult, any[]>,
|
T extends Record<keyof GetAllProductsResult, any[]>,
|
||||||
@ -93,7 +95,8 @@ async function getAllProducts({
|
|||||||
variables?: ProductVariables
|
variables?: ProductVariables
|
||||||
config?: BigcommerceConfig
|
config?: BigcommerceConfig
|
||||||
preview?: boolean
|
preview?: boolean
|
||||||
} = {}): Promise<GetAllProductsResult> {
|
// TODO: fix the product type here
|
||||||
|
} = {}): Promise<{ products: Product[] | any[] }> {
|
||||||
config = getConfig(config)
|
config = getConfig(config)
|
||||||
|
|
||||||
const locale = vars.locale || config.locale
|
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
|
export default getAllProducts
|
@ -1,7 +1,9 @@
|
|||||||
import type { GetProductQuery, GetProductQueryVariables } from '../../schema'
|
import type { GetProductQuery, GetProductQueryVariables } from '../schema'
|
||||||
import setProductLocaleMeta from '../utils/set-product-locale-meta'
|
import setProductLocaleMeta from '../api/utils/set-product-locale-meta'
|
||||||
import { productInfoFragment } from '../fragments/product'
|
import { productInfoFragment } from '../api/fragments/product'
|
||||||
import { BigcommerceConfig, getConfig } from '..'
|
import { BigcommerceConfig, getConfig } from '../api'
|
||||||
|
import { normalizeProduct } from '../lib/normalize'
|
||||||
|
import type { Product } from '@commerce/types'
|
||||||
|
|
||||||
export const getProductQuery = /* GraphQL */ `
|
export const getProductQuery = /* GraphQL */ `
|
||||||
query getProduct(
|
query getProduct(
|
||||||
@ -92,7 +94,7 @@ async function getProduct({
|
|||||||
variables: ProductVariables
|
variables: ProductVariables
|
||||||
config?: BigcommerceConfig
|
config?: BigcommerceConfig
|
||||||
preview?: boolean
|
preview?: boolean
|
||||||
}): Promise<GetProductResult> {
|
}): Promise<Product | {} | any> {
|
||||||
config = getConfig(config)
|
config = getConfig(config)
|
||||||
|
|
||||||
const locale = vars.locale || config.locale
|
const locale = vars.locale || config.locale
|
||||||
@ -109,7 +111,8 @@ async function getProduct({
|
|||||||
if (locale && config.applyLocale) {
|
if (locale && config.applyLocale) {
|
||||||
setProductLocaleMeta(product)
|
setProductLocaleMeta(product)
|
||||||
}
|
}
|
||||||
return { product }
|
|
||||||
|
return { product: normalizeProduct(product as any) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return {}
|
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 { useCallback } from 'react'
|
||||||
import { HookFetcher } from '@commerce/utils/types'
|
import type { MutationHook } from '@commerce/utils/types'
|
||||||
import { CommerceError } from '@commerce/utils/errors'
|
import { CommerceError } from '@commerce/utils/errors'
|
||||||
import useWishlistAddItem from '@commerce/wishlist/use-add-item'
|
import useAddItem, { UseAddItem } from '@commerce/wishlist/use-add-item'
|
||||||
import type { ItemBody, AddItemBody } from '../api/wishlist'
|
import type { ItemBody, AddItemBody } from '../api/wishlist'
|
||||||
import useCustomer from '../use-customer'
|
import useCustomer from '../customer/use-customer'
|
||||||
import useWishlist, { UseWishlistOptions, Wishlist } from './use-wishlist'
|
import useWishlist from './use-wishlist'
|
||||||
|
|
||||||
const defaultOpts = {
|
export default useAddItem as UseAddItem<typeof handler>
|
||||||
url: '/api/bigcommerce/wishlist',
|
|
||||||
method: 'POST',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AddItemInput = ItemBody
|
export const handler: MutationHook<any, {}, ItemBody, AddItemBody> = {
|
||||||
|
fetchOptions: {
|
||||||
export const fetcher: HookFetcher<Wishlist, AddItemBody> = (
|
url: '/api/bigcommerce/wishlist',
|
||||||
options,
|
method: 'POST',
|
||||||
{ item },
|
},
|
||||||
fetch
|
useHook: ({ fetch }) => () => {
|
||||||
) => {
|
|
||||||
// TODO: add validations before doing the fetch
|
|
||||||
return fetch({
|
|
||||||
...defaultOpts,
|
|
||||||
...options,
|
|
||||||
body: { item },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extendHook(customFetcher: typeof fetcher) {
|
|
||||||
const useAddItem = (opts?: UseWishlistOptions) => {
|
|
||||||
const { data: customer } = useCustomer()
|
const { data: customer } = useCustomer()
|
||||||
const { revalidate } = useWishlist(opts)
|
const { revalidate } = useWishlist()
|
||||||
const fn = useWishlistAddItem(defaultOpts, customFetcher)
|
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async function addItem(input: AddItemInput) {
|
async function addItem(item) {
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
// A signed customer is required in order to have a wishlist
|
// A signed customer is required in order to have a wishlist
|
||||||
throw new CommerceError({
|
throw new CommerceError({
|
||||||
@ -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()
|
await revalidate()
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
[fn, revalidate, customer]
|
[fetch, revalidate, customer]
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
|
||||||
useAddItem.extend = extendHook
|
|
||||||
|
|
||||||
return useAddItem
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default extendHook(fetcher)
|
|
||||||
|
@ -1,43 +1,32 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { HookFetcher } from '@commerce/utils/types'
|
import type { MutationHook } from '@commerce/utils/types'
|
||||||
import { CommerceError } from '@commerce/utils/errors'
|
import { CommerceError } from '@commerce/utils/errors'
|
||||||
import useWishlistRemoveItem from '@commerce/wishlist/use-remove-item'
|
import useRemoveItem, {
|
||||||
import type { RemoveItemBody } from '../api/wishlist'
|
RemoveItemInput,
|
||||||
import useCustomer from '../use-customer'
|
UseRemoveItem,
|
||||||
import useWishlist, { UseWishlistOptions, Wishlist } from './use-wishlist'
|
} 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 = {
|
export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||||
url: '/api/bigcommerce/wishlist',
|
|
||||||
method: 'DELETE',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RemoveItemInput = {
|
export const handler: MutationHook<
|
||||||
id: string | number
|
Wishlist | null,
|
||||||
}
|
{ wishlist?: UseWishlistInput },
|
||||||
|
RemoveItemInput,
|
||||||
export const fetcher: HookFetcher<Wishlist | null, RemoveItemBody> = (
|
RemoveItemBody
|
||||||
options,
|
> = {
|
||||||
{ itemId },
|
fetchOptions: {
|
||||||
fetch
|
url: '/api/bigcommerce/wishlist',
|
||||||
) => {
|
method: 'DELETE',
|
||||||
return fetch({
|
},
|
||||||
...defaultOpts,
|
useHook: ({ fetch }) => ({ wishlist } = {}) => {
|
||||||
...options,
|
|
||||||
body: { itemId },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extendHook(customFetcher: typeof fetcher) {
|
|
||||||
const useRemoveItem = (opts?: UseWishlistOptions) => {
|
|
||||||
const { data: customer } = useCustomer()
|
const { data: customer } = useCustomer()
|
||||||
const { revalidate } = useWishlist(opts)
|
const { revalidate } = useWishlist(wishlist)
|
||||||
const fn = useWishlistRemoveItem<Wishlist | null, RemoveItemBody>(
|
|
||||||
defaultOpts,
|
|
||||||
customFetcher
|
|
||||||
)
|
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async function removeItem(input: RemoveItemInput) {
|
async function removeItem(input) {
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
// A signed customer is required in order to have a wishlist
|
// A signed customer is required in order to have a wishlist
|
||||||
throw new CommerceError({
|
throw new CommerceError({
|
||||||
@ -45,17 +34,11 @@ export function extendHook(customFetcher: typeof fetcher) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await fn({ itemId: String(input.id) })
|
const data = await fetch({ input: { itemId: String(input.id) } })
|
||||||
await revalidate()
|
await revalidate()
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
[fn, revalidate, customer]
|
[fetch, revalidate, customer]
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
|
||||||
useRemoveItem.extend = extendHook
|
|
||||||
|
|
||||||
return useRemoveItem
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default extendHook(fetcher)
|
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
import useAddItem from './use-add-item'
|
|
||||||
import useRemoveItem from './use-remove-item'
|
|
||||||
|
|
||||||
// This hook is probably not going to be used, but it's here
|
|
||||||
// to show how a commerce should be structuring it
|
|
||||||
export default function useWishlistActions() {
|
|
||||||
const addItem = useAddItem()
|
|
||||||
const removeItem = useRemoveItem()
|
|
||||||
|
|
||||||
return { addItem, removeItem }
|
|
||||||
}
|
|
@ -1,76 +1,60 @@
|
|||||||
import { HookFetcher } from '@commerce/utils/types'
|
import { useMemo } from 'react'
|
||||||
import { SwrOptions } from '@commerce/utils/use-data'
|
import { SWRHook } from '@commerce/utils/types'
|
||||||
import useCommerceWishlist from '@commerce/wishlist/use-wishlist'
|
import useWishlist, { UseWishlist } from '@commerce/wishlist/use-wishlist'
|
||||||
import type { Wishlist } from '../api/wishlist'
|
import type { Wishlist } from '../api/wishlist'
|
||||||
import useCustomer from '../use-customer'
|
import useCustomer from '../customer/use-customer'
|
||||||
|
|
||||||
const defaultOpts = {
|
export type UseWishlistInput = { includeProducts?: boolean }
|
||||||
url: '/api/bigcommerce/wishlist',
|
|
||||||
method: 'GET',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { Wishlist }
|
export default useWishlist as UseWishlist<typeof handler>
|
||||||
|
|
||||||
export interface UseWishlistOptions {
|
export const handler: SWRHook<
|
||||||
includeProducts?: boolean
|
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 {
|
// Use a dummy base as we only care about the relative path
|
||||||
customerId?: number
|
const url = new URL(options.url!, 'http://a')
|
||||||
}
|
|
||||||
|
|
||||||
export const fetcher: HookFetcher<Wishlist | null, UseWishlistInput> = (
|
if (includeProducts) url.searchParams.set('products', '1')
|
||||||
options,
|
|
||||||
{ customerId, includeProducts },
|
|
||||||
fetch
|
|
||||||
) => {
|
|
||||||
if (!customerId) return null
|
|
||||||
|
|
||||||
// Use a dummy base as we only care about the relative path
|
return fetch({
|
||||||
const url = new URL(options?.url ?? defaultOpts.url, 'http://a')
|
url: url.pathname + url.search,
|
||||||
|
method: options.method,
|
||||||
if (includeProducts) url.searchParams.set('products', '1')
|
})
|
||||||
|
},
|
||||||
return fetch({
|
useHook: ({ useData }) => (input) => {
|
||||||
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 = {}) => {
|
|
||||||
const { data: customer } = useCustomer()
|
const { data: customer } = useCustomer()
|
||||||
const response = useCommerceWishlist(
|
const response = useData({
|
||||||
defaultOpts,
|
input: [
|
||||||
[
|
|
||||||
['customerId', customer?.entityId],
|
['customerId', customer?.entityId],
|
||||||
['includeProducts', includeProducts],
|
['includeProducts', input?.includeProducts],
|
||||||
],
|
],
|
||||||
customFetcher,
|
swrOptions: {
|
||||||
{
|
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
...swrOptions,
|
...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 (response.data?.items?.length || 0) <= 0
|
|
||||||
},
|
},
|
||||||
set: (x) => x,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return response
|
return useMemo(
|
||||||
}
|
() =>
|
||||||
|
Object.create(response, {
|
||||||
useWishlist.extend = extendHook
|
isEmpty: {
|
||||||
|
get() {
|
||||||
return useWishlist
|
return (response.data?.items?.length || 0) <= 0
|
||||||
|
},
|
||||||
|
enumerable: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[response]
|
||||||
|
)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default extendHook(fetcher)
|
|
||||||
|
@ -32,5 +32,3 @@ export interface CommerceAPIFetchOptions<Variables> {
|
|||||||
variables?: Variables
|
variables?: Variables
|
||||||
preview?: boolean
|
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