mirror of
https://github.com/vercel/commerce.git
synced 2025-05-18 07:26:59 +00:00
Merge branch 'main' of https://github.com/vercel/commerce into elasticpath-master
This commit is contained in:
commit
19300cbafc
@ -1,4 +1,4 @@
|
|||||||
# Available providers: bigcommerce, shopify, swell
|
# Available providers: local, bigcommerce, shopify, swell, saleor
|
||||||
COMMERCE_PROVIDER=
|
COMMERCE_PROVIDER=
|
||||||
|
|
||||||
BIGCOMMERCE_STOREFRONT_API_URL=
|
BIGCOMMERCE_STOREFRONT_API_URL=
|
||||||
@ -7,6 +7,10 @@ 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=
|
BIGCOMMERCE_CHANNEL_ID=
|
||||||
|
BIGCOMMERCE_STORE_URL=
|
||||||
|
BIGCOMMERCE_STORE_API_STORE_HASH=
|
||||||
|
BIGCOMMERCE_STORE_API_CLIENT_SECRET=
|
||||||
|
|
||||||
|
|
||||||
NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=
|
NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=
|
||||||
NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=
|
NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=
|
||||||
@ -16,3 +20,10 @@ NEXT_PUBLIC_SWELL_PUBLIC_KEY=
|
|||||||
|
|
||||||
NEXT_PUBLIC_SALEOR_API_URL=
|
NEXT_PUBLIC_SALEOR_API_URL=
|
||||||
NEXT_PUBLIC_SALEOR_CHANNEL=
|
NEXT_PUBLIC_SALEOR_CHANNEL=
|
||||||
|
|
||||||
|
NEXT_PUBLIC_VENDURE_SHOP_API_URL=
|
||||||
|
NEXT_PUBLIC_VENDURE_LOCAL_URL=
|
||||||
|
|
||||||
|
ORDERCLOUD_CLIENT_ID=
|
||||||
|
ORDERCLOUD_CLIENT_SECRET=
|
||||||
|
STRIPE_SECRET=
|
||||||
|
6
.eslintrc
Normal file
6
.eslintrc
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": ["next", "prettier"],
|
||||||
|
"rules": {
|
||||||
|
"react/no-unescaped-entities": "off"
|
||||||
|
}
|
||||||
|
}
|
10
README.md
10
README.md
@ -66,7 +66,13 @@ Every provider defines the features that it supports under `framework/{provider}
|
|||||||
|
|
||||||
#### Features Available
|
#### Features Available
|
||||||
|
|
||||||
|
The following features can be enabled or disabled. This means that the UI will remove all code related to the feature.
|
||||||
|
For example: Turning `cart` off will disable Cart capabilities.
|
||||||
|
|
||||||
|
- cart
|
||||||
|
- search
|
||||||
- wishlist
|
- wishlist
|
||||||
|
- customerAuth
|
||||||
- customCheckout
|
- customCheckout
|
||||||
|
|
||||||
#### How to turn Features on and off
|
#### How to turn Features on and off
|
||||||
@ -83,7 +89,7 @@ Every provider defines the features that it supports under `framework/{provider}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
- Turn wishlist on by setting wishlist to true.
|
- Turn `wishlist` on by setting `wishlist` to `true`.
|
||||||
- Run the app and the wishlist functionality should be back on.
|
- Run the app and the wishlist functionality should be back on.
|
||||||
|
|
||||||
### How to create a new provider
|
### How to create a new provider
|
||||||
@ -145,5 +151,5 @@ Next, you're free to customize the starter. More updates coming soon. Stay tuned
|
|||||||
After Email confirmation, Checkout should be manually enabled through BigCommerce platform. Look for "Review & test your store" section through BigCommerce's dashboard.
|
After Email confirmation, Checkout should be manually enabled through BigCommerce platform. Look for "Review & test your store" section through BigCommerce's dashboard.
|
||||||
<br>
|
<br>
|
||||||
<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 details about this subject.
|
||||||
</details>
|
</details>
|
||||||
|
@ -77,7 +77,6 @@ html {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
touch-action: manipulation;
|
touch-action: manipulation;
|
||||||
font-feature-settings: 'case' 1, 'rlig' 1, 'calt' 0;
|
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
@ -38,6 +38,7 @@ const LoginView: FC<Props> = () => {
|
|||||||
} catch ({ errors }) {
|
} catch ({ errors }) {
|
||||||
setMessage(errors[0].message)
|
setMessage(errors[0].message)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
setDisabled(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,6 +70,9 @@ const CartItem = ({
|
|||||||
if (item.quantity !== Number(quantity)) {
|
if (item.quantity !== Number(quantity)) {
|
||||||
setQuantity(item.quantity)
|
setQuantity(item.quantity)
|
||||||
}
|
}
|
||||||
|
// TODO: currently not including quantity in deps is intended, but we should
|
||||||
|
// do this differently as it could break easily
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [item.quantity])
|
}, [item.quantity])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -82,25 +85,29 @@ const CartItem = ({
|
|||||||
<div className="flex flex-row space-x-4 py-4">
|
<div className="flex flex-row space-x-4 py-4">
|
||||||
<div className="w-16 h-16 bg-violet relative overflow-hidden cursor-pointer z-0">
|
<div className="w-16 h-16 bg-violet relative overflow-hidden cursor-pointer z-0">
|
||||||
<Link href={`/product/${item.path}`}>
|
<Link href={`/product/${item.path}`}>
|
||||||
<Image
|
<a>
|
||||||
onClick={() => closeSidebarIfPresent()}
|
<Image
|
||||||
className={s.productImage}
|
onClick={() => closeSidebarIfPresent()}
|
||||||
width={150}
|
className={s.productImage}
|
||||||
height={150}
|
width={150}
|
||||||
src={item.variant.image!.url}
|
height={150}
|
||||||
alt={item.variant.image!.altText}
|
src={item.variant.image!.url}
|
||||||
unoptimized
|
alt={item.variant.image!.altText}
|
||||||
/>
|
unoptimized
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex flex-col text-base">
|
<div className="flex-1 flex flex-col text-base">
|
||||||
<Link href={`/product/${item.path}`}>
|
<Link href={`/product/${item.path}`}>
|
||||||
<span
|
<a>
|
||||||
className={s.productName}
|
<span
|
||||||
onClick={() => closeSidebarIfPresent()}
|
className={s.productName}
|
||||||
>
|
onClick={() => closeSidebarIfPresent()}
|
||||||
{item.name}
|
>
|
||||||
</span>
|
{item.name}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
{options && options.length > 0 && (
|
{options && options.length > 0 && (
|
||||||
<div className="flex items-center pb-1">
|
<div className="flex items-center pb-1">
|
||||||
|
@ -74,9 +74,11 @@ const CartSidebarView: FC = () => {
|
|||||||
<>
|
<>
|
||||||
<div className="px-4 sm:px-6 flex-1">
|
<div className="px-4 sm:px-6 flex-1">
|
||||||
<Link href="/cart">
|
<Link href="/cart">
|
||||||
<Text variant="sectionHeading" onClick={handleClose}>
|
<a>
|
||||||
My Cart
|
<Text variant="sectionHeading" onClick={handleClose}>
|
||||||
</Text>
|
My Cart
|
||||||
|
</Text>
|
||||||
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
<ul className={s.lineItemsList}>
|
<ul className={s.lineItemsList}>
|
||||||
{data!.lineItems.map((item: any) => (
|
{data!.lineItems.map((item: any) => (
|
||||||
|
@ -1,30 +1,39 @@
|
|||||||
import cn from 'classnames'
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import CartItem from '@components/cart/CartItem'
|
import CartItem from '@components/cart/CartItem'
|
||||||
import { Button, Text } from '@components/ui'
|
import { Button, Text } from '@components/ui'
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
|
import SidebarLayout from '@components/common/SidebarLayout'
|
||||||
import useCart from '@framework/cart/use-cart'
|
import useCart from '@framework/cart/use-cart'
|
||||||
import usePrice from '@framework/product/use-price'
|
import usePrice from '@framework/product/use-price'
|
||||||
|
import useCheckout from '@framework/checkout/use-checkout'
|
||||||
import ShippingWidget from '../ShippingWidget'
|
import ShippingWidget from '../ShippingWidget'
|
||||||
import PaymentWidget from '../PaymentWidget'
|
import PaymentWidget from '../PaymentWidget'
|
||||||
import SidebarLayout from '@components/common/SidebarLayout'
|
|
||||||
import s from './CheckoutSidebarView.module.css'
|
import s from './CheckoutSidebarView.module.css'
|
||||||
|
|
||||||
const CheckoutSidebarView: FC = () => {
|
const CheckoutSidebarView: FC = () => {
|
||||||
const { setSidebarView } = useUI()
|
const { setSidebarView, closeSidebar } = useUI()
|
||||||
const { data } = useCart()
|
const { data: cartData } = useCart()
|
||||||
|
const { data: checkoutData, submit: onCheckout } = useCheckout()
|
||||||
|
|
||||||
|
async function handleSubmit(event: React.ChangeEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
await onCheckout()
|
||||||
|
|
||||||
|
closeSidebar()
|
||||||
|
}
|
||||||
|
|
||||||
const { price: subTotal } = usePrice(
|
const { price: subTotal } = usePrice(
|
||||||
data && {
|
cartData && {
|
||||||
amount: Number(data.subtotalPrice),
|
amount: Number(cartData.subtotalPrice),
|
||||||
currencyCode: data.currency.code,
|
currencyCode: cartData.currency.code,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
const { price: total } = usePrice(
|
const { price: total } = usePrice(
|
||||||
data && {
|
cartData && {
|
||||||
amount: Number(data.totalPrice),
|
amount: Number(cartData.totalPrice),
|
||||||
currencyCode: data.currency.code,
|
currencyCode: cartData.currency.code,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -35,25 +44,36 @@ const CheckoutSidebarView: FC = () => {
|
|||||||
>
|
>
|
||||||
<div className="px-4 sm:px-6 flex-1">
|
<div className="px-4 sm:px-6 flex-1">
|
||||||
<Link href="/cart">
|
<Link href="/cart">
|
||||||
<Text variant="sectionHeading">Checkout</Text>
|
<a>
|
||||||
|
<Text variant="sectionHeading">Checkout</Text>
|
||||||
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<PaymentWidget onClick={() => setSidebarView('PAYMENT_VIEW')} />
|
<PaymentWidget
|
||||||
<ShippingWidget onClick={() => setSidebarView('SHIPPING_VIEW')} />
|
isValid={checkoutData?.hasPayment}
|
||||||
|
onClick={() => setSidebarView('PAYMENT_VIEW')}
|
||||||
|
/>
|
||||||
|
<ShippingWidget
|
||||||
|
isValid={checkoutData?.hasShipping}
|
||||||
|
onClick={() => setSidebarView('SHIPPING_VIEW')}
|
||||||
|
/>
|
||||||
|
|
||||||
<ul className={s.lineItemsList}>
|
<ul className={s.lineItemsList}>
|
||||||
{data!.lineItems.map((item: any) => (
|
{cartData!.lineItems.map((item: any) => (
|
||||||
<CartItem
|
<CartItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
currencyCode={data!.currency.code}
|
currencyCode={cartData!.currency.code}
|
||||||
variant="display"
|
variant="display"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-shrink-0 px-6 py-6 sm:px-6 sticky z-20 bottom-0 w-full right-0 left-0 bg-accent-0 border-t text-sm">
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex-shrink-0 px-6 py-6 sm:px-6 sticky z-20 bottom-0 w-full right-0 left-0 bg-accent-0 border-t text-sm"
|
||||||
|
>
|
||||||
<ul className="pb-2">
|
<ul className="pb-2">
|
||||||
<li className="flex justify-between py-1">
|
<li className="flex justify-between py-1">
|
||||||
<span>Subtotal</span>
|
<span>Subtotal</span>
|
||||||
@ -74,14 +94,15 @@ const CheckoutSidebarView: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{/* Once data is correcly filled */}
|
{/* Once data is correcly filled */}
|
||||||
{/* <Button Component="a" width="100%">
|
<Button
|
||||||
Confirm Purchase
|
type="submit"
|
||||||
</Button> */}
|
width="100%"
|
||||||
<Button Component="a" width="100%" variant="ghost" disabled>
|
disabled={!checkoutData?.hasPayment || !checkoutData?.hasShipping}
|
||||||
Continue
|
>
|
||||||
|
Confirm Purchase
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</SidebarLayout>
|
</SidebarLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,83 +1,129 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
|
|
||||||
|
import useAddCard from '@framework/customer/card/use-add-item'
|
||||||
import { Button, Text } from '@components/ui'
|
import { Button, Text } from '@components/ui'
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
import s from './PaymentMethodView.module.css'
|
|
||||||
import SidebarLayout from '@components/common/SidebarLayout'
|
import SidebarLayout from '@components/common/SidebarLayout'
|
||||||
|
|
||||||
|
import s from './PaymentMethodView.module.css'
|
||||||
|
|
||||||
|
interface Form extends HTMLFormElement {
|
||||||
|
cardHolder: HTMLInputElement
|
||||||
|
cardNumber: HTMLInputElement
|
||||||
|
cardExpireDate: HTMLInputElement
|
||||||
|
cardCvc: HTMLInputElement
|
||||||
|
firstName: HTMLInputElement
|
||||||
|
lastName: HTMLInputElement
|
||||||
|
company: HTMLInputElement
|
||||||
|
streetNumber: HTMLInputElement
|
||||||
|
zipCode: HTMLInputElement
|
||||||
|
city: HTMLInputElement
|
||||||
|
country: HTMLSelectElement
|
||||||
|
}
|
||||||
|
|
||||||
const PaymentMethodView: FC = () => {
|
const PaymentMethodView: FC = () => {
|
||||||
const { setSidebarView } = useUI()
|
const { setSidebarView } = useUI()
|
||||||
|
const addCard = useAddCard()
|
||||||
|
|
||||||
|
async function handleSubmit(event: React.ChangeEvent<Form>) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
await addCard({
|
||||||
|
cardHolder: event.target.cardHolder.value,
|
||||||
|
cardNumber: event.target.cardNumber.value,
|
||||||
|
cardExpireDate: event.target.cardExpireDate.value,
|
||||||
|
cardCvc: event.target.cardCvc.value,
|
||||||
|
firstName: event.target.firstName.value,
|
||||||
|
lastName: event.target.lastName.value,
|
||||||
|
company: event.target.company.value,
|
||||||
|
streetNumber: event.target.streetNumber.value,
|
||||||
|
zipCode: event.target.zipCode.value,
|
||||||
|
city: event.target.city.value,
|
||||||
|
country: event.target.country.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
setSidebarView('CHECKOUT_VIEW')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarLayout handleBack={() => setSidebarView('CHECKOUT_VIEW')}>
|
<form className="h-full" onSubmit={handleSubmit}>
|
||||||
<div className="px-4 sm:px-6 flex-1">
|
<SidebarLayout handleBack={() => setSidebarView('CHECKOUT_VIEW')}>
|
||||||
<Text variant="sectionHeading"> Payment Method</Text>
|
<div className="px-4 sm:px-6 flex-1">
|
||||||
<div>
|
<Text variant="sectionHeading"> Payment Method</Text>
|
||||||
<div className={s.fieldset}>
|
<div>
|
||||||
<label className={s.label}>Cardholder Name</label>
|
<div className={s.fieldset}>
|
||||||
<input className={s.input} />
|
<label className={s.label}>Cardholder Name</label>
|
||||||
</div>
|
<input name="cardHolder" className={s.input} />
|
||||||
<div className="grid gap-3 grid-flow-row grid-cols-12">
|
|
||||||
<div className={cn(s.fieldset, 'col-span-7')}>
|
|
||||||
<label className={s.label}>Card Number</label>
|
|
||||||
<input className={s.input} />
|
|
||||||
</div>
|
</div>
|
||||||
<div className={cn(s.fieldset, 'col-span-3')}>
|
<div className="grid gap-3 grid-flow-row grid-cols-12">
|
||||||
<label className={s.label}>Expires</label>
|
<div className={cn(s.fieldset, 'col-span-7')}>
|
||||||
<input className={s.input} placeholder="MM/YY" />
|
<label className={s.label}>Card Number</label>
|
||||||
|
<input name="cardNumber" className={s.input} />
|
||||||
|
</div>
|
||||||
|
<div className={cn(s.fieldset, 'col-span-3')}>
|
||||||
|
<label className={s.label}>Expires</label>
|
||||||
|
<input
|
||||||
|
name="cardExpireDate"
|
||||||
|
className={s.input}
|
||||||
|
placeholder="MM/YY"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={cn(s.fieldset, 'col-span-2')}>
|
||||||
|
<label className={s.label}>CVC</label>
|
||||||
|
<input name="cardCvc" className={s.input} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={cn(s.fieldset, 'col-span-2')}>
|
<hr className="border-accent-2 my-6" />
|
||||||
<label className={s.label}>CVC</label>
|
<div className="grid gap-3 grid-flow-row grid-cols-12">
|
||||||
<input className={s.input} />
|
<div className={cn(s.fieldset, 'col-span-6')}>
|
||||||
|
<label className={s.label}>First Name</label>
|
||||||
|
<input name="firstName" className={s.input} />
|
||||||
|
</div>
|
||||||
|
<div className={cn(s.fieldset, 'col-span-6')}>
|
||||||
|
<label className={s.label}>Last Name</label>
|
||||||
|
<input name="lastName" className={s.input} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className={s.fieldset}>
|
||||||
<hr className="border-accent-2 my-6" />
|
<label className={s.label}>Company (Optional)</label>
|
||||||
<div className="grid gap-3 grid-flow-row grid-cols-12">
|
<input name="company" className={s.input} />
|
||||||
<div className={cn(s.fieldset, 'col-span-6')}>
|
|
||||||
<label className={s.label}>First Name</label>
|
|
||||||
<input className={s.input} />
|
|
||||||
</div>
|
</div>
|
||||||
<div className={cn(s.fieldset, 'col-span-6')}>
|
<div className={s.fieldset}>
|
||||||
<label className={s.label}>Last Name</label>
|
<label className={s.label}>Street and House Number</label>
|
||||||
<input className={s.input} />
|
<input name="streetNumber" className={s.input} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className={s.fieldset}>
|
||||||
<div className={s.fieldset}>
|
<label className={s.label}>
|
||||||
<label className={s.label}>Company (Optional)</label>
|
Apartment, Suite, Etc. (Optional)
|
||||||
<input className={s.input} />
|
</label>
|
||||||
</div>
|
<input className={s.input} name="apartment" />
|
||||||
<div className={s.fieldset}>
|
|
||||||
<label className={s.label}>Street and House Number</label>
|
|
||||||
<input className={s.input} />
|
|
||||||
</div>
|
|
||||||
<div className={s.fieldset}>
|
|
||||||
<label className={s.label}>Apartment, Suite, Etc. (Optional)</label>
|
|
||||||
<input className={s.input} />
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-3 grid-flow-row grid-cols-12">
|
|
||||||
<div className={cn(s.fieldset, 'col-span-6')}>
|
|
||||||
<label className={s.label}>Postal Code</label>
|
|
||||||
<input className={s.input} />
|
|
||||||
</div>
|
</div>
|
||||||
<div className={cn(s.fieldset, 'col-span-6')}>
|
<div className="grid gap-3 grid-flow-row grid-cols-12">
|
||||||
<label className={s.label}>City</label>
|
<div className={cn(s.fieldset, 'col-span-6')}>
|
||||||
<input className={s.input} />
|
<label className={s.label}>Postal Code</label>
|
||||||
|
<input name="zipCode" className={s.input} />
|
||||||
|
</div>
|
||||||
|
<div className={cn(s.fieldset, 'col-span-6')}>
|
||||||
|
<label className={s.label}>City</label>
|
||||||
|
<input name="city" className={s.input} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={s.fieldset}>
|
||||||
|
<label className={s.label}>Country/Region</label>
|
||||||
|
<select name="country" className={s.select}>
|
||||||
|
<option>Hong Kong</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className={s.fieldset}>
|
|
||||||
<label className={s.label}>Country/Region</label>
|
|
||||||
<select className={s.select}>
|
|
||||||
<option>Hong Kong</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="sticky z-20 bottom-0 w-full right-0 left-0 py-12 bg-accent-0 border-t border-accent-2 px-6">
|
||||||
<div className="sticky z-20 bottom-0 w-full right-0 left-0 py-12 bg-accent-0 border-t border-accent-2 px-6">
|
<Button type="submit" width="100%" variant="ghost">
|
||||||
<Button Component="a" width="100%" variant="ghost">
|
Continue
|
||||||
Continue
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
</SidebarLayout>
|
||||||
</SidebarLayout>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import s from './PaymentWidget.module.css'
|
import s from './PaymentWidget.module.css'
|
||||||
import { ChevronRight, CreditCard } from '@components/icons'
|
import { ChevronRight, CreditCard, Check } from '@components/icons'
|
||||||
|
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
onClick?: () => any
|
onClick?: () => any
|
||||||
|
isValid?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const PaymentWidget: FC<ComponentProps> = ({ onClick }) => {
|
const PaymentWidget: FC<ComponentProps> = ({ onClick, isValid }) => {
|
||||||
/* Shipping Address
|
/* Shipping Address
|
||||||
Only available with checkout set to true -
|
Only available with checkout set to true -
|
||||||
This means that the provider does offer checkout functionality. */
|
This means that the provider does offer checkout functionality. */
|
||||||
return (
|
return (
|
||||||
<div onClick={onClick} className={s.root}>
|
<div onClick={onClick} className={s.root}>
|
||||||
@ -19,9 +20,7 @@ const PaymentWidget: FC<ComponentProps> = ({ onClick }) => {
|
|||||||
</span>
|
</span>
|
||||||
{/* <span>VISA #### #### #### 2345</span> */}
|
{/* <span>VISA #### #### #### 2345</span> */}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>{isValid ? <Check /> : <ChevronRight />}</div>
|
||||||
<ChevronRight />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,77 +1,117 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import s from './ShippingView.module.css'
|
|
||||||
import Button from '@components/ui/Button'
|
import Button from '@components/ui/Button'
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
import SidebarLayout from '@components/common/SidebarLayout'
|
import SidebarLayout from '@components/common/SidebarLayout'
|
||||||
|
import useAddAddress from '@framework/customer/address/use-add-item'
|
||||||
|
|
||||||
|
import s from './ShippingView.module.css'
|
||||||
|
|
||||||
|
interface Form extends HTMLFormElement {
|
||||||
|
cardHolder: HTMLInputElement
|
||||||
|
cardNumber: HTMLInputElement
|
||||||
|
cardExpireDate: HTMLInputElement
|
||||||
|
cardCvc: HTMLInputElement
|
||||||
|
firstName: HTMLInputElement
|
||||||
|
lastName: HTMLInputElement
|
||||||
|
company: HTMLInputElement
|
||||||
|
streetNumber: HTMLInputElement
|
||||||
|
zipCode: HTMLInputElement
|
||||||
|
city: HTMLInputElement
|
||||||
|
country: HTMLSelectElement
|
||||||
|
}
|
||||||
|
|
||||||
const PaymentMethodView: FC = () => {
|
const PaymentMethodView: FC = () => {
|
||||||
const { setSidebarView } = useUI()
|
const { setSidebarView } = useUI()
|
||||||
|
const addAddress = useAddAddress()
|
||||||
|
|
||||||
|
async function handleSubmit(event: React.ChangeEvent<Form>) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
await addAddress({
|
||||||
|
type: event.target.type.value,
|
||||||
|
firstName: event.target.firstName.value,
|
||||||
|
lastName: event.target.lastName.value,
|
||||||
|
company: event.target.company.value,
|
||||||
|
streetNumber: event.target.streetNumber.value,
|
||||||
|
apartments: event.target.streetNumber.value,
|
||||||
|
zipCode: event.target.zipCode.value,
|
||||||
|
city: event.target.city.value,
|
||||||
|
country: event.target.country.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
setSidebarView('CHECKOUT_VIEW')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarLayout handleBack={() => setSidebarView('CHECKOUT_VIEW')}>
|
<form className="h-full" onSubmit={handleSubmit}>
|
||||||
<div className="px-4 sm:px-6 flex-1">
|
<SidebarLayout handleBack={() => setSidebarView('CHECKOUT_VIEW')}>
|
||||||
<h2 className="pt-1 pb-8 text-2xl font-semibold tracking-wide cursor-pointer inline-block">
|
<div className="px-4 sm:px-6 flex-1">
|
||||||
Shipping
|
<h2 className="pt-1 pb-8 text-2xl font-semibold tracking-wide cursor-pointer inline-block">
|
||||||
</h2>
|
Shipping
|
||||||
<div>
|
</h2>
|
||||||
<div className="flex flex-row my-3 items-center">
|
<div>
|
||||||
<input className={s.radio} type="radio" />
|
<div className="flex flex-row my-3 items-center">
|
||||||
<span className="ml-3 text-sm">Same as billing address</span>
|
<input name="type" className={s.radio} type="radio" />
|
||||||
</div>
|
<span className="ml-3 text-sm">Same as billing address</span>
|
||||||
<div className="flex flex-row my-3 items-center">
|
|
||||||
<input className={s.radio} type="radio" />
|
|
||||||
<span className="ml-3 text-sm">
|
|
||||||
Use a different shipping address
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<hr className="border-accent-2 my-6" />
|
|
||||||
<div className="grid gap-3 grid-flow-row grid-cols-12">
|
|
||||||
<div className={cn(s.fieldset, 'col-span-6')}>
|
|
||||||
<label className={s.label}>First Name</label>
|
|
||||||
<input className={s.input} />
|
|
||||||
</div>
|
</div>
|
||||||
<div className={cn(s.fieldset, 'col-span-6')}>
|
<div className="flex flex-row my-3 items-center">
|
||||||
<label className={s.label}>Last Name</label>
|
<input name="type" className={s.radio} type="radio" />
|
||||||
<input className={s.input} />
|
<span className="ml-3 text-sm">
|
||||||
|
Use a different shipping address
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<hr className="border-accent-2 my-6" />
|
||||||
<div className={s.fieldset}>
|
<div className="grid gap-3 grid-flow-row grid-cols-12">
|
||||||
<label className={s.label}>Company (Optional)</label>
|
<div className={cn(s.fieldset, 'col-span-6')}>
|
||||||
<input className={s.input} />
|
<label className={s.label}>First Name</label>
|
||||||
</div>
|
<input name="firstName" className={s.input} />
|
||||||
<div className={s.fieldset}>
|
</div>
|
||||||
<label className={s.label}>Street and House Number</label>
|
<div className={cn(s.fieldset, 'col-span-6')}>
|
||||||
<input className={s.input} />
|
<label className={s.label}>Last Name</label>
|
||||||
</div>
|
<input name="lastName" className={s.input} />
|
||||||
<div className={s.fieldset}>
|
</div>
|
||||||
<label className={s.label}>Apartment, Suite, Etc. (Optional)</label>
|
|
||||||
<input className={s.input} />
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-3 grid-flow-row grid-cols-12">
|
|
||||||
<div className={cn(s.fieldset, 'col-span-6')}>
|
|
||||||
<label className={s.label}>Postal Code</label>
|
|
||||||
<input className={s.input} />
|
|
||||||
</div>
|
</div>
|
||||||
<div className={cn(s.fieldset, 'col-span-6')}>
|
<div className={s.fieldset}>
|
||||||
<label className={s.label}>City</label>
|
<label className={s.label}>Company (Optional)</label>
|
||||||
<input className={s.input} />
|
<input name="company" className={s.input} />
|
||||||
|
</div>
|
||||||
|
<div className={s.fieldset}>
|
||||||
|
<label className={s.label}>Street and House Number</label>
|
||||||
|
<input name="streetNumber" className={s.input} />
|
||||||
|
</div>
|
||||||
|
<div className={s.fieldset}>
|
||||||
|
<label className={s.label}>
|
||||||
|
Apartment, Suite, Etc. (Optional)
|
||||||
|
</label>
|
||||||
|
<input name="apartments" className={s.input} />
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 grid-flow-row grid-cols-12">
|
||||||
|
<div className={cn(s.fieldset, 'col-span-6')}>
|
||||||
|
<label className={s.label}>Postal Code</label>
|
||||||
|
<input name="zipCode" className={s.input} />
|
||||||
|
</div>
|
||||||
|
<div className={cn(s.fieldset, 'col-span-6')}>
|
||||||
|
<label className={s.label}>City</label>
|
||||||
|
<input name="city" className={s.input} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={s.fieldset}>
|
||||||
|
<label className={s.label}>Country/Region</label>
|
||||||
|
<select name="country" className={s.select}>
|
||||||
|
<option>Hong Kong</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className={s.fieldset}>
|
|
||||||
<label className={s.label}>Country/Region</label>
|
|
||||||
<select className={s.select}>
|
|
||||||
<option>Hong Kong</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="sticky z-20 bottom-0 w-full right-0 left-0 py-12 bg-accent-0 border-t border-accent-2 px-6">
|
||||||
<div className="sticky z-20 bottom-0 w-full right-0 left-0 py-12 bg-accent-0 border-t border-accent-2 px-6">
|
<Button type="submit" width="100%" variant="ghost">
|
||||||
<Button Component="a" width="100%" variant="ghost">
|
Continue
|
||||||
Continue
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
</SidebarLayout>
|
||||||
</SidebarLayout>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import s from './ShippingWidget.module.css'
|
import s from './ShippingWidget.module.css'
|
||||||
import { ChevronRight, MapPin } from '@components/icons'
|
import { ChevronRight, MapPin, Check } from '@components/icons'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
|
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
onClick?: () => any
|
onClick?: () => any
|
||||||
|
isValid?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShippingWidget: FC<ComponentProps> = ({ onClick }) => {
|
const ShippingWidget: FC<ComponentProps> = ({ onClick, isValid }) => {
|
||||||
/* Shipping Address
|
/* Shipping Address
|
||||||
Only available with checkout set to true -
|
Only available with checkout set to true -
|
||||||
This means that the provider does offer checkout functionality. */
|
This means that the provider does offer checkout functionality. */
|
||||||
return (
|
return (
|
||||||
<div onClick={onClick} className={s.root}>
|
<div onClick={onClick} className={s.root}>
|
||||||
@ -23,9 +24,7 @@ const ShippingWidget: FC<ComponentProps> = ({ onClick }) => {
|
|||||||
San Franssisco, California
|
San Franssisco, California
|
||||||
</span> */}
|
</span> */}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>{isValid ? <Check /> : <ChevronRight />}</div>
|
||||||
<ChevronRight />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,6 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
|||||||
<span>© 2020 CKUBE. All rights reserved.</span>
|
<span>© 2020 CKUBE. All rights reserved.</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center text-primary text-sm">
|
<div className="flex items-center text-primary text-sm">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
|
@ -24,7 +24,7 @@ const Loading = () => (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const dynamicProps = {
|
const dynamicProps = {
|
||||||
loading: () => <Loading />,
|
loading: Loading,
|
||||||
}
|
}
|
||||||
|
|
||||||
const SignUpView = dynamic(
|
const SignUpView = dynamic(
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply sticky top-0 bg-primary z-40 transition-all duration-150;
|
@apply sticky top-0 bg-primary z-40 transition-all duration-150;
|
||||||
|
min-height: 74px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
|
@ -34,9 +34,11 @@ const Navbar: FC<NavbarProps> = ({ links }) => (
|
|||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div className="justify-center flex-1 hidden lg:flex">
|
{process.env.COMMERCE_SEARCH_ENABLED && (
|
||||||
<Searchbar />
|
<div className="justify-center flex-1 hidden lg:flex">
|
||||||
</div>
|
<Searchbar />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex items-center justify-end flex-1 space-x-8">
|
<div className="flex items-center justify-end flex-1 space-x-8">
|
||||||
<UserNav />
|
<UserNav />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC, InputHTMLAttributes, useEffect, useMemo } from 'react'
|
import { FC, memo, useEffect } from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import s from './Searchbar.module.css'
|
import s from './Searchbar.module.css'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
@ -13,7 +13,7 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
router.prefetch('/search')
|
router.prefetch('/search')
|
||||||
}, [])
|
}, [router])
|
||||||
|
|
||||||
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@ -32,32 +32,29 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return useMemo(
|
return (
|
||||||
() => (
|
<div className={cn(s.root, className)}>
|
||||||
<div className={cn(s.root, className)}>
|
<label className="hidden" htmlFor={id}>
|
||||||
<label className="hidden" htmlFor={id}>
|
Search
|
||||||
Search
|
</label>
|
||||||
</label>
|
<input
|
||||||
<input
|
id={id}
|
||||||
id={id}
|
className={s.input}
|
||||||
className={s.input}
|
placeholder="Search for products..."
|
||||||
placeholder="Search for products..."
|
defaultValue={router.query.q}
|
||||||
defaultValue={router.query.q}
|
onKeyUp={handleKeyUp}
|
||||||
onKeyUp={handleKeyUp}
|
/>
|
||||||
/>
|
<div className={s.iconContainer}>
|
||||||
<div className={s.iconContainer}>
|
<svg className={s.icon} fill="currentColor" viewBox="0 0 20 20">
|
||||||
<svg className={s.icon} fill="currentColor" viewBox="0 0 20 20">
|
<path
|
||||||
<path
|
fillRule="evenodd"
|
||||||
fillRule="evenodd"
|
clipRule="evenodd"
|
||||||
clipRule="evenodd"
|
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
||||||
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
/>
|
||||||
/>
|
</svg>
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
</div>
|
||||||
[]
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Searchbar
|
export default memo(Searchbar)
|
||||||
|
@ -7,6 +7,7 @@ import useCustomer from '@framework/customer/use-customer'
|
|||||||
import { Avatar } from '@components/common'
|
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 Button from '@components/ui/Button'
|
||||||
import DropdownMenu from './DropdownMenu'
|
import DropdownMenu from './DropdownMenu'
|
||||||
import s from './UserNav.module.css'
|
import s from './UserNav.module.css'
|
||||||
|
|
||||||
@ -25,10 +26,14 @@ const UserNav: FC<Props> = ({ className }) => {
|
|||||||
return (
|
return (
|
||||||
<nav className={cn(s.root, className)}>
|
<nav className={cn(s.root, className)}>
|
||||||
<ul className={s.list}>
|
<ul className={s.list}>
|
||||||
<li className={s.item} onClick={toggleSidebar}>
|
{process.env.COMMERCE_CART_ENABLED && (
|
||||||
<Bag />
|
<li className={s.item}>
|
||||||
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
|
<Button className={s.item} variant="naked" onClick={toggleSidebar} aria-label="Cart">
|
||||||
</li>
|
<Bag />
|
||||||
|
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
||||||
<li className={s.item}>
|
<li className={s.item}>
|
||||||
<Link href="/wishlist">
|
<Link href="/wishlist">
|
||||||
@ -38,7 +43,7 @@ const UserNav: FC<Props> = ({ className }) => {
|
|||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
{process.env.COMMERCE_CUSTOMER_ENABLED && (
|
{process.env.COMMERCE_CUSTOMERAUTH_ENABLED && (
|
||||||
<li className={s.item}>
|
<li className={s.item}>
|
||||||
{customer ? (
|
{customer ? (
|
||||||
<DropdownMenu />
|
<DropdownMenu />
|
||||||
|
@ -7,11 +7,12 @@ import Image, { ImageProps } from 'next/image'
|
|||||||
import WishlistButton from '@components/wishlist/WishlistButton'
|
import WishlistButton from '@components/wishlist/WishlistButton'
|
||||||
import usePrice from '@framework/product/use-price'
|
import usePrice from '@framework/product/use-price'
|
||||||
import ProductTag from '../ProductTag'
|
import ProductTag from '../ProductTag'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
product: Product
|
product: Product
|
||||||
noNameTag?: boolean
|
noNameTag?: boolean
|
||||||
imgProps?: Omit<ImageProps, 'src'>
|
imgProps?: Omit<ImageProps, 'src' | 'layout' | 'placeholder' | 'blurDataURL'>
|
||||||
variant?: 'default' | 'slim' | 'simple'
|
variant?: 'default' | 'slim' | 'simple'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,7 +24,6 @@ const ProductCard: FC<Props> = ({
|
|||||||
className,
|
className,
|
||||||
noNameTag = false,
|
noNameTag = false,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
...props
|
|
||||||
}) => {
|
}) => {
|
||||||
const { price } = usePrice({
|
const { price } = usePrice({
|
||||||
amount: product.price.value,
|
amount: product.price.value,
|
||||||
@ -38,7 +38,7 @@ const ProductCard: FC<Props> = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/product/${product.slug}`} {...props}>
|
<Link href={`/product/${product.slug}`}>
|
||||||
<a className={rootClassName}>
|
<a className={rootClassName}>
|
||||||
{variant === 'slim' && (
|
{variant === 'slim' && (
|
||||||
<>
|
<>
|
||||||
@ -83,7 +83,7 @@ const ProductCard: FC<Props> = ({
|
|||||||
<Image
|
<Image
|
||||||
alt={product.name || 'Product Image'}
|
alt={product.name || 'Product Image'}
|
||||||
className={s.productImage}
|
className={s.productImage}
|
||||||
src={product.images[0].url || placeholderImg}
|
src={product.images[0]?.url || placeholderImg}
|
||||||
height={540}
|
height={540}
|
||||||
width={540}
|
width={540}
|
||||||
quality="85"
|
quality="85"
|
||||||
|
@ -1,50 +1,52 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
import { Swatch } from '@components/product'
|
import { Swatch } from '@components/product'
|
||||||
import type { ProductOption } from '@commerce/types/product'
|
import type { ProductOption } from '@commerce/types/product'
|
||||||
import { SelectedOptions } from '../helpers'
|
import { SelectedOptions } from '../helpers'
|
||||||
import React from 'react'
|
|
||||||
interface ProductOptionsProps {
|
interface ProductOptionsProps {
|
||||||
options: ProductOption[]
|
options: ProductOption[]
|
||||||
selectedOptions: SelectedOptions
|
selectedOptions: SelectedOptions
|
||||||
setSelectedOptions: React.Dispatch<React.SetStateAction<SelectedOptions>>
|
setSelectedOptions: React.Dispatch<React.SetStateAction<SelectedOptions>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductOptions: React.FC<ProductOptionsProps> = React.memo(
|
const ProductOptions: React.FC<ProductOptionsProps> = ({
|
||||||
({ options, selectedOptions, setSelectedOptions }) => {
|
options,
|
||||||
return (
|
selectedOptions,
|
||||||
<div>
|
setSelectedOptions,
|
||||||
{options.map((opt) => (
|
}) => {
|
||||||
<div className="pb-4" key={opt.displayName}>
|
return (
|
||||||
<h2 className="uppercase font-medium text-sm tracking-wide">
|
<div>
|
||||||
{opt.displayName}
|
{options.map((opt) => (
|
||||||
</h2>
|
<div className="pb-4" key={opt.displayName}>
|
||||||
<div className="flex flex-row py-4">
|
<h2 className="uppercase font-medium text-sm tracking-wide">
|
||||||
{opt.values.map((v, i: number) => {
|
{opt.displayName}
|
||||||
const active = selectedOptions[opt.displayName.toLowerCase()]
|
</h2>
|
||||||
return (
|
<div className="flex flex-row py-4">
|
||||||
<Swatch
|
{opt.values.map((v, i: number) => {
|
||||||
key={`${opt.id}-${i}`}
|
const active = selectedOptions[opt.displayName.toLowerCase()]
|
||||||
active={v.label.toLowerCase() === active}
|
return (
|
||||||
variant={opt.displayName}
|
<Swatch
|
||||||
color={v.hexColors ? v.hexColors[0] : ''}
|
key={`${opt.id}-${i}`}
|
||||||
label={v.label}
|
active={v.label.toLowerCase() === active}
|
||||||
onClick={() => {
|
variant={opt.displayName}
|
||||||
setSelectedOptions((selectedOptions) => {
|
color={v.hexColors ? v.hexColors[0] : ''}
|
||||||
return {
|
label={v.label}
|
||||||
...selectedOptions,
|
onClick={() => {
|
||||||
[opt.displayName.toLowerCase()]:
|
setSelectedOptions((selectedOptions) => {
|
||||||
v.label.toLowerCase(),
|
return {
|
||||||
}
|
...selectedOptions,
|
||||||
})
|
[opt.displayName.toLowerCase()]: v.label.toLowerCase(),
|
||||||
}}
|
}
|
||||||
/>
|
})
|
||||||
)
|
}}
|
||||||
})}
|
/>
|
||||||
</div>
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
)
|
</div>
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
export default ProductOptions
|
export default memo(ProductOptions)
|
||||||
|
@ -23,7 +23,7 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
selectDefaultOptionFromProduct(product, setSelectedOptions)
|
selectDefaultOptionFromProduct(product, setSelectedOptions)
|
||||||
}, [])
|
}, [product])
|
||||||
|
|
||||||
const variant = getProductVariant(product, selectedOptions)
|
const variant = getProductVariant(product, selectedOptions)
|
||||||
const addToCart = async () => {
|
const addToCart = async () => {
|
||||||
@ -56,18 +56,20 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
|
|||||||
<div className="text-accent-6 pr-1 font-medium text-sm">36 reviews</div>
|
<div className="text-accent-6 pr-1 font-medium text-sm">36 reviews</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
{process.env.COMMERCE_CART_ENABLED && (
|
||||||
aria-label="Add to Cart"
|
<Button
|
||||||
type="button"
|
aria-label="Add to Cart"
|
||||||
className={s.button}
|
type="button"
|
||||||
onClick={addToCart}
|
className={s.button}
|
||||||
loading={loading}
|
onClick={addToCart}
|
||||||
disabled={variant?.availableForSale === false}
|
loading={loading}
|
||||||
>
|
disabled={variant?.availableForSale === false}
|
||||||
{variant?.availableForSale === false
|
>
|
||||||
? 'Not Available'
|
{variant?.availableForSale === false
|
||||||
: 'Add To Cart'}
|
? 'Not Available'
|
||||||
</Button>
|
: 'Add To Cart'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<Collapse title="Care">
|
<Collapse title="Care">
|
||||||
|
@ -66,17 +66,13 @@ const ProductSlider: React.FC<ProductSliderProps> = ({
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
sliderContainerRef.current!.addEventListener(
|
const slider = sliderContainerRef.current!
|
||||||
'touchstart',
|
|
||||||
preventNavigation
|
slider.addEventListener('touchstart', preventNavigation)
|
||||||
)
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (sliderContainerRef.current) {
|
if (slider) {
|
||||||
sliderContainerRef.current!.removeEventListener(
|
slider.removeEventListener('touchstart', preventNavigation)
|
||||||
'touchstart',
|
|
||||||
preventNavigation
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
@ -1,31 +1,30 @@
|
|||||||
|
import { FC, MouseEventHandler, memo } from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import React from 'react'
|
|
||||||
import s from './ProductSliderControl.module.css'
|
import s from './ProductSliderControl.module.css'
|
||||||
import { ArrowLeft, ArrowRight } from '@components/icons'
|
import { ArrowLeft, ArrowRight } from '@components/icons'
|
||||||
|
|
||||||
interface ProductSliderControl {
|
interface ProductSliderControl {
|
||||||
onPrev: React.MouseEventHandler<HTMLButtonElement>
|
onPrev: MouseEventHandler<HTMLButtonElement>
|
||||||
onNext: React.MouseEventHandler<HTMLButtonElement>
|
onNext: MouseEventHandler<HTMLButtonElement>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductSliderControl: React.FC<ProductSliderControl> = React.memo(
|
const ProductSliderControl: FC<ProductSliderControl> = ({ onPrev, onNext }) => (
|
||||||
({ onPrev, onNext }) => (
|
<div className={s.control}>
|
||||||
<div className={s.control}>
|
<button
|
||||||
<button
|
className={cn(s.leftControl)}
|
||||||
className={cn(s.leftControl)}
|
onClick={onPrev}
|
||||||
onClick={onPrev}
|
aria-label="Previous Product Image"
|
||||||
aria-label="Previous Product Image"
|
>
|
||||||
>
|
<ArrowLeft />
|
||||||
<ArrowLeft />
|
</button>
|
||||||
</button>
|
<button
|
||||||
<button
|
className={cn(s.rightControl)}
|
||||||
className={cn(s.rightControl)}
|
onClick={onNext}
|
||||||
onClick={onNext}
|
aria-label="Next Product Image"
|
||||||
aria-label="Next Product Image"
|
>
|
||||||
>
|
<ArrowRight />
|
||||||
<ArrowRight />
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
export default ProductSliderControl
|
|
||||||
|
export default memo(ProductSliderControl)
|
||||||
|
@ -58,7 +58,7 @@ const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ProductSidebar product={product} className={s.sidebar} />
|
<ProductSidebar key={product.id} product={product} className={s.sidebar} />
|
||||||
</div>
|
</div>
|
||||||
<hr className="mt-7 border-accent-2" />
|
<hr className="mt-7 border-accent-2" />
|
||||||
<section className="py-12 px-6 mb-10">
|
<section className="py-12 px-6 mb-10">
|
||||||
|
@ -7,19 +7,19 @@ import { useRouter } from 'next/router'
|
|||||||
import { Layout } from '@components/common'
|
import { Layout } from '@components/common'
|
||||||
import { ProductCard } from '@components/product'
|
import { ProductCard } from '@components/product'
|
||||||
import type { Product } from '@commerce/types/product'
|
import type { Product } from '@commerce/types/product'
|
||||||
import { Container, Grid, Skeleton } from '@components/ui'
|
import { Container, Skeleton } from '@components/ui'
|
||||||
|
|
||||||
import useSearch from '@framework/product/use-search'
|
import useSearch from '@framework/product/use-search'
|
||||||
|
|
||||||
import getSlug from '@lib/get-slug'
|
import getSlug from '@lib/get-slug'
|
||||||
import rangeMap from '@lib/range-map'
|
import rangeMap from '@lib/range-map'
|
||||||
|
|
||||||
const SORT = Object.entries({
|
const SORT = {
|
||||||
'trending-desc': 'Trending',
|
'trending-desc': 'Trending',
|
||||||
'latest-desc': 'Latest arrivals',
|
'latest-desc': 'Latest arrivals',
|
||||||
'price-asc': 'Price: Low to high',
|
'price-asc': 'Price: Low to high',
|
||||||
'price-desc': 'Price: High to low',
|
'price-desc': 'Price: High to low',
|
||||||
})
|
}
|
||||||
|
|
||||||
import {
|
import {
|
||||||
filterQuery,
|
filterQuery,
|
||||||
@ -351,7 +351,7 @@ export default function Search({ categories, brands }: SearchPropsType) {
|
|||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-expanded="true"
|
aria-expanded="true"
|
||||||
>
|
>
|
||||||
{sort ? `Sort: ${sort}` : 'Relevance'}
|
{sort ? SORT[sort as keyof typeof SORT] : 'Relevance'}
|
||||||
<svg
|
<svg
|
||||||
className="-mr-1 ml-2 h-5 w-5"
|
className="-mr-1 ml-2 h-5 w-5"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -398,7 +398,7 @@ export default function Search({ categories, brands }: SearchPropsType) {
|
|||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
{SORT.map(([key, text]) => (
|
{Object.entries(SORT).map(([key, text]) => (
|
||||||
<li
|
<li
|
||||||
key={key}
|
key={key}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
@ -35,6 +35,15 @@
|
|||||||
@apply border-accent-9 bg-accent-9 text-accent-0;
|
@apply border-accent-9 bg-accent-9 text-accent-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.naked {
|
||||||
|
@apply bg-transparent font-semibold border-none shadow-none outline-none py-0 px-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.naked:hover,
|
||||||
|
.naked:focus {
|
||||||
|
@apply bg-transparent border-none;
|
||||||
|
}
|
||||||
|
|
||||||
.disabled,
|
.disabled,
|
||||||
.disabled:hover {
|
.disabled:hover {
|
||||||
@apply text-accent-4 border-accent-2 bg-accent-1 cursor-not-allowed;
|
@apply text-accent-4 border-accent-2 bg-accent-1 cursor-not-allowed;
|
||||||
|
@ -12,7 +12,7 @@ import { LoadingDots } from '@components/ui'
|
|||||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
href?: string
|
href?: string
|
||||||
className?: string
|
className?: string
|
||||||
variant?: 'flat' | 'slim' | 'ghost'
|
variant?: 'flat' | 'slim' | 'ghost' | 'naked'
|
||||||
active?: boolean
|
active?: boolean
|
||||||
type?: 'submit' | 'reset' | 'button'
|
type?: 'submit' | 'reset' | 'button'
|
||||||
Component?: string | JSXElementConstructor<any>
|
Component?: string | JSXElementConstructor<any>
|
||||||
@ -41,6 +41,7 @@ const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
|
|||||||
{
|
{
|
||||||
[s.ghost]: variant === 'ghost',
|
[s.ghost]: variant === 'ghost',
|
||||||
[s.slim]: variant === 'slim',
|
[s.slim]: variant === 'slim',
|
||||||
|
[s.naked]: variant === 'naked',
|
||||||
[s.loading]: loading,
|
[s.loading]: loading,
|
||||||
[s.disabled]: disabled,
|
[s.disabled]: disabled,
|
||||||
},
|
},
|
||||||
|
@ -27,13 +27,15 @@ const Modal: FC<ModalProps> = ({ children, onClose }) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current) {
|
const modal = ref.current
|
||||||
disableBodyScroll(ref.current, { reserveScrollBarGap: true })
|
|
||||||
|
if (modal) {
|
||||||
|
disableBodyScroll(modal, { reserveScrollBarGap: true })
|
||||||
window.addEventListener('keydown', handleKey)
|
window.addEventListener('keydown', handleKey)
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
if (ref && ref.current) {
|
if (modal) {
|
||||||
enableBodyScroll(ref.current)
|
enableBodyScroll(modal)
|
||||||
}
|
}
|
||||||
clearAllBodyScrollLocks()
|
clearAllBodyScrollLocks()
|
||||||
window.removeEventListener('keydown', handleKey)
|
window.removeEventListener('keydown', handleKey)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { FC } from 'react'
|
import { FC, memo } from 'react'
|
||||||
import rangeMap from '@lib/range-map'
|
import rangeMap from '@lib/range-map'
|
||||||
import { Star } from '@components/icons'
|
import { Star } from '@components/icons'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
@ -7,21 +7,19 @@ export interface RatingProps {
|
|||||||
value: number
|
value: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const Quantity: React.FC<RatingProps> = React.memo(({ value = 5 }) => {
|
const Quantity: FC<RatingProps> = ({ value = 5 }) => (
|
||||||
return (
|
<div className="flex flex-row py-6 text-accent-9">
|
||||||
<div className="flex flex-row py-6 text-accent-9">
|
{rangeMap(5, (i) => (
|
||||||
{rangeMap(5, (i) => (
|
<span
|
||||||
<span
|
key={`star_${i}`}
|
||||||
key={`star_${i}`}
|
className={cn('inline-block ml-1 ', {
|
||||||
className={cn('inline-block ml-1 ', {
|
'text-accent-5': i >= Math.floor(value),
|
||||||
'text-accent-5': i >= Math.floor(value),
|
})}
|
||||||
})}
|
>
|
||||||
>
|
<Star />
|
||||||
<Star />
|
</span>
|
||||||
</span>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
)
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default Quantity
|
export default memo(Quantity)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply fixed inset-0 h-full z-50 box-border;
|
@apply fixed inset-0 h-full z-50 box-border outline-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
|
@ -13,27 +13,44 @@ interface SidebarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Sidebar: FC<SidebarProps> = ({ children, onClose }) => {
|
const Sidebar: FC<SidebarProps> = ({ children, onClose }) => {
|
||||||
const ref = useRef() as React.MutableRefObject<HTMLDivElement>
|
const sidebarRef = useRef() as React.MutableRefObject<HTMLDivElement>
|
||||||
|
const contentRef = useRef() as React.MutableRefObject<HTMLDivElement>
|
||||||
|
|
||||||
|
const onKeyDownSidebar = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (event.code === 'Escape') {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current) {
|
if (sidebarRef.current) {
|
||||||
disableBodyScroll(ref.current, { reserveScrollBarGap: true })
|
sidebarRef.current.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contentElement = contentRef.current
|
||||||
|
|
||||||
|
if (contentElement) {
|
||||||
|
disableBodyScroll(contentElement, { reserveScrollBarGap: true })
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (ref && ref.current) {
|
if (contentElement) enableBodyScroll(contentElement)
|
||||||
enableBodyScroll(ref.current)
|
|
||||||
}
|
|
||||||
clearAllBodyScrollLocks()
|
clearAllBodyScrollLocks()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(s.root)}>
|
<div
|
||||||
|
className={cn(s.root)}
|
||||||
|
ref={sidebarRef}
|
||||||
|
onKeyDown={onKeyDownSidebar}
|
||||||
|
tabIndex={1}
|
||||||
|
>
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
<div className={s.backdrop} onClick={onClose} />
|
<div className={s.backdrop} onClick={onClose} />
|
||||||
<section className="absolute inset-y-0 right-0 max-w-full flex outline-none pl-10">
|
<section className="absolute inset-y-0 right-0 max-w-full flex outline-none pl-10">
|
||||||
<div className="h-full w-full md:w-screen md:max-w-md">
|
<div className="h-full w-full md:w-screen md:max-w-md">
|
||||||
<div className={s.sidebar} ref={ref}>
|
<div className={s.sidebar} ref={contentRef}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,3 +13,64 @@
|
|||||||
.sectionHeading {
|
.sectionHeading {
|
||||||
@apply pt-1 pb-2 text-2xl font-bold tracking-wide cursor-pointer mb-2;
|
@apply pt-1 pb-2 text-2xl font-bold tracking-wide cursor-pointer mb-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Apply base font sizes and styles for typography markup (h2, h2, ul, p, etc.).
|
||||||
|
A helpful addition for whenn page content is consumed from a source managed through a wysiwyg editor. */
|
||||||
|
|
||||||
|
.body :is(h1, h2, h3, h4, h5, h6, p, ul, ol) {
|
||||||
|
@apply mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body :is(h1, h2, h3, h4, h5, h6):not(:first-child) {
|
||||||
|
@apply mt-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body :is(h1, h2, h3, h4, h5, h6) {
|
||||||
|
@apply font-semibold tracking-wide;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body h1 {
|
||||||
|
@apply text-5xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body h2 {
|
||||||
|
@apply text-4xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body h3 {
|
||||||
|
@apply text-3xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body h4 {
|
||||||
|
@apply text-2xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body h5 {
|
||||||
|
@apply text-xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body h6 {
|
||||||
|
@apply text-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body ul,
|
||||||
|
.body ol {
|
||||||
|
@apply pl-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body ul {
|
||||||
|
@apply list-disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body ol {
|
||||||
|
@apply list-decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body a {
|
||||||
|
@apply underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body a:hover {
|
||||||
|
@apply no-underline;
|
||||||
|
}
|
||||||
|
@ -1,30 +1,58 @@
|
|||||||
import type { CheckoutEndpoint } from '.'
|
import type { CheckoutEndpoint } from '.'
|
||||||
|
import getCustomerId from '../../utils/get-customer-id'
|
||||||
|
import jwt from 'jsonwebtoken'
|
||||||
|
import { uuid } from 'uuidv4'
|
||||||
|
|
||||||
const fullCheckout = true
|
const fullCheckout = true
|
||||||
|
|
||||||
const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({
|
const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
config,
|
config,
|
||||||
}) => {
|
}) => {
|
||||||
const { cookies } = req
|
const { cookies } = req
|
||||||
const cartId = cookies[config.cartCookie]
|
const cartId = cookies[config.cartCookie]
|
||||||
|
const customerToken = cookies[config.customerCookie]
|
||||||
if (!cartId) {
|
if (!cartId) {
|
||||||
res.redirect('/cart')
|
res.redirect('/cart')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data } = await config.storeApiFetch(
|
const { data } = await config.storeApiFetch(
|
||||||
`/v3/carts/${cartId}/redirect_urls`,
|
`/v3/carts/${cartId}/redirect_urls`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
const customerId =
|
||||||
|
customerToken && (await getCustomerId({ customerToken, config }))
|
||||||
|
|
||||||
if (fullCheckout) {
|
//if there is a customer create a jwt token
|
||||||
res.redirect(data.checkout_url)
|
if (!customerId) {
|
||||||
return
|
if (fullCheckout) {
|
||||||
|
res.redirect(data.checkout_url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const dateCreated = Math.round(new Date().getTime() / 1000)
|
||||||
|
const payload = {
|
||||||
|
iss: config.storeApiClientId,
|
||||||
|
iat: dateCreated,
|
||||||
|
jti: uuid(),
|
||||||
|
operation: 'customer_login',
|
||||||
|
store_hash: config.storeHash,
|
||||||
|
customer_id: customerId,
|
||||||
|
channel_id: config.storeChannelId,
|
||||||
|
redirect_to: data.checkout_url,
|
||||||
|
}
|
||||||
|
let token = jwt.sign(payload, config.storeApiClientSecret!, {
|
||||||
|
algorithm: 'HS256',
|
||||||
|
})
|
||||||
|
let checkouturl = `${config.storeUrl}/login/token/${token}`
|
||||||
|
console.log('checkouturl', checkouturl)
|
||||||
|
if (fullCheckout) {
|
||||||
|
res.redirect(checkouturl)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: make the embedded checkout work too!
|
// TODO: make the embedded checkout work too!
|
||||||
@ -59,4 +87,4 @@ const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({
|
|||||||
res.end()
|
res.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
export default checkout
|
export default getCheckout
|
@ -2,13 +2,13 @@ import { GetAPISchema, createEndpoint } from '@commerce/api'
|
|||||||
import checkoutEndpoint from '@commerce/api/endpoints/checkout'
|
import checkoutEndpoint from '@commerce/api/endpoints/checkout'
|
||||||
import type { CheckoutSchema } from '../../../types/checkout'
|
import type { CheckoutSchema } from '../../../types/checkout'
|
||||||
import type { BigcommerceAPI } from '../..'
|
import type { BigcommerceAPI } from '../..'
|
||||||
import checkout from './checkout'
|
import getCheckout from './get-checkout'
|
||||||
|
|
||||||
export type CheckoutAPI = GetAPISchema<BigcommerceAPI, CheckoutSchema>
|
export type CheckoutAPI = GetAPISchema<BigcommerceAPI, CheckoutSchema>
|
||||||
|
|
||||||
export type CheckoutEndpoint = CheckoutAPI['endpoint']
|
export type CheckoutEndpoint = CheckoutAPI['endpoint']
|
||||||
|
|
||||||
export const handlers: CheckoutEndpoint['handlers'] = { checkout }
|
export const handlers: CheckoutEndpoint['handlers'] = { getCheckout }
|
||||||
|
|
||||||
const checkoutApi = createEndpoint<CheckoutAPI>({
|
const checkoutApi = createEndpoint<CheckoutAPI>({
|
||||||
handler: checkoutEndpoint,
|
handler: checkoutEndpoint,
|
||||||
|
1
framework/bigcommerce/api/endpoints/customer/address.ts
Normal file
1
framework/bigcommerce/api/endpoints/customer/address.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
1
framework/bigcommerce/api/endpoints/customer/card.ts
Normal file
1
framework/bigcommerce/api/endpoints/customer/card.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
@ -1,6 +1,6 @@
|
|||||||
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
||||||
import { parseWishlistItem } from '../../utils/parse-item'
|
import { parseWishlistItem } from '../../utils/parse-item'
|
||||||
import getCustomerId from './utils/get-customer-id'
|
import getCustomerId from '../../utils/get-customer-id'
|
||||||
import type { WishlistEndpoint } from '.'
|
import type { WishlistEndpoint } from '.'
|
||||||
|
|
||||||
// Return wishlist info
|
// Return wishlist info
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { Wishlist } from '../../../types/wishlist'
|
import type { Wishlist } from '../../../types/wishlist'
|
||||||
import type { WishlistEndpoint } from '.'
|
import type { WishlistEndpoint } from '.'
|
||||||
import getCustomerId from './utils/get-customer-id'
|
import getCustomerId from '../../utils/get-customer-id'
|
||||||
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
||||||
|
|
||||||
// Return wishlist info
|
// Return wishlist info
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { Wishlist } from '../../../types/wishlist'
|
import type { Wishlist } from '../../../types/wishlist'
|
||||||
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
||||||
import getCustomerId from './utils/get-customer-id'
|
import getCustomerId from '../../utils/get-customer-id'
|
||||||
import type { WishlistEndpoint } from '.'
|
import type { WishlistEndpoint } from '.'
|
||||||
|
|
||||||
// Return wishlist info
|
// Return wishlist info
|
||||||
|
@ -32,6 +32,9 @@ export interface BigcommerceConfig extends CommerceAPIConfig {
|
|||||||
storeApiToken: string
|
storeApiToken: string
|
||||||
storeApiClientId: string
|
storeApiClientId: string
|
||||||
storeChannelId?: string
|
storeChannelId?: string
|
||||||
|
storeUrl?: string
|
||||||
|
storeApiClientSecret?: string
|
||||||
|
storeHash?:string
|
||||||
storeApiFetch<T>(endpoint: string, options?: RequestInit): Promise<T>
|
storeApiFetch<T>(endpoint: string, options?: RequestInit): Promise<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,6 +44,9 @@ const STORE_API_URL = process.env.BIGCOMMERCE_STORE_API_URL
|
|||||||
const STORE_API_TOKEN = process.env.BIGCOMMERCE_STORE_API_TOKEN
|
const STORE_API_TOKEN = process.env.BIGCOMMERCE_STORE_API_TOKEN
|
||||||
const STORE_API_CLIENT_ID = process.env.BIGCOMMERCE_STORE_API_CLIENT_ID
|
const STORE_API_CLIENT_ID = process.env.BIGCOMMERCE_STORE_API_CLIENT_ID
|
||||||
const STORE_CHANNEL_ID = process.env.BIGCOMMERCE_CHANNEL_ID
|
const STORE_CHANNEL_ID = process.env.BIGCOMMERCE_CHANNEL_ID
|
||||||
|
const STORE_URL = process.env.BIGCOMMERCE_STORE_URL
|
||||||
|
const CLIENT_SECRET = process.env.BIGCOMMERCE_STORE_API_CLIENT_SECRET
|
||||||
|
const STOREFRONT_HASH = process.env.BIGCOMMERCE_STORE_API_STORE_HASH
|
||||||
|
|
||||||
if (!API_URL) {
|
if (!API_URL) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -75,6 +81,9 @@ const config: BigcommerceConfig = {
|
|||||||
storeApiToken: STORE_API_TOKEN,
|
storeApiToken: STORE_API_TOKEN,
|
||||||
storeApiClientId: STORE_API_CLIENT_ID,
|
storeApiClientId: STORE_API_CLIENT_ID,
|
||||||
storeChannelId: STORE_CHANNEL_ID,
|
storeChannelId: STORE_CHANNEL_ID,
|
||||||
|
storeUrl:STORE_URL,
|
||||||
|
storeApiClientSecret:CLIENT_SECRET,
|
||||||
|
storeHash: STOREFRONT_HASH,
|
||||||
storeApiFetch: createFetchStoreApi(() => getCommerceApi().getConfig()),
|
storeApiFetch: createFetchStoreApi(() => getCommerceApi().getConfig()),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ export default function getCustomerWishlistOperation({
|
|||||||
|
|
||||||
if (ids?.length) {
|
if (ids?.length) {
|
||||||
const graphqlData = await commerce.getAllProducts({
|
const graphqlData = await commerce.getAllProducts({
|
||||||
variables: { first: 100, ids },
|
variables: { first: 50, ids },
|
||||||
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
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { GetCustomerIdQuery } from '../../../../schema'
|
import type { GetCustomerIdQuery } from '../../schema'
|
||||||
import type { BigcommerceConfig } from '../../..'
|
import type { BigcommerceConfig } from '../'
|
||||||
|
|
||||||
export const getCustomerIdQuery = /* GraphQL */ `
|
export const getCustomerIdQuery = /* GraphQL */ `
|
||||||
query getCustomerId {
|
query getCustomerId {
|
@ -10,7 +10,7 @@ type BCCartItemBody = {
|
|||||||
product_id: number
|
product_id: number
|
||||||
variant_id: number
|
variant_id: number
|
||||||
quantity?: number
|
quantity?: number
|
||||||
option_selections?: OptionSelections
|
option_selections?: OptionSelections[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseWishlistItem = (
|
export const parseWishlistItem = (
|
||||||
|
@ -16,7 +16,7 @@ export const handler: MutationHook<LoginHook> = {
|
|||||||
if (!(email && password)) {
|
if (!(email && password)) {
|
||||||
throw new CommerceError({
|
throw new CommerceError({
|
||||||
message:
|
message:
|
||||||
'A first name, last name, email and password are required to login',
|
'An email and password are required to login',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,4 +41,4 @@ export const handler: MutationHook<AddItemHook> = {
|
|||||||
[fetch, mutate]
|
[fetch, mutate]
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
14
framework/bigcommerce/checkout/use-checkout.tsx
Normal file
14
framework/bigcommerce/checkout/use-checkout.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { SWRHook } from '@commerce/utils/types'
|
||||||
|
import useCheckout, { UseCheckout } from '@commerce/checkout/use-checkout'
|
||||||
|
|
||||||
|
export default useCheckout as UseCheckout<typeof handler>
|
||||||
|
|
||||||
|
export const handler: SWRHook<any> = {
|
||||||
|
fetchOptions: {
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
async fetcher({ input, options, fetch }) {},
|
||||||
|
useHook:
|
||||||
|
({ useData }) =>
|
||||||
|
async (input) => ({}),
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"provider": "bigcommerce",
|
"provider": "bigcommerce",
|
||||||
"features": {
|
"features": {
|
||||||
"wishlist": true
|
"wishlist": true,
|
||||||
|
"customerAuth": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15
framework/bigcommerce/customer/address/use-add-item.tsx
Normal file
15
framework/bigcommerce/customer/address/use-add-item.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import useAddItem, { UseAddItem } from '@commerce/customer/address/use-add-item'
|
||||||
|
import { MutationHook } from '@commerce/utils/types'
|
||||||
|
|
||||||
|
export default useAddItem as UseAddItem<typeof handler>
|
||||||
|
|
||||||
|
export const handler: MutationHook<any> = {
|
||||||
|
fetchOptions: {
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
async fetcher({ input, options, fetch }) {},
|
||||||
|
useHook:
|
||||||
|
({ fetch }) =>
|
||||||
|
() =>
|
||||||
|
async () => ({}),
|
||||||
|
}
|
15
framework/bigcommerce/customer/card/use-add-item.tsx
Normal file
15
framework/bigcommerce/customer/card/use-add-item.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import useAddItem, { UseAddItem } from '@commerce/customer/card/use-add-item'
|
||||||
|
import { MutationHook } from '@commerce/utils/types'
|
||||||
|
|
||||||
|
export default useAddItem as UseAddItem<typeof handler>
|
||||||
|
|
||||||
|
export const handler: MutationHook<any> = {
|
||||||
|
fetchOptions: {
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
async fetcher({ input, options, fetch }) {},
|
||||||
|
useHook:
|
||||||
|
({ fetch }) =>
|
||||||
|
() =>
|
||||||
|
async () => ({}),
|
||||||
|
}
|
@ -1,36 +1,9 @@
|
|||||||
import type { ReactNode } from 'react'
|
import { getCommerceProvider, useCommerce as useCoreCommerce } from '@commerce'
|
||||||
import {
|
import { bigcommerceProvider, BigcommerceProvider } from './provider'
|
||||||
CommerceConfig,
|
|
||||||
CommerceProvider as CoreCommerceProvider,
|
|
||||||
useCommerce as useCoreCommerce,
|
|
||||||
} from '@commerce'
|
|
||||||
import { bigcommerceProvider } from './provider'
|
|
||||||
import type { BigcommerceProvider } from './provider'
|
|
||||||
|
|
||||||
export { bigcommerceProvider }
|
export { bigcommerceProvider }
|
||||||
export type { BigcommerceProvider }
|
export type { BigcommerceProvider }
|
||||||
|
|
||||||
export const bigcommerceConfig: CommerceConfig = {
|
export const CommerceProvider = getCommerceProvider(bigcommerceProvider)
|
||||||
locale: 'en-us',
|
|
||||||
cartCookie: 'bc_cartId',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BigcommerceConfig = Partial<CommerceConfig>
|
|
||||||
|
|
||||||
export type BigcommerceProps = {
|
|
||||||
children?: ReactNode
|
|
||||||
locale: string
|
|
||||||
} & BigcommerceConfig
|
|
||||||
|
|
||||||
export function CommerceProvider({ children, ...config }: BigcommerceProps) {
|
|
||||||
return (
|
|
||||||
<CoreCommerceProvider
|
|
||||||
provider={bigcommerceProvider}
|
|
||||||
config={{ ...bigcommerceConfig, ...config }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</CoreCommerceProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCommerce = () => useCoreCommerce<BigcommerceProvider>()
|
export const useCommerce = () => useCoreCommerce<BigcommerceProvider>()
|
||||||
|
@ -10,7 +10,7 @@ function normalizeProductOption(productOption: any) {
|
|||||||
const {
|
const {
|
||||||
node: {
|
node: {
|
||||||
entityId,
|
entityId,
|
||||||
values: { edges },
|
values: { edges = [] } = {},
|
||||||
...rest
|
...rest
|
||||||
},
|
},
|
||||||
} = productOption
|
} = productOption
|
||||||
@ -91,7 +91,10 @@ export function normalizeCart(data: BigcommerceCart): Cart {
|
|||||||
createdAt: data.created_time,
|
createdAt: data.created_time,
|
||||||
currency: data.currency,
|
currency: data.currency,
|
||||||
taxesIncluded: data.tax_included,
|
taxesIncluded: data.tax_included,
|
||||||
lineItems: data.line_items.physical_items.map(normalizeLineItem),
|
lineItems: [
|
||||||
|
...data.line_items.physical_items.map(normalizeLineItem),
|
||||||
|
...data.line_items.digital_items.map(normalizeLineItem),
|
||||||
|
],
|
||||||
lineItemsSubtotalPrice: data.base_amount,
|
lineItemsSubtotalPrice: data.base_amount,
|
||||||
subtotalPrice: data.base_amount + data.discount_amount,
|
subtotalPrice: data.base_amount + data.discount_amount,
|
||||||
totalPrice: data.cart_amount,
|
totalPrice: data.cart_amount,
|
||||||
|
@ -22,7 +22,7 @@ export const handler: SWRHook<SearchProductsHook> = {
|
|||||||
const url = new URL(options.url!, 'http://a')
|
const url = new URL(options.url!, 'http://a')
|
||||||
|
|
||||||
if (search) url.searchParams.set('search', search)
|
if (search) url.searchParams.set('search', search)
|
||||||
if (Number.isInteger(categoryId))
|
if (Number.isInteger(Number(categoryId)))
|
||||||
url.searchParams.set('categoryId', String(categoryId))
|
url.searchParams.set('categoryId', String(categoryId))
|
||||||
if (Number.isInteger(brandId))
|
if (Number.isInteger(brandId))
|
||||||
url.searchParams.set('brandId', String(brandId))
|
url.searchParams.set('brandId', String(brandId))
|
||||||
|
@ -40,7 +40,7 @@ export type OptionSelections = {
|
|||||||
|
|
||||||
export type CartItemBody = Core.CartItemBody & {
|
export type CartItemBody = Core.CartItemBody & {
|
||||||
productId: string // The product id is always required for BC
|
productId: string // The product id is always required for BC
|
||||||
optionSelections?: OptionSelections
|
optionSelections?: OptionSelections[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CartTypes = {
|
export type CartTypes = {
|
||||||
|
@ -1,25 +1,39 @@
|
|||||||
import type { CheckoutSchema } from '../../types/checkout'
|
import type { CheckoutSchema } from '../../types/checkout'
|
||||||
|
import type { GetAPISchema } from '..'
|
||||||
|
|
||||||
import { CommerceAPIError } from '../utils/errors'
|
import { CommerceAPIError } from '../utils/errors'
|
||||||
import isAllowedOperation from '../utils/is-allowed-operation'
|
import isAllowedOperation from '../utils/is-allowed-operation'
|
||||||
import type { GetAPISchema } from '..'
|
|
||||||
|
|
||||||
const checkoutEndpoint: GetAPISchema<
|
const checkoutEndpoint: GetAPISchema<
|
||||||
any,
|
any,
|
||||||
CheckoutSchema
|
CheckoutSchema
|
||||||
>['endpoint']['handler'] = async (ctx) => {
|
>['endpoint']['handler'] = async (ctx) => {
|
||||||
const { req, res, handlers } = ctx
|
const { req, res, handlers, config } = ctx
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isAllowedOperation(req, res, {
|
!isAllowedOperation(req, res, {
|
||||||
GET: handlers['checkout'],
|
GET: handlers['getCheckout'],
|
||||||
|
POST: handlers['submitCheckout'],
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { cookies } = req
|
||||||
|
const cartId = cookies[config.cartCookie]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = null
|
// Create checkout
|
||||||
return await handlers['checkout']({ ...ctx, body })
|
if (req.method === 'GET') {
|
||||||
|
const body = { ...req.body, cartId }
|
||||||
|
return await handlers['getCheckout']({ ...ctx, body })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create checkout
|
||||||
|
if (req.method === 'POST' && handlers['submitCheckout']) {
|
||||||
|
const body = { ...req.body, cartId }
|
||||||
|
return await handlers['submitCheckout']({ ...ctx, body })
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|
||||||
|
65
framework/commerce/api/endpoints/customer/address.ts
Normal file
65
framework/commerce/api/endpoints/customer/address.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import type { CustomerAddressSchema } from '../../../types/customer/address'
|
||||||
|
import type { GetAPISchema } from '../..'
|
||||||
|
|
||||||
|
import { CommerceAPIError } from '../../utils/errors'
|
||||||
|
import isAllowedOperation from '../../utils/is-allowed-operation'
|
||||||
|
|
||||||
|
const customerShippingEndpoint: GetAPISchema<
|
||||||
|
any,
|
||||||
|
CustomerAddressSchema
|
||||||
|
>['endpoint']['handler'] = async (ctx) => {
|
||||||
|
const { req, res, handlers, config } = ctx
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isAllowedOperation(req, res, {
|
||||||
|
GET: handlers['getAddresses'],
|
||||||
|
POST: handlers['addItem'],
|
||||||
|
PUT: handlers['updateItem'],
|
||||||
|
DELETE: handlers['removeItem'],
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { cookies } = req
|
||||||
|
|
||||||
|
// Cart id might be usefull for anonymous shopping
|
||||||
|
const cartId = cookies[config.cartCookie]
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Return customer addresses
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
const body = { cartId }
|
||||||
|
return await handlers['getAddresses']({ ...ctx, body })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or add an item to customer addresses list
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
const body = { ...req.body, cartId }
|
||||||
|
return await handlers['addItem']({ ...ctx, body })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update item in customer addresses list
|
||||||
|
if (req.method === 'PUT') {
|
||||||
|
const body = { ...req.body, cartId }
|
||||||
|
return await handlers['updateItem']({ ...ctx, body })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove an item from customer addresses list
|
||||||
|
if (req.method === 'DELETE') {
|
||||||
|
const body = { ...req.body, cartId }
|
||||||
|
return await handlers['removeItem']({ ...ctx, body })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
const message =
|
||||||
|
error instanceof CommerceAPIError
|
||||||
|
? 'An unexpected error ocurred with the Commerce API'
|
||||||
|
: 'An unexpected error ocurred'
|
||||||
|
|
||||||
|
res.status(500).json({ data: null, errors: [{ message }] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default customerShippingEndpoint
|
65
framework/commerce/api/endpoints/customer/card.ts
Normal file
65
framework/commerce/api/endpoints/customer/card.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import type { CustomerCardSchema } from '../../../types/customer/card'
|
||||||
|
import type { GetAPISchema } from '../..'
|
||||||
|
|
||||||
|
import { CommerceAPIError } from '../../utils/errors'
|
||||||
|
import isAllowedOperation from '../../utils/is-allowed-operation'
|
||||||
|
|
||||||
|
const customerCardEndpoint: GetAPISchema<
|
||||||
|
any,
|
||||||
|
CustomerCardSchema
|
||||||
|
>['endpoint']['handler'] = async (ctx) => {
|
||||||
|
const { req, res, handlers, config } = ctx
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isAllowedOperation(req, res, {
|
||||||
|
GET: handlers['getCards'],
|
||||||
|
POST: handlers['addItem'],
|
||||||
|
PUT: handlers['updateItem'],
|
||||||
|
DELETE: handlers['removeItem'],
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { cookies } = req
|
||||||
|
|
||||||
|
// Cart id might be usefull for anonymous shopping
|
||||||
|
const cartId = cookies[config.cartCookie]
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create or add a card
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
const body = { ...req.body }
|
||||||
|
return await handlers['getCards']({ ...ctx, body })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or add an item to customer cards
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
const body = { ...req.body, cartId }
|
||||||
|
return await handlers['addItem']({ ...ctx, body })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update item in customer cards
|
||||||
|
if (req.method === 'PUT') {
|
||||||
|
const body = { ...req.body, cartId }
|
||||||
|
return await handlers['updateItem']({ ...ctx, body })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove an item from customer cards
|
||||||
|
if (req.method === 'DELETE') {
|
||||||
|
const body = { ...req.body, cartId }
|
||||||
|
return await handlers['removeItem']({ ...ctx, body })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
const message =
|
||||||
|
error instanceof CommerceAPIError
|
||||||
|
? 'An unexpected error ocurred with the Commerce API'
|
||||||
|
: 'An unexpected error ocurred'
|
||||||
|
|
||||||
|
res.status(500).json({ data: null, errors: [{ message }] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default customerCardEndpoint
|
@ -1,7 +1,8 @@
|
|||||||
import type { CustomerSchema } from '../../types/customer'
|
import type { CustomerSchema } from '../../../types/customer'
|
||||||
import { CommerceAPIError } from '../utils/errors'
|
import type { GetAPISchema } from '../..'
|
||||||
import isAllowedOperation from '../utils/is-allowed-operation'
|
|
||||||
import type { GetAPISchema } from '..'
|
import { CommerceAPIError } from '../../utils/errors'
|
||||||
|
import isAllowedOperation from '../../utils/is-allowed-operation'
|
||||||
|
|
||||||
const customerEndpoint: GetAPISchema<
|
const customerEndpoint: GetAPISchema<
|
||||||
any,
|
any,
|
@ -9,6 +9,8 @@ import type { SignupSchema } from '../types/signup'
|
|||||||
import type { ProductsSchema } from '../types/product'
|
import type { ProductsSchema } from '../types/product'
|
||||||
import type { WishlistSchema } from '../types/wishlist'
|
import type { WishlistSchema } from '../types/wishlist'
|
||||||
import type { CheckoutSchema } from '../types/checkout'
|
import type { CheckoutSchema } from '../types/checkout'
|
||||||
|
import type { CustomerCardSchema } from '../types/customer/card'
|
||||||
|
import type { CustomerAddressSchema } from '../types/customer/address'
|
||||||
import {
|
import {
|
||||||
defaultOperations,
|
defaultOperations,
|
||||||
OPERATIONS,
|
OPERATIONS,
|
||||||
@ -25,6 +27,8 @@ export type APISchemas =
|
|||||||
| ProductsSchema
|
| ProductsSchema
|
||||||
| WishlistSchema
|
| WishlistSchema
|
||||||
| CheckoutSchema
|
| CheckoutSchema
|
||||||
|
| CustomerCardSchema
|
||||||
|
| CustomerAddressSchema
|
||||||
|
|
||||||
export type GetAPISchema<
|
export type GetAPISchema<
|
||||||
C extends CommerceAPI<any>,
|
C extends CommerceAPI<any>,
|
||||||
@ -61,8 +65,8 @@ export type EndpointHandlers<
|
|||||||
[H in keyof E['handlers']]: APIHandler<
|
[H in keyof E['handlers']]: APIHandler<
|
||||||
C,
|
C,
|
||||||
EndpointHandlers<C, E>,
|
EndpointHandlers<C, E>,
|
||||||
E['handlers'][H]['data'],
|
NonNullable<E['handlers'][H]>['data'],
|
||||||
E['handlers'][H]['body'],
|
NonNullable<E['handlers'][H]>['body'],
|
||||||
E['options']
|
E['options']
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
34
framework/commerce/checkout/use-checkout.ts
Normal file
34
framework/commerce/checkout/use-checkout.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import type { SWRHook, HookFetcherFn } from '../utils/types'
|
||||||
|
import type { GetCheckoutHook } from '../types/checkout'
|
||||||
|
|
||||||
|
import Cookies from 'js-cookie'
|
||||||
|
|
||||||
|
import { useHook, useSWRHook } from '../utils/use-hook'
|
||||||
|
import { Provider, useCommerce } from '..'
|
||||||
|
|
||||||
|
export type UseCheckout<
|
||||||
|
H extends SWRHook<GetCheckoutHook<any>> = SWRHook<GetCheckoutHook>
|
||||||
|
> = ReturnType<H['useHook']>
|
||||||
|
|
||||||
|
export const fetcher: HookFetcherFn<GetCheckoutHook> = async ({
|
||||||
|
options,
|
||||||
|
input: { cartId },
|
||||||
|
fetch,
|
||||||
|
}) => {
|
||||||
|
return cartId ? await fetch(options) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn = (provider: Provider) => provider.checkout?.useCheckout!
|
||||||
|
|
||||||
|
const useCheckout: UseCheckout = (input) => {
|
||||||
|
const hook = useHook(fn)
|
||||||
|
const { cartCookie } = useCommerce()
|
||||||
|
const fetcherFn = hook.fetcher ?? fetcher
|
||||||
|
const wrapper: typeof fetcher = (context) => {
|
||||||
|
context.input.cartId = Cookies.get(cartCookie)
|
||||||
|
return fetcherFn(context)
|
||||||
|
}
|
||||||
|
return useSWRHook({ ...hook, fetcher: wrapper })(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useCheckout
|
23
framework/commerce/checkout/use-submit-checkout.tsx
Normal file
23
framework/commerce/checkout/use-submit-checkout.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import type { HookFetcherFn, MutationHook } from '../utils/types'
|
||||||
|
import type { SubmitCheckoutHook } from '../types/checkout'
|
||||||
|
import type { Provider } from '..'
|
||||||
|
|
||||||
|
import { useHook, useMutationHook } from '../utils/use-hook'
|
||||||
|
import { mutationFetcher } from '../utils/default-fetcher'
|
||||||
|
|
||||||
|
export type UseSubmitCheckout<
|
||||||
|
H extends MutationHook<
|
||||||
|
SubmitCheckoutHook<any>
|
||||||
|
> = MutationHook<SubmitCheckoutHook>
|
||||||
|
> = ReturnType<H['useHook']>
|
||||||
|
|
||||||
|
export const fetcher: HookFetcherFn<SubmitCheckoutHook> = mutationFetcher
|
||||||
|
|
||||||
|
const fn = (provider: Provider) => provider.checkout?.useSubmitCheckout!
|
||||||
|
|
||||||
|
const useSubmitCheckout: UseSubmitCheckout = (...args) => {
|
||||||
|
const hook = useHook(fn)
|
||||||
|
return useMutationHook({ fetcher, ...hook })(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useSubmitCheckout
|
@ -8,13 +8,15 @@ const merge = require('deepmerge')
|
|||||||
const prettier = require('prettier')
|
const prettier = require('prettier')
|
||||||
|
|
||||||
const PROVIDERS = [
|
const PROVIDERS = [
|
||||||
|
'local',
|
||||||
'bigcommerce',
|
'bigcommerce',
|
||||||
'saleor',
|
'saleor',
|
||||||
'shopify',
|
'shopify',
|
||||||
'swell',
|
'swell',
|
||||||
'vendure',
|
'vendure',
|
||||||
'local',
|
'local',
|
||||||
'elasticpath'
|
'elasticpath',
|
||||||
|
'ordercloud'
|
||||||
]
|
]
|
||||||
|
|
||||||
function getProviderName() {
|
function getProviderName() {
|
||||||
@ -58,11 +60,27 @@ function withCommerceConfig(nextConfig = {}) {
|
|||||||
|
|
||||||
// Update paths in `tsconfig.json` to point to the selected provider
|
// Update paths in `tsconfig.json` to point to the selected provider
|
||||||
if (config.commerce.updateTSConfig !== false) {
|
if (config.commerce.updateTSConfig !== false) {
|
||||||
const staticTsconfigPath = path.join(process.cwd(), 'tsconfig.json')
|
const tsconfigPath = path.join(process.cwd(), 'tsconfig.json')
|
||||||
const tsconfig = require('../../tsconfig.js')
|
const tsconfig = require(tsconfigPath)
|
||||||
|
|
||||||
|
tsconfig.compilerOptions.paths['@framework'] = [`framework/${name}`]
|
||||||
|
tsconfig.compilerOptions.paths['@framework/*'] = [`framework/${name}/*`]
|
||||||
|
|
||||||
|
// When running for production it may be useful to exclude the other providers
|
||||||
|
// from TS checking
|
||||||
|
if (process.env.VERCEL) {
|
||||||
|
const exclude = tsconfig.exclude.filter(
|
||||||
|
(item) => !item.startsWith('framework/')
|
||||||
|
)
|
||||||
|
|
||||||
|
tsconfig.exclude = PROVIDERS.reduce((exclude, current) => {
|
||||||
|
if (current !== name) exclude.push(`framework/${current}`)
|
||||||
|
return exclude
|
||||||
|
}, exclude)
|
||||||
|
}
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
staticTsconfigPath,
|
tsconfigPath,
|
||||||
prettier.format(JSON.stringify(tsconfig), { parser: 'json' })
|
prettier.format(JSON.stringify(tsconfig), { parser: 'json' })
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
21
framework/commerce/customer/address/use-add-item.tsx
Normal file
21
framework/commerce/customer/address/use-add-item.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { HookFetcherFn, MutationHook } from '../../utils/types'
|
||||||
|
import type { AddItemHook } from '../../types/customer/address'
|
||||||
|
import type { Provider } from '../..'
|
||||||
|
|
||||||
|
import { useHook, useMutationHook } from '../../utils/use-hook'
|
||||||
|
import { mutationFetcher } from '../../utils/default-fetcher'
|
||||||
|
|
||||||
|
export type UseAddItem<
|
||||||
|
H extends MutationHook<AddItemHook<any>> = MutationHook<AddItemHook>
|
||||||
|
> = ReturnType<H['useHook']>
|
||||||
|
|
||||||
|
export const fetcher: HookFetcherFn<AddItemHook> = mutationFetcher
|
||||||
|
|
||||||
|
const fn = (provider: Provider) => provider.customer?.address?.useAddItem!
|
||||||
|
|
||||||
|
const useAddItem: UseAddItem = (...args) => {
|
||||||
|
const hook = useHook(fn)
|
||||||
|
return useMutationHook({ fetcher, ...hook })(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useAddItem
|
34
framework/commerce/customer/address/use-addresses.tsx
Normal file
34
framework/commerce/customer/address/use-addresses.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import type { SWRHook, HookFetcherFn } from '../../utils/types'
|
||||||
|
import type { GetAddressesHook } from '../../types/customer/address'
|
||||||
|
|
||||||
|
import Cookies from 'js-cookie'
|
||||||
|
|
||||||
|
import { useHook, useSWRHook } from '../../utils/use-hook'
|
||||||
|
import { Provider, useCommerce } from '../..'
|
||||||
|
|
||||||
|
export type UseAddresses<
|
||||||
|
H extends SWRHook<GetAddressesHook<any>> = SWRHook<GetAddressesHook>
|
||||||
|
> = ReturnType<H['useHook']>
|
||||||
|
|
||||||
|
export const fetcher: HookFetcherFn<GetAddressesHook> = async ({
|
||||||
|
options,
|
||||||
|
input: { cartId },
|
||||||
|
fetch,
|
||||||
|
}) => {
|
||||||
|
return cartId ? await fetch(options) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn = (provider: Provider) => provider.customer?.address?.useAddresses!
|
||||||
|
|
||||||
|
const useAddresses: UseAddresses = (input) => {
|
||||||
|
const hook = useHook(fn)
|
||||||
|
const { cartCookie } = useCommerce()
|
||||||
|
const fetcherFn = hook.fetcher ?? fetcher
|
||||||
|
const wrapper: typeof fetcher = (context) => {
|
||||||
|
context.input.cartId = Cookies.get(cartCookie)
|
||||||
|
return fetcherFn(context)
|
||||||
|
}
|
||||||
|
return useSWRHook({ ...hook, fetcher: wrapper })(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useAddresses
|
21
framework/commerce/customer/address/use-remove-item.tsx
Normal file
21
framework/commerce/customer/address/use-remove-item.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { HookFetcherFn, MutationHook } from '../../utils/types'
|
||||||
|
import type { RemoveItemHook } from '../../types/customer/address'
|
||||||
|
import type { Provider } from '../..'
|
||||||
|
|
||||||
|
import { useHook, useMutationHook } from '../../utils/use-hook'
|
||||||
|
import { mutationFetcher } from '../../utils/default-fetcher'
|
||||||
|
|
||||||
|
export type UseRemoveItem<
|
||||||
|
H extends MutationHook<RemoveItemHook<any>> = MutationHook<RemoveItemHook>
|
||||||
|
> = ReturnType<H['useHook']>
|
||||||
|
|
||||||
|
export const fetcher: HookFetcherFn<RemoveItemHook> = mutationFetcher
|
||||||
|
|
||||||
|
const fn = (provider: Provider) => provider.customer?.address?.useRemoveItem!
|
||||||
|
|
||||||
|
const useRemoveItem: UseRemoveItem = (input) => {
|
||||||
|
const hook = useHook(fn)
|
||||||
|
return useMutationHook({ fetcher, ...hook })(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useRemoveItem
|
21
framework/commerce/customer/address/use-update-item.tsx
Normal file
21
framework/commerce/customer/address/use-update-item.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { HookFetcherFn, MutationHook } from '../../utils/types'
|
||||||
|
import type { UpdateItemHook } from '../../types/customer/address'
|
||||||
|
import type { Provider } from '../..'
|
||||||
|
|
||||||
|
import { useHook, useMutationHook } from '../../utils/use-hook'
|
||||||
|
import { mutationFetcher } from '../../utils/default-fetcher'
|
||||||
|
|
||||||
|
export type UseUpdateItem<
|
||||||
|
H extends MutationHook<UpdateItemHook<any>> = MutationHook<UpdateItemHook>
|
||||||
|
> = ReturnType<H['useHook']>
|
||||||
|
|
||||||
|
export const fetcher: HookFetcherFn<UpdateItemHook> = mutationFetcher
|
||||||
|
|
||||||
|
const fn = (provider: Provider) => provider.customer?.address?.useUpdateItem!
|
||||||
|
|
||||||
|
const useUpdateItem: UseUpdateItem = (input) => {
|
||||||
|
const hook = useHook(fn)
|
||||||
|
return useMutationHook({ fetcher, ...hook })(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useUpdateItem
|
21
framework/commerce/customer/card/use-add-item.tsx
Normal file
21
framework/commerce/customer/card/use-add-item.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { HookFetcherFn, MutationHook } from '../../utils/types'
|
||||||
|
import type { AddItemHook } from '../../types/customer/card'
|
||||||
|
import type { Provider } from '../..'
|
||||||
|
|
||||||
|
import { useHook, useMutationHook } from '../../utils/use-hook'
|
||||||
|
import { mutationFetcher } from '../../utils/default-fetcher'
|
||||||
|
|
||||||
|
export type UseAddItem<
|
||||||
|
H extends MutationHook<AddItemHook<any>> = MutationHook<AddItemHook>
|
||||||
|
> = ReturnType<H['useHook']>
|
||||||
|
|
||||||
|
export const fetcher: HookFetcherFn<AddItemHook> = mutationFetcher
|
||||||
|
|
||||||
|
const fn = (provider: Provider) => provider.customer?.card?.useAddItem!
|
||||||
|
|
||||||
|
const useAddItem: UseAddItem = (...args) => {
|
||||||
|
const hook = useHook(fn)
|
||||||
|
return useMutationHook({ fetcher, ...hook })(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useAddItem
|
34
framework/commerce/customer/card/use-cards.tsx
Normal file
34
framework/commerce/customer/card/use-cards.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import type { SWRHook, HookFetcherFn } from '../../utils/types'
|
||||||
|
import type { GetCardsHook } from '../../types/customer/card'
|
||||||
|
|
||||||
|
import Cookies from 'js-cookie'
|
||||||
|
|
||||||
|
import { useHook, useSWRHook } from '../../utils/use-hook'
|
||||||
|
import { Provider, useCommerce } from '../..'
|
||||||
|
|
||||||
|
export type UseCards<
|
||||||
|
H extends SWRHook<GetCardsHook<any>> = SWRHook<GetCardsHook>
|
||||||
|
> = ReturnType<H['useHook']>
|
||||||
|
|
||||||
|
export const fetcher: HookFetcherFn<GetCardsHook> = async ({
|
||||||
|
options,
|
||||||
|
input: { cartId },
|
||||||
|
fetch,
|
||||||
|
}) => {
|
||||||
|
return cartId ? await fetch(options) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn = (provider: Provider) => provider.customer?.card?.useCards!
|
||||||
|
|
||||||
|
const useCards: UseCards = (input) => {
|
||||||
|
const hook = useHook(fn)
|
||||||
|
const { cartCookie } = useCommerce()
|
||||||
|
const fetcherFn = hook.fetcher ?? fetcher
|
||||||
|
const wrapper: typeof fetcher = (context) => {
|
||||||
|
context.input.cartId = Cookies.get(cartCookie)
|
||||||
|
return fetcherFn(context)
|
||||||
|
}
|
||||||
|
return useSWRHook({ ...hook, fetcher: wrapper })(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useCards
|
21
framework/commerce/customer/card/use-remove-item.tsx
Normal file
21
framework/commerce/customer/card/use-remove-item.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { HookFetcherFn, MutationHook } from '../../utils/types'
|
||||||
|
import type { RemoveItemHook } from '../../types/customer/card'
|
||||||
|
import type { Provider } from '../..'
|
||||||
|
|
||||||
|
import { useHook, useMutationHook } from '../../utils/use-hook'
|
||||||
|
import { mutationFetcher } from '../../utils/default-fetcher'
|
||||||
|
|
||||||
|
export type UseRemoveItem<
|
||||||
|
H extends MutationHook<RemoveItemHook<any>> = MutationHook<RemoveItemHook>
|
||||||
|
> = ReturnType<H['useHook']>
|
||||||
|
|
||||||
|
export const fetcher: HookFetcherFn<RemoveItemHook> = mutationFetcher
|
||||||
|
|
||||||
|
const fn = (provider: Provider) => provider.customer?.card?.useRemoveItem!
|
||||||
|
|
||||||
|
const useRemoveItem: UseRemoveItem = (input) => {
|
||||||
|
const hook = useHook(fn)
|
||||||
|
return useMutationHook({ fetcher, ...hook })(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useRemoveItem
|
21
framework/commerce/customer/card/use-update-item.tsx
Normal file
21
framework/commerce/customer/card/use-update-item.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { HookFetcherFn, MutationHook } from '../../utils/types'
|
||||||
|
import type { UpdateItemHook } from '../../types/customer/card'
|
||||||
|
import type { Provider } from '../..'
|
||||||
|
|
||||||
|
import { useHook, useMutationHook } from '../../utils/use-hook'
|
||||||
|
import { mutationFetcher } from '../../utils/default-fetcher'
|
||||||
|
|
||||||
|
export type UseUpdateItem<
|
||||||
|
H extends MutationHook<UpdateItemHook<any>> = MutationHook<UpdateItemHook>
|
||||||
|
> = ReturnType<H['useHook']>
|
||||||
|
|
||||||
|
export const fetcher: HookFetcherFn<UpdateItemHook> = mutationFetcher
|
||||||
|
|
||||||
|
const fn = (provider: Provider) => provider?.customer?.card?.useUpdateItem!
|
||||||
|
|
||||||
|
const useUpdateItem: UseUpdateItem = (input) => {
|
||||||
|
const hook = useHook(fn)
|
||||||
|
return useMutationHook({ fetcher, ...hook })(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useUpdateItem
|
@ -15,6 +15,7 @@ import type {
|
|||||||
Signup,
|
Signup,
|
||||||
Login,
|
Login,
|
||||||
Logout,
|
Logout,
|
||||||
|
Checkout,
|
||||||
} from '@commerce/types'
|
} from '@commerce/types'
|
||||||
|
|
||||||
import type { Fetcher, SWRHook, MutationHook } from './utils/types'
|
import type { Fetcher, SWRHook, MutationHook } from './utils/types'
|
||||||
@ -29,6 +30,10 @@ export type Provider = CommerceConfig & {
|
|||||||
useUpdateItem?: MutationHook<Cart.UpdateItemHook>
|
useUpdateItem?: MutationHook<Cart.UpdateItemHook>
|
||||||
useRemoveItem?: MutationHook<Cart.RemoveItemHook>
|
useRemoveItem?: MutationHook<Cart.RemoveItemHook>
|
||||||
}
|
}
|
||||||
|
checkout?: {
|
||||||
|
useCheckout?: SWRHook<Checkout.GetCheckoutHook>
|
||||||
|
useSubmitCheckout?: MutationHook<Checkout.SubmitCheckoutHook>
|
||||||
|
}
|
||||||
wishlist?: {
|
wishlist?: {
|
||||||
useWishlist?: SWRHook<Wishlist.GetWishlistHook>
|
useWishlist?: SWRHook<Wishlist.GetWishlistHook>
|
||||||
useAddItem?: MutationHook<Wishlist.AddItemHook>
|
useAddItem?: MutationHook<Wishlist.AddItemHook>
|
||||||
@ -36,6 +41,18 @@ export type Provider = CommerceConfig & {
|
|||||||
}
|
}
|
||||||
customer?: {
|
customer?: {
|
||||||
useCustomer?: SWRHook<Customer.CustomerHook>
|
useCustomer?: SWRHook<Customer.CustomerHook>
|
||||||
|
card?: {
|
||||||
|
useCards?: SWRHook<Customer.Card.GetCardsHook>
|
||||||
|
useAddItem?: MutationHook<Customer.Card.AddItemHook>
|
||||||
|
useUpdateItem?: MutationHook<Customer.Card.UpdateItemHook>
|
||||||
|
useRemoveItem?: MutationHook<Customer.Card.RemoveItemHook>
|
||||||
|
}
|
||||||
|
address?: {
|
||||||
|
useAddresses?: SWRHook<Customer.Address.GetAddressesHook>
|
||||||
|
useAddItem?: MutationHook<Customer.Address.AddItemHook>
|
||||||
|
useUpdateItem?: MutationHook<Customer.Address.UpdateItemHook>
|
||||||
|
useRemoveItem?: MutationHook<Customer.Address.RemoveItemHook>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
products?: {
|
products?: {
|
||||||
useSearch?: SWRHook<Product.SearchProductsHook>
|
useSearch?: SWRHook<Product.SearchProductsHook>
|
||||||
@ -47,51 +64,60 @@ export type Provider = CommerceConfig & {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CommerceProps<P extends Provider> = {
|
export type CommerceConfig = {
|
||||||
children?: ReactNode
|
|
||||||
provider: P
|
|
||||||
config: CommerceConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CommerceConfig = Omit<
|
|
||||||
CommerceContextValue<any>,
|
|
||||||
'providerRef' | 'fetcherRef'
|
|
||||||
>
|
|
||||||
|
|
||||||
export type CommerceContextValue<P extends Provider> = {
|
|
||||||
providerRef: MutableRefObject<P>
|
|
||||||
fetcherRef: MutableRefObject<Fetcher>
|
|
||||||
locale: string
|
locale: string
|
||||||
cartCookie: string
|
cartCookie: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CommerceProvider<P extends Provider>({
|
export type CommerceContextValue<P extends Provider> = {
|
||||||
|
providerRef: MutableRefObject<P>
|
||||||
|
fetcherRef: MutableRefObject<Fetcher>
|
||||||
|
} & CommerceConfig
|
||||||
|
|
||||||
|
export type CommerceProps<P extends Provider> = {
|
||||||
|
children?: ReactNode
|
||||||
|
provider: P
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These are the properties every provider should allow when implementing
|
||||||
|
* the core commerce provider
|
||||||
|
*/
|
||||||
|
export type CommerceProviderProps = {
|
||||||
|
children?: ReactNode
|
||||||
|
} & Partial<CommerceConfig>
|
||||||
|
|
||||||
|
export function CoreCommerceProvider<P extends Provider>({
|
||||||
provider,
|
provider,
|
||||||
children,
|
children,
|
||||||
config,
|
|
||||||
}: CommerceProps<P>) {
|
}: CommerceProps<P>) {
|
||||||
if (!config) {
|
|
||||||
throw new Error('CommerceProvider requires a valid config object')
|
|
||||||
}
|
|
||||||
|
|
||||||
const providerRef = useRef(provider)
|
const providerRef = useRef(provider)
|
||||||
// TODO: Remove the fetcherRef
|
// TODO: Remove the fetcherRef
|
||||||
const fetcherRef = useRef(provider.fetcher)
|
const fetcherRef = useRef(provider.fetcher)
|
||||||
// Because the config is an object, if the parent re-renders this provider
|
// If the parent re-renders this provider will re-render every
|
||||||
// will re-render every consumer unless we memoize the config
|
// consumer unless we memoize the config
|
||||||
|
const { locale, cartCookie } = providerRef.current
|
||||||
const cfg = useMemo(
|
const cfg = useMemo(
|
||||||
() => ({
|
() => ({ providerRef, fetcherRef, locale, cartCookie }),
|
||||||
providerRef,
|
[locale, cartCookie]
|
||||||
fetcherRef,
|
|
||||||
locale: config.locale,
|
|
||||||
cartCookie: config.cartCookie,
|
|
||||||
}),
|
|
||||||
[config.locale, config.cartCookie]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return <Commerce.Provider value={cfg}>{children}</Commerce.Provider>
|
return <Commerce.Provider value={cfg}>{children}</Commerce.Provider>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCommerceProvider<P extends Provider>(provider: P) {
|
||||||
|
return function CommerceProvider({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: CommerceProviderProps) {
|
||||||
|
return (
|
||||||
|
<CoreCommerceProvider provider={{ ...provider, ...props }}>
|
||||||
|
{children}
|
||||||
|
</CoreCommerceProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function useCommerce<P extends Provider>() {
|
export function useCommerce<P extends Provider>() {
|
||||||
return useContext(Commerce) as CommerceContextValue<P>
|
return useContext(Commerce) as CommerceContextValue<P>
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,12 @@ The app imports from the provider directly instead of the core commerce folder (
|
|||||||
|
|
||||||
The provider folder should only depend on `framework/commerce` and dependencies in the main `package.json`. In the future we'll move the `framework` folder to a package that can be shared easily for multiple apps.
|
The provider folder should only depend on `framework/commerce` and dependencies in the main `package.json`. In the future we'll move the `framework` folder to a package that can be shared easily for multiple apps.
|
||||||
|
|
||||||
|
## Updating the list of known providers
|
||||||
|
|
||||||
|
Open [./config.js](./config.js) and add the provider name to the list in `PROVIDERS`.
|
||||||
|
|
||||||
|
Then, open [/.env.template](/.env.template) and add the provider name in the first line.
|
||||||
|
|
||||||
## Adding the provider hooks
|
## Adding the provider hooks
|
||||||
|
|
||||||
Using BigCommerce as an example. The first thing to do is export a `CommerceProvider` component that includes a `provider` object with all the handlers that can be used for hooks:
|
Using BigCommerce as an example. The first thing to do is export a `CommerceProvider` component that includes a `provider` object with all the handlers that can be used for hooks:
|
||||||
|
@ -1,10 +1,57 @@
|
|||||||
export type CheckoutSchema = {
|
import type { UseSubmitCheckout } from '../checkout/use-submit-checkout'
|
||||||
|
import type { Address } from './customer/address'
|
||||||
|
import type { Card } from './customer/card'
|
||||||
|
|
||||||
|
// Index
|
||||||
|
export type Checkout = any
|
||||||
|
|
||||||
|
export type CheckoutTypes = {
|
||||||
|
card?: Card
|
||||||
|
address?: Address
|
||||||
|
checkout?: Checkout
|
||||||
|
hasPayment?: boolean
|
||||||
|
hasShipping?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SubmitCheckoutHook<T extends CheckoutTypes = CheckoutTypes> = {
|
||||||
|
data: T
|
||||||
|
input?: T
|
||||||
|
fetcherInput: T
|
||||||
|
body: { item: T }
|
||||||
|
actionInput: T
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetCheckoutHook<T extends CheckoutTypes = CheckoutTypes> = {
|
||||||
|
data: T['checkout'] | null
|
||||||
|
input: {}
|
||||||
|
fetcherInput: { cartId?: string }
|
||||||
|
swrState: { isEmpty: boolean }
|
||||||
|
mutations: { submit: UseSubmitCheckout }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CheckoutHooks<T extends CheckoutTypes = CheckoutTypes> = {
|
||||||
|
submitCheckout?: SubmitCheckoutHook<T>
|
||||||
|
getCheckout: GetCheckoutHook<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetCheckoutHandler<T extends CheckoutTypes = CheckoutTypes> =
|
||||||
|
GetCheckoutHook<T> & {
|
||||||
|
body: { cartId: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SubmitCheckoutHandler<T extends CheckoutTypes = CheckoutTypes> =
|
||||||
|
SubmitCheckoutHook<T> & {
|
||||||
|
body: { cartId: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CheckoutHandlers<T extends CheckoutTypes = CheckoutTypes> = {
|
||||||
|
getCheckout: GetCheckoutHandler<T>
|
||||||
|
submitCheckout?: SubmitCheckoutHandler<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CheckoutSchema<T extends CheckoutTypes = CheckoutTypes> = {
|
||||||
endpoint: {
|
endpoint: {
|
||||||
options: {}
|
options: {}
|
||||||
handlers: {
|
handlers: CheckoutHandlers<T>
|
||||||
checkout: {
|
|
||||||
data: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
111
framework/commerce/types/customer/address.ts
Normal file
111
framework/commerce/types/customer/address.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
export interface Address {
|
||||||
|
id: string
|
||||||
|
mask: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddressFields {
|
||||||
|
type: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
company: string
|
||||||
|
streetNumber: string
|
||||||
|
apartments: string
|
||||||
|
zipCode: string
|
||||||
|
city: string
|
||||||
|
country: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomerAddressTypes = {
|
||||||
|
address?: Address
|
||||||
|
fields: AddressFields
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetAddressesHook<
|
||||||
|
T extends CustomerAddressTypes = CustomerAddressTypes
|
||||||
|
> = {
|
||||||
|
data: T['address'][] | null
|
||||||
|
input: {}
|
||||||
|
fetcherInput: { cartId?: string }
|
||||||
|
swrState: { isEmpty: boolean }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AddItemHook<T extends CustomerAddressTypes = CustomerAddressTypes> =
|
||||||
|
{
|
||||||
|
data: T['address']
|
||||||
|
input?: T['fields']
|
||||||
|
fetcherInput: T['fields']
|
||||||
|
body: { item: T['fields'] }
|
||||||
|
actionInput: T['fields']
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateItemHook<
|
||||||
|
T extends CustomerAddressTypes = CustomerAddressTypes
|
||||||
|
> = {
|
||||||
|
data: T['address'] | null
|
||||||
|
input: { item?: T['fields']; wait?: number }
|
||||||
|
fetcherInput: { itemId: string; item: T['fields'] }
|
||||||
|
body: { itemId: string; item: T['fields'] }
|
||||||
|
actionInput: T['fields'] & { id: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RemoveItemHook<
|
||||||
|
T extends CustomerAddressTypes = CustomerAddressTypes
|
||||||
|
> = {
|
||||||
|
data: T['address'] | null
|
||||||
|
input: { item?: T['address'] }
|
||||||
|
fetcherInput: { itemId: string }
|
||||||
|
body: { itemId: string }
|
||||||
|
actionInput: { id: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomerAddressHooks<
|
||||||
|
T extends CustomerAddressTypes = CustomerAddressTypes
|
||||||
|
> = {
|
||||||
|
getAddresses: GetAddressesHook<T>
|
||||||
|
addItem: AddItemHook<T>
|
||||||
|
updateItem: UpdateItemHook<T>
|
||||||
|
removeItem: RemoveItemHook<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AddressHandler<
|
||||||
|
T extends CustomerAddressTypes = CustomerAddressTypes
|
||||||
|
> = GetAddressesHook<T> & {
|
||||||
|
body: { cartId?: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AddItemHandler<
|
||||||
|
T extends CustomerAddressTypes = CustomerAddressTypes
|
||||||
|
> = AddItemHook<T> & {
|
||||||
|
body: { cartId: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateItemHandler<
|
||||||
|
T extends CustomerAddressTypes = CustomerAddressTypes
|
||||||
|
> = UpdateItemHook<T> & {
|
||||||
|
data: T['address']
|
||||||
|
body: { cartId: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RemoveItemHandler<
|
||||||
|
T extends CustomerAddressTypes = CustomerAddressTypes
|
||||||
|
> = RemoveItemHook<T> & {
|
||||||
|
body: { cartId: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomerAddressHandlers<
|
||||||
|
T extends CustomerAddressTypes = CustomerAddressTypes
|
||||||
|
> = {
|
||||||
|
getAddresses: GetAddressesHook<T>
|
||||||
|
addItem: AddItemHandler<T>
|
||||||
|
updateItem: UpdateItemHandler<T>
|
||||||
|
removeItem: RemoveItemHandler<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomerAddressSchema<
|
||||||
|
T extends CustomerAddressTypes = CustomerAddressTypes
|
||||||
|
> = {
|
||||||
|
endpoint: {
|
||||||
|
options: {}
|
||||||
|
handlers: CustomerAddressHandlers<T>
|
||||||
|
}
|
||||||
|
}
|
102
framework/commerce/types/customer/card.ts
Normal file
102
framework/commerce/types/customer/card.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
export interface Card {
|
||||||
|
id: string
|
||||||
|
mask: string
|
||||||
|
provider: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardFields {
|
||||||
|
cardHolder: string
|
||||||
|
cardNumber: string
|
||||||
|
cardExpireDate: string
|
||||||
|
cardCvc: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
company: string
|
||||||
|
streetNumber: string
|
||||||
|
zipCode: string
|
||||||
|
city: string
|
||||||
|
country: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomerCardTypes = {
|
||||||
|
card?: Card
|
||||||
|
fields: CardFields
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetCardsHook<T extends CustomerCardTypes = CustomerCardTypes> = {
|
||||||
|
data: T['card'][] | null
|
||||||
|
input: {}
|
||||||
|
fetcherInput: { cartId?: string }
|
||||||
|
swrState: { isEmpty: boolean }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AddItemHook<T extends CustomerCardTypes = CustomerCardTypes> = {
|
||||||
|
data: T['card']
|
||||||
|
input?: T['fields']
|
||||||
|
fetcherInput: T['fields']
|
||||||
|
body: { item: T['fields'] }
|
||||||
|
actionInput: T['fields']
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateItemHook<T extends CustomerCardTypes = CustomerCardTypes> = {
|
||||||
|
data: T['card'] | null
|
||||||
|
input: { item?: T['fields']; wait?: number }
|
||||||
|
fetcherInput: { itemId: string; item: T['fields'] }
|
||||||
|
body: { itemId: string; item: T['fields'] }
|
||||||
|
actionInput: T['fields'] & { id: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RemoveItemHook<T extends CustomerCardTypes = CustomerCardTypes> = {
|
||||||
|
data: T['card'] | null
|
||||||
|
input: { item?: T['card'] }
|
||||||
|
fetcherInput: { itemId: string }
|
||||||
|
body: { itemId: string }
|
||||||
|
actionInput: { id: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomerCardHooks<T extends CustomerCardTypes = CustomerCardTypes> =
|
||||||
|
{
|
||||||
|
getCards: GetCardsHook<T>
|
||||||
|
addItem: AddItemHook<T>
|
||||||
|
updateItem: UpdateItemHook<T>
|
||||||
|
removeItem: RemoveItemHook<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CardsHandler<T extends CustomerCardTypes = CustomerCardTypes> =
|
||||||
|
GetCardsHook<T> & {
|
||||||
|
body: { cartId?: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AddItemHandler<T extends CustomerCardTypes = CustomerCardTypes> =
|
||||||
|
AddItemHook<T> & {
|
||||||
|
body: { cartId: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateItemHandler<T extends CustomerCardTypes = CustomerCardTypes> =
|
||||||
|
UpdateItemHook<T> & {
|
||||||
|
data: T['card']
|
||||||
|
body: { cartId: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RemoveItemHandler<T extends CustomerCardTypes = CustomerCardTypes> =
|
||||||
|
RemoveItemHook<T> & {
|
||||||
|
body: { cartId: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomerCardHandlers<
|
||||||
|
T extends CustomerCardTypes = CustomerCardTypes
|
||||||
|
> = {
|
||||||
|
getCards: GetCardsHook<T>
|
||||||
|
addItem: AddItemHandler<T>
|
||||||
|
updateItem: UpdateItemHandler<T>
|
||||||
|
removeItem: RemoveItemHandler<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomerCardSchema<
|
||||||
|
T extends CustomerCardTypes = CustomerCardTypes
|
||||||
|
> = {
|
||||||
|
endpoint: {
|
||||||
|
options: {}
|
||||||
|
handlers: CustomerCardHandlers<T>
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,6 @@
|
|||||||
|
export * as Card from "./card"
|
||||||
|
export * as Address from "./address"
|
||||||
|
|
||||||
// TODO: define this type
|
// TODO: define this type
|
||||||
export type Customer = any
|
export type Customer = any
|
||||||
|
|
@ -87,6 +87,8 @@ export type HookSchemaBase = {
|
|||||||
export type SWRHookSchemaBase = HookSchemaBase & {
|
export type SWRHookSchemaBase = HookSchemaBase & {
|
||||||
// Custom state added to the response object of SWR
|
// Custom state added to the response object of SWR
|
||||||
swrState?: {}
|
swrState?: {}
|
||||||
|
// Instances of MutationSchemaBase that the hook returns for better DX
|
||||||
|
mutations?: Record<string, ReturnType<MutationHook<any>['useHook']>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationSchemaBase = HookSchemaBase & {
|
export type MutationSchemaBase = HookSchemaBase & {
|
||||||
@ -102,7 +104,7 @@ export type SWRHook<H extends SWRHookSchemaBase> = {
|
|||||||
context: SWRHookContext<H>
|
context: SWRHookContext<H>
|
||||||
): HookFunction<
|
): HookFunction<
|
||||||
H['input'] & { swrOptions?: SwrOptions<H['data'], H['fetcherInput']> },
|
H['input'] & { swrOptions?: SwrOptions<H['data'], H['fetcherInput']> },
|
||||||
ResponseState<H['data']> & H['swrState']
|
ResponseState<H['data']> & H['swrState'] & H['mutations']
|
||||||
>
|
>
|
||||||
fetchOptions: HookFetcherOptions
|
fetchOptions: HookFetcherOptions
|
||||||
fetcher?: HookFetcherFn<H>
|
fetcher?: HookFetcherFn<H>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import useSWR, { responseInterface } from 'swr'
|
import useSWR, { SWRResponse } from 'swr'
|
||||||
import type {
|
import type {
|
||||||
HookSWRInput,
|
HookSWRInput,
|
||||||
HookFetchInput,
|
HookFetchInput,
|
||||||
@ -11,7 +11,7 @@ import type {
|
|||||||
import defineProperty from './define-property'
|
import defineProperty from './define-property'
|
||||||
import { CommerceError } from './errors'
|
import { CommerceError } from './errors'
|
||||||
|
|
||||||
export type ResponseState<Result> = responseInterface<Result, CommerceError> & {
|
export type ResponseState<Result> = SWRResponse<Result, CommerceError> & {
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ const useData: UseData = (options, input, fetcherFn, swrOptions) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return response as typeof response & { isLoading: boolean }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useData
|
export default useData
|
||||||
|
1
framework/local/api/endpoints/customer/address.ts
Normal file
1
framework/local/api/endpoints/customer/address.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
1
framework/local/api/endpoints/customer/card.ts
Normal file
1
framework/local/api/endpoints/customer/card.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
14
framework/local/checkout/use-checkout.tsx
Normal file
14
framework/local/checkout/use-checkout.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { SWRHook } from '@commerce/utils/types'
|
||||||
|
import useCheckout, { UseCheckout } from '@commerce/checkout/use-checkout'
|
||||||
|
|
||||||
|
export default useCheckout as UseCheckout<typeof handler>
|
||||||
|
|
||||||
|
export const handler: SWRHook<any> = {
|
||||||
|
fetchOptions: {
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
async fetcher({ input, options, fetch }) {},
|
||||||
|
useHook:
|
||||||
|
({ useData }) =>
|
||||||
|
async (input) => ({}),
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
{
|
{
|
||||||
"provider": "local",
|
"provider": "local",
|
||||||
"features": {
|
"features": {
|
||||||
"wishlist": false
|
"wishlist": false,
|
||||||
|
"cart": false,
|
||||||
|
"search": false,
|
||||||
|
"customerAuth": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15
framework/local/customer/address/use-add-item.tsx
Normal file
15
framework/local/customer/address/use-add-item.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import useAddItem, { UseAddItem } from '@commerce/customer/address/use-add-item'
|
||||||
|
import { MutationHook } from '@commerce/utils/types'
|
||||||
|
|
||||||
|
export default useAddItem as UseAddItem<typeof handler>
|
||||||
|
|
||||||
|
export const handler: MutationHook<any> = {
|
||||||
|
fetchOptions: {
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
async fetcher({ input, options, fetch }) {},
|
||||||
|
useHook:
|
||||||
|
({ fetch }) =>
|
||||||
|
() =>
|
||||||
|
async () => ({}),
|
||||||
|
}
|
15
framework/local/customer/card/use-add-item.tsx
Normal file
15
framework/local/customer/card/use-add-item.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import useAddItem, { UseAddItem } from '@commerce/customer/card/use-add-item'
|
||||||
|
import { MutationHook } from '@commerce/utils/types'
|
||||||
|
|
||||||
|
export default useAddItem as UseAddItem<typeof handler>
|
||||||
|
|
||||||
|
export const handler: MutationHook<any> = {
|
||||||
|
fetchOptions: {
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
async fetcher({ input, options, fetch }) {},
|
||||||
|
useHook:
|
||||||
|
({ fetch }) =>
|
||||||
|
() =>
|
||||||
|
async () => ({}),
|
||||||
|
}
|
@ -1,32 +1,9 @@
|
|||||||
import * as React from 'react'
|
import { getCommerceProvider, useCommerce as useCoreCommerce } from '@commerce'
|
||||||
import { ReactNode } from 'react'
|
import { localProvider, LocalProvider } from './provider'
|
||||||
import { localProvider } from './provider'
|
|
||||||
import {
|
|
||||||
CommerceConfig,
|
|
||||||
CommerceProvider as CoreCommerceProvider,
|
|
||||||
useCommerce as useCoreCommerce,
|
|
||||||
} from '@commerce'
|
|
||||||
|
|
||||||
export const localConfig: CommerceConfig = {
|
export { localProvider }
|
||||||
locale: 'en-us',
|
export type { LocalProvider }
|
||||||
cartCookie: 'session',
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CommerceProvider({
|
export const CommerceProvider = getCommerceProvider(localProvider)
|
||||||
children,
|
|
||||||
...config
|
|
||||||
}: {
|
|
||||||
children?: ReactNode
|
|
||||||
locale: string
|
|
||||||
} & Partial<CommerceConfig>) {
|
|
||||||
return (
|
|
||||||
<CoreCommerceProvider
|
|
||||||
provider={localProvider}
|
|
||||||
config={{ ...localConfig, ...config }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</CoreCommerceProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCommerce = () => useCoreCommerce()
|
export const useCommerce = () => useCoreCommerce<LocalProvider>()
|
||||||
|
@ -9,7 +9,6 @@ import { handler as useLogin } from './auth/use-login'
|
|||||||
import { handler as useLogout } from './auth/use-logout'
|
import { handler as useLogout } from './auth/use-logout'
|
||||||
import { handler as useSignup } from './auth/use-signup'
|
import { handler as useSignup } from './auth/use-signup'
|
||||||
|
|
||||||
export type Provider = typeof localProvider
|
|
||||||
export const localProvider = {
|
export const localProvider = {
|
||||||
locale: 'en-us',
|
locale: 'en-us',
|
||||||
cartCookie: 'session',
|
cartCookie: 'session',
|
||||||
@ -19,3 +18,5 @@ export const localProvider = {
|
|||||||
products: { useSearch },
|
products: { useSearch },
|
||||||
auth: { useLogin, useLogout, useSignup },
|
auth: { useLogin, useLogout, useSignup },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LocalProvider = typeof localProvider
|
||||||
|
5
framework/ordercloud/.env.template
Normal file
5
framework/ordercloud/.env.template
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
COMMERCE_PROVIDER=ordercloud
|
||||||
|
|
||||||
|
ORDERCLOUD_CLIENT_ID=
|
||||||
|
ORDERCLOUD_CLIENT_SECRET=
|
||||||
|
STRIPE_SECRET=
|
3
framework/ordercloud/README.md
Normal file
3
framework/ordercloud/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Next.js Ordercloud Provider
|
||||||
|
|
||||||
|
Create your own store from [here](https://nextjs.org/commerce)
|
99
framework/ordercloud/api/endpoints/cart/add-item.ts
Normal file
99
framework/ordercloud/api/endpoints/cart/add-item.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import type { CartEndpoint } from '.'
|
||||||
|
import type { RawVariant } from '../../../types/product'
|
||||||
|
import type { OrdercloudLineItem } from '../../../types/cart'
|
||||||
|
|
||||||
|
import { serialize } from 'cookie'
|
||||||
|
|
||||||
|
import { formatCart } from '../../utils/cart'
|
||||||
|
|
||||||
|
const addItem: CartEndpoint['handlers']['addItem'] = async ({
|
||||||
|
res,
|
||||||
|
body: { cartId, item },
|
||||||
|
config: { restBuyerFetch, cartCookie, tokenCookie },
|
||||||
|
}) => {
|
||||||
|
// Return an error if no item is present
|
||||||
|
if (!item) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Missing item' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store token
|
||||||
|
let token
|
||||||
|
|
||||||
|
// Set the quantity if not present
|
||||||
|
if (!item.quantity) item.quantity = 1
|
||||||
|
|
||||||
|
// Create an order if it doesn't exist
|
||||||
|
if (!cartId) {
|
||||||
|
const { ID, meta } = await restBuyerFetch(
|
||||||
|
'POST',
|
||||||
|
`/orders/Outgoing`,
|
||||||
|
{}
|
||||||
|
).then((response: { ID: string; meta: { token: string } }) => response)
|
||||||
|
|
||||||
|
// Set the cart id and token
|
||||||
|
cartId = ID
|
||||||
|
token = meta.token
|
||||||
|
|
||||||
|
// Set the cart and token cookie
|
||||||
|
res.setHeader('Set-Cookie', [
|
||||||
|
serialize(tokenCookie, meta.token, {
|
||||||
|
maxAge: 60 * 60 * 24 * 30,
|
||||||
|
expires: new Date(Date.now() + 60 * 60 * 24 * 30 * 1000),
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
path: '/',
|
||||||
|
sameSite: 'lax',
|
||||||
|
}),
|
||||||
|
serialize(cartCookie, cartId, {
|
||||||
|
maxAge: 60 * 60 * 24 * 30,
|
||||||
|
expires: new Date(Date.now() + 60 * 60 * 24 * 30 * 1000),
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
path: '/',
|
||||||
|
sameSite: 'lax',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store specs
|
||||||
|
let specs: RawVariant['Specs'] = []
|
||||||
|
|
||||||
|
// If a variant is present, fetch its specs
|
||||||
|
if (item.variantId) {
|
||||||
|
specs = await restBuyerFetch(
|
||||||
|
'GET',
|
||||||
|
`/me/products/${item.productId}/variants/${item.variantId}`,
|
||||||
|
null,
|
||||||
|
{ token }
|
||||||
|
).then((res: RawVariant) => res.Specs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the item to the order
|
||||||
|
await restBuyerFetch(
|
||||||
|
'POST',
|
||||||
|
`/orders/Outgoing/${cartId}/lineitems`,
|
||||||
|
{
|
||||||
|
ProductID: item.productId,
|
||||||
|
Quantity: item.quantity,
|
||||||
|
Specs: specs,
|
||||||
|
},
|
||||||
|
{ token }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get cart
|
||||||
|
const [cart, lineItems] = await Promise.all([
|
||||||
|
restBuyerFetch('GET', `/orders/Outgoing/${cartId}`, null, { token }),
|
||||||
|
restBuyerFetch('GET', `/orders/Outgoing/${cartId}/lineitems`, null, {
|
||||||
|
token,
|
||||||
|
}).then((response: { Items: OrdercloudLineItem[] }) => response.Items),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Format cart
|
||||||
|
const formattedCart = formatCart(cart, lineItems)
|
||||||
|
|
||||||
|
// Return cart and errors
|
||||||
|
res.status(200).json({ data: formattedCart, errors: [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default addItem
|
65
framework/ordercloud/api/endpoints/cart/get-cart.ts
Normal file
65
framework/ordercloud/api/endpoints/cart/get-cart.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import type { OrdercloudLineItem } from '../../../types/cart'
|
||||||
|
import type { CartEndpoint } from '.'
|
||||||
|
|
||||||
|
import { serialize } from 'cookie'
|
||||||
|
|
||||||
|
import { formatCart } from '../../utils/cart'
|
||||||
|
|
||||||
|
// Return current cart info
|
||||||
|
const getCart: CartEndpoint['handlers']['getCart'] = async ({
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
body: { cartId },
|
||||||
|
config: { restBuyerFetch, cartCookie, tokenCookie },
|
||||||
|
}) => {
|
||||||
|
if (!cartId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Invalid request' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get token from cookies
|
||||||
|
const token = req.cookies[tokenCookie]
|
||||||
|
|
||||||
|
// Get cart
|
||||||
|
const cart = await restBuyerFetch(
|
||||||
|
'GET',
|
||||||
|
`/orders/Outgoing/${cartId}`,
|
||||||
|
null,
|
||||||
|
{ token }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get line items
|
||||||
|
const lineItems = await restBuyerFetch(
|
||||||
|
'GET',
|
||||||
|
`/orders/Outgoing/${cartId}/lineitems`,
|
||||||
|
null,
|
||||||
|
{ token }
|
||||||
|
).then((response: { Items: OrdercloudLineItem[] }) => response.Items)
|
||||||
|
|
||||||
|
// Format cart
|
||||||
|
const formattedCart = formatCart(cart, lineItems)
|
||||||
|
|
||||||
|
// Return cart and errors
|
||||||
|
res.status(200).json({ data: formattedCart, errors: [] })
|
||||||
|
} catch (error) {
|
||||||
|
// Reset cart and token cookie
|
||||||
|
res.setHeader('Set-Cookie', [
|
||||||
|
serialize(cartCookie, cartId, {
|
||||||
|
maxAge: -1,
|
||||||
|
path: '/',
|
||||||
|
}),
|
||||||
|
serialize(tokenCookie, cartId, {
|
||||||
|
maxAge: -1,
|
||||||
|
path: '/',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Return empty cart
|
||||||
|
res.status(200).json({ data: null, errors: [] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getCart
|
28
framework/ordercloud/api/endpoints/cart/index.ts
Normal file
28
framework/ordercloud/api/endpoints/cart/index.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import type { CartSchema } from '../../../types/cart'
|
||||||
|
import type { OrdercloudAPI } from '../..'
|
||||||
|
|
||||||
|
import { GetAPISchema, createEndpoint } from '@commerce/api'
|
||||||
|
import cartEndpoint from '@commerce/api/endpoints/cart'
|
||||||
|
|
||||||
|
import getCart from './get-cart'
|
||||||
|
import addItem from './add-item'
|
||||||
|
import updateItem from './update-item'
|
||||||
|
import removeItem from './remove-item'
|
||||||
|
|
||||||
|
export type CartAPI = GetAPISchema<OrdercloudAPI, CartSchema>
|
||||||
|
|
||||||
|
export type CartEndpoint = CartAPI['endpoint']
|
||||||
|
|
||||||
|
export const handlers: CartEndpoint['handlers'] = {
|
||||||
|
getCart,
|
||||||
|
addItem,
|
||||||
|
updateItem,
|
||||||
|
removeItem,
|
||||||
|
}
|
||||||
|
|
||||||
|
const cartApi = createEndpoint<CartAPI>({
|
||||||
|
handler: cartEndpoint,
|
||||||
|
handlers,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default cartApi
|
45
framework/ordercloud/api/endpoints/cart/remove-item.ts
Normal file
45
framework/ordercloud/api/endpoints/cart/remove-item.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import type { CartEndpoint } from '.'
|
||||||
|
|
||||||
|
import { formatCart } from '../../utils/cart'
|
||||||
|
import { OrdercloudLineItem } from '../../../types/cart'
|
||||||
|
|
||||||
|
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
body: { cartId, itemId },
|
||||||
|
config: { restBuyerFetch, tokenCookie },
|
||||||
|
}) => {
|
||||||
|
if (!cartId || !itemId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Invalid request' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get token from cookies
|
||||||
|
const token = req.cookies[tokenCookie]
|
||||||
|
|
||||||
|
// Remove the item to the order
|
||||||
|
await restBuyerFetch(
|
||||||
|
'DELETE',
|
||||||
|
`/orders/Outgoing/${cartId}/lineitems/${itemId}`,
|
||||||
|
null,
|
||||||
|
{ token }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get cart
|
||||||
|
const [cart, lineItems] = await Promise.all([
|
||||||
|
restBuyerFetch('GET', `/orders/Outgoing/${cartId}`, null, { token }),
|
||||||
|
restBuyerFetch('GET', `/orders/Outgoing/${cartId}/lineitems`, null, {
|
||||||
|
token,
|
||||||
|
}).then((response: { Items: OrdercloudLineItem[] }) => response.Items),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Format cart
|
||||||
|
const formattedCart = formatCart(cart, lineItems)
|
||||||
|
|
||||||
|
// Return cart and errors
|
||||||
|
res.status(200).json({ data: formattedCart, errors: [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default removeItem
|
63
framework/ordercloud/api/endpoints/cart/update-item.ts
Normal file
63
framework/ordercloud/api/endpoints/cart/update-item.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import type { OrdercloudLineItem } from '../../../types/cart'
|
||||||
|
import type { RawVariant } from '../../../types/product'
|
||||||
|
import type { CartEndpoint } from '.'
|
||||||
|
|
||||||
|
import { formatCart } from '../../utils/cart'
|
||||||
|
|
||||||
|
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
body: { cartId, itemId, item },
|
||||||
|
config: { restBuyerFetch, tokenCookie },
|
||||||
|
}) => {
|
||||||
|
if (!cartId || !itemId || !item) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Invalid request' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get token from cookies
|
||||||
|
const token = req.cookies[tokenCookie]
|
||||||
|
|
||||||
|
// Store specs
|
||||||
|
let specs: RawVariant['Specs'] = []
|
||||||
|
|
||||||
|
// If a variant is present, fetch its specs
|
||||||
|
if (item.variantId) {
|
||||||
|
specs = await restBuyerFetch(
|
||||||
|
'GET',
|
||||||
|
`/me/products/${item.productId}/variants/${item.variantId}`,
|
||||||
|
null,
|
||||||
|
{ token }
|
||||||
|
).then((res: RawVariant) => res.Specs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the item to the order
|
||||||
|
await restBuyerFetch(
|
||||||
|
'PATCH',
|
||||||
|
`/orders/Outgoing/${cartId}/lineitems/${itemId}`,
|
||||||
|
{
|
||||||
|
ProductID: item.productId,
|
||||||
|
Quantity: item.quantity,
|
||||||
|
Specs: specs,
|
||||||
|
},
|
||||||
|
{ token }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get cart
|
||||||
|
const [cart, lineItems] = await Promise.all([
|
||||||
|
restBuyerFetch('GET', `/orders/Outgoing/${cartId}`, null, { token }),
|
||||||
|
restBuyerFetch('GET', `/orders/Outgoing/${cartId}/lineitems`, null, {
|
||||||
|
token,
|
||||||
|
}).then((response: { Items: OrdercloudLineItem[] }) => response.Items),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Format cart
|
||||||
|
const formattedCart = formatCart(cart, lineItems)
|
||||||
|
|
||||||
|
// Return cart and errors
|
||||||
|
res.status(200).json({ data: formattedCart, errors: [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default updateItem
|
1
framework/ordercloud/api/endpoints/catalog/index.ts
Normal file
1
framework/ordercloud/api/endpoints/catalog/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
1
framework/ordercloud/api/endpoints/catalog/products.ts
Normal file
1
framework/ordercloud/api/endpoints/catalog/products.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
47
framework/ordercloud/api/endpoints/checkout/get-checkout.ts
Normal file
47
framework/ordercloud/api/endpoints/checkout/get-checkout.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import type { CheckoutEndpoint } from '.'
|
||||||
|
|
||||||
|
const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
body: { cartId },
|
||||||
|
config: { restBuyerFetch, tokenCookie },
|
||||||
|
}) => {
|
||||||
|
// Return an error if no item is present
|
||||||
|
if (!cartId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Missing cookie' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get token from cookies
|
||||||
|
const token = req.cookies[tokenCookie]
|
||||||
|
|
||||||
|
// Register credit card
|
||||||
|
const payments = await restBuyerFetch(
|
||||||
|
'GET',
|
||||||
|
`/orders/Outgoing/${cartId}/payments`,
|
||||||
|
null,
|
||||||
|
{ token }
|
||||||
|
).then((response: { Items: unknown[] }) => response.Items)
|
||||||
|
|
||||||
|
const address = await restBuyerFetch(
|
||||||
|
'GET',
|
||||||
|
`/orders/Outgoing/${cartId}`,
|
||||||
|
null,
|
||||||
|
{ token }
|
||||||
|
).then(
|
||||||
|
(response: { ShippingAddressID: string }) => response.ShippingAddressID
|
||||||
|
)
|
||||||
|
|
||||||
|
// Return cart and errors
|
||||||
|
res.status(200).json({
|
||||||
|
data: {
|
||||||
|
hasPayment: payments.length > 0,
|
||||||
|
hasShipping: Boolean(address),
|
||||||
|
},
|
||||||
|
errors: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getCheckout
|
23
framework/ordercloud/api/endpoints/checkout/index.ts
Normal file
23
framework/ordercloud/api/endpoints/checkout/index.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import type { CheckoutSchema } from '../../../types/checkout'
|
||||||
|
import type { OrdercloudAPI } from '../..'
|
||||||
|
|
||||||
|
import { GetAPISchema, createEndpoint } from '@commerce/api'
|
||||||
|
import checkoutEndpoint from '@commerce/api/endpoints/checkout'
|
||||||
|
|
||||||
|
import getCheckout from './get-checkout'
|
||||||
|
import submitCheckout from './submit-checkout'
|
||||||
|
|
||||||
|
export type CheckoutAPI = GetAPISchema<OrdercloudAPI, CheckoutSchema>
|
||||||
|
export type CheckoutEndpoint = CheckoutAPI['endpoint']
|
||||||
|
|
||||||
|
export const handlers: CheckoutEndpoint['handlers'] = {
|
||||||
|
getCheckout,
|
||||||
|
submitCheckout,
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkoutApi = createEndpoint<CheckoutAPI>({
|
||||||
|
handler: checkoutEndpoint,
|
||||||
|
handlers,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default checkoutApi
|
@ -0,0 +1,32 @@
|
|||||||
|
import type { CheckoutEndpoint } from '.'
|
||||||
|
|
||||||
|
const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
body: { cartId },
|
||||||
|
config: { restBuyerFetch, tokenCookie },
|
||||||
|
}) => {
|
||||||
|
// Return an error if no item is present
|
||||||
|
if (!cartId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Missing item' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get token from cookies
|
||||||
|
const token = req.cookies[tokenCookie]
|
||||||
|
|
||||||
|
// Submit order
|
||||||
|
await restBuyerFetch(
|
||||||
|
'POST',
|
||||||
|
`/orders/Outgoing/${cartId}/submit`,
|
||||||
|
{},
|
||||||
|
{ token }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Return cart and errors
|
||||||
|
res.status(200).json({ data: null, errors: [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default submitCheckout
|
@ -0,0 +1,47 @@
|
|||||||
|
import type { CustomerAddressEndpoint } from '.'
|
||||||
|
|
||||||
|
const addItem: CustomerAddressEndpoint['handlers']['addItem'] = async ({
|
||||||
|
res,
|
||||||
|
body: { item, cartId },
|
||||||
|
config: { restBuyerFetch },
|
||||||
|
}) => {
|
||||||
|
// Return an error if no item is present
|
||||||
|
if (!item) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Missing item' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return an error if no item is present
|
||||||
|
if (!cartId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Cookie not found' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register address
|
||||||
|
const address = await restBuyerFetch('POST', `/me/addresses`, {
|
||||||
|
AddressName: 'main address',
|
||||||
|
CompanyName: item.company,
|
||||||
|
FirstName: item.firstName,
|
||||||
|
LastName: item.lastName,
|
||||||
|
Street1: item.streetNumber,
|
||||||
|
Street2: item.streetNumber,
|
||||||
|
City: item.city,
|
||||||
|
State: item.city,
|
||||||
|
Zip: item.zipCode,
|
||||||
|
Country: item.country.slice(0, 2).toLowerCase(),
|
||||||
|
Shipping: true,
|
||||||
|
}).then((response: { ID: string }) => response.ID)
|
||||||
|
|
||||||
|
// Assign address to order
|
||||||
|
await restBuyerFetch('PATCH', `/orders/Outgoing/${cartId}`, {
|
||||||
|
ShippingAddressID: address,
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.status(200).json({ data: null, errors: [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default addItem
|
@ -0,0 +1,9 @@
|
|||||||
|
import type { CustomerAddressEndpoint } from '.'
|
||||||
|
|
||||||
|
const getCards: CustomerAddressEndpoint['handlers']['getAddresses'] = async ({
|
||||||
|
res,
|
||||||
|
}) => {
|
||||||
|
return res.status(200).json({ data: null, errors: [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getCards
|
27
framework/ordercloud/api/endpoints/customer/address/index.ts
Normal file
27
framework/ordercloud/api/endpoints/customer/address/index.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import type { CustomerAddressSchema } from '../../../../types/customer/address'
|
||||||
|
import type { OrdercloudAPI } from '../../..'
|
||||||
|
|
||||||
|
import { GetAPISchema, createEndpoint } from '@commerce/api'
|
||||||
|
import customerAddressEndpoint from '@commerce/api/endpoints/customer/address'
|
||||||
|
|
||||||
|
import getAddresses from './get-addresses'
|
||||||
|
import addItem from './add-item'
|
||||||
|
import updateItem from './update-item'
|
||||||
|
import removeItem from './remove-item'
|
||||||
|
|
||||||
|
export type CustomerAddressAPI = GetAPISchema<OrdercloudAPI, CustomerAddressSchema>
|
||||||
|
export type CustomerAddressEndpoint = CustomerAddressAPI['endpoint']
|
||||||
|
|
||||||
|
export const handlers: CustomerAddressEndpoint['handlers'] = {
|
||||||
|
getAddresses,
|
||||||
|
addItem,
|
||||||
|
updateItem,
|
||||||
|
removeItem,
|
||||||
|
}
|
||||||
|
|
||||||
|
const customerAddressApi = createEndpoint<CustomerAddressAPI>({
|
||||||
|
handler: customerAddressEndpoint,
|
||||||
|
handlers,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default customerAddressApi
|
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