Polish checkout view

Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
Chloe 2022-05-17 17:09:08 +07:00
parent 9bbd1b29a7
commit 630b4ba486
22 changed files with 279 additions and 27 deletions

View File

@ -61,6 +61,7 @@ const addItem: CustomerAddressEndpoint['handlers']['addItem'] = async ({
updateFulfillmentOptionsForGroup.cart.checkout.fulfillmentGroups[0] updateFulfillmentOptionsForGroup.cart.checkout.fulfillmentGroups[0]
._id, ._id,
fulfillmentMethodId: fulfillmentMethodId:
item.shippingMethodId ||
updateFulfillmentOptionsForGroup.cart.checkout.fulfillmentGroups[0] updateFulfillmentOptionsForGroup.cart.checkout.fulfillmentGroups[0]
.availableFulfillmentOptions[0].fulfillmentMethod._id, .availableFulfillmentOptions[0].fulfillmentMethod._id,
}, },

View File

@ -1,4 +1,7 @@
import type { CustomerAddressSchema } from '../../../../types/customer/address' import type {
CustomerAddressSchema,
CustomerAddressTypes,
} from '../../../../types/customer/address'
import type { OpenCommerceAPI } from '../../..' import type { OpenCommerceAPI } from '../../..'
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api' import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
@ -11,7 +14,7 @@ import removeItem from './remove-item'
export type CustomerAddressAPI = GetAPISchema< export type CustomerAddressAPI = GetAPISchema<
OpenCommerceAPI, OpenCommerceAPI,
CustomerAddressSchema CustomerAddressSchema<CustomerAddressTypes>
> >
export type CustomerAddressEndpoint = CustomerAddressAPI['endpoint'] export type CustomerAddressEndpoint = CustomerAddressAPI['endpoint']

View File

@ -1,8 +1,33 @@
import selectFulfillmentOptions from '../../../mutations/select-fulfillment-options'
import type { CustomerAddressEndpoint } from '.' import type { CustomerAddressEndpoint } from '.'
const updateItem: CustomerAddressEndpoint['handlers']['updateItem'] = async ({ const updateItem: CustomerAddressEndpoint['handlers']['updateItem'] = async ({
res, res,
body: { item, cartId },
config: { fetch, anonymousCartTokenCookie },
req: { cookies },
}) => { }) => {
// Return an error if no cart is present
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Cookie not found' }],
})
}
if (item.shippingMethod) {
await fetch(selectFulfillmentOptions, {
variables: {
input: {
cartId,
cartToken: cookies[anonymousCartTokenCookie],
fulfillmentGroupId: item.shippingMethod.fulfillmentGroupId,
fulfillmentMethodId: item.shippingMethod.id,
},
},
})
}
return res.status(200).json({ data: null, errors: [] }) return res.status(200).json({ data: null, errors: [] })
} }

View File

@ -2,10 +2,11 @@ import { useMemo } from 'react'
import { SWRHook } from '@vercel/commerce/utils/types' import { SWRHook } from '@vercel/commerce/utils/types'
import useCart, { UseCart } from '@vercel/commerce/cart/use-cart' import useCart, { UseCart } from '@vercel/commerce/cart/use-cart'
import type { GetCartHook } from '@vercel/commerce/types/cart' import type { GetCartHook } from '@vercel/commerce/types/cart'
import { CartTypes } from '../types/cart'
export default useCart as UseCart<typeof handler> export default useCart as UseCart<typeof handler>
export const handler: SWRHook<GetCartHook> = { export const handler: SWRHook<GetCartHook<CartTypes>> = {
fetchOptions: { fetchOptions: {
url: '/api/cart', url: '/api/cart',
method: 'GET', method: 'GET',

View File

@ -7,6 +7,7 @@ import useCheckout, {
} from '@vercel/commerce/checkout/use-checkout' } from '@vercel/commerce/checkout/use-checkout'
import useSubmitCheckout from './use-submit-checkout' import useSubmitCheckout from './use-submit-checkout'
import { useCheckoutContext } from '@components/checkout/context' import { useCheckoutContext } from '@components/checkout/context'
import { useCart } from '../cart'
export default useCheckout as UseCheckout<typeof handler> export default useCheckout as UseCheckout<typeof handler>
@ -17,13 +18,23 @@ export const handler: SWRHook<GetCheckoutHook> = {
}, },
useHook: () => useHook: () =>
function useHook() { function useHook() {
const { data: cart } = useCart()
const hasShippingMethods = !!(
cart?.checkout?.fulfillmentGroups &&
cart.checkout.fulfillmentGroups.find(
(group) => group?.type === 'shipping'
)
)
const { cardFields, addressFields } = useCheckoutContext() const { cardFields, addressFields } = useCheckoutContext()
const { shippingMethod, ...restAddressFields } = addressFields
// Basic validation - check that at least one field has a value. // Basic validation - check that at least one field has a value.
const hasEnteredCard = Object.values(cardFields).some( const hasEnteredCard = Object.values(cardFields).some(
(fieldValue) => !!fieldValue (fieldValue) => !!fieldValue
) )
const hasEnteredAddress = Object.values(addressFields).some( const hasEnteredAddress = Object.values(restAddressFields).some(
(fieldValue) => !!fieldValue (fieldValue) => !!fieldValue
) )
@ -32,6 +43,8 @@ export const handler: SWRHook<GetCheckoutHook> = {
data: { data: {
hasPayment: hasEnteredCard, hasPayment: hasEnteredCard,
hasShipping: hasEnteredAddress, hasShipping: hasEnteredAddress,
hasShippingMethods,
hasSelectedShippingMethod: !!shippingMethod?.id,
}, },
}), }),
[hasEnteredCard, hasEnteredAddress] [hasEnteredCard, hasEnteredAddress]

View File

@ -1 +1,2 @@
export { default as useAddItem } from './use-add-item' export { default as useAddItem } from './use-add-item'
export { default as useUpdateItem } from './use-update-item'

View File

@ -5,10 +5,11 @@ import useAddItem, {
UseAddItem, UseAddItem,
} from '@vercel/commerce/customer/address/use-add-item' } from '@vercel/commerce/customer/address/use-add-item'
import { useCheckoutContext } from '@components/checkout/context' import { useCheckoutContext } from '@components/checkout/context'
import { CustomerAddressTypes } from '../../types/customer/address'
export default useAddItem as UseAddItem<typeof handler> export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = { export const handler: MutationHook<AddItemHook<CustomerAddressTypes>> = {
fetchOptions: { fetchOptions: {
url: '/api/customer/address', url: '/api/customer/address',
method: 'POST', method: 'POST',
@ -23,11 +24,11 @@ export const handler: MutationHook<AddItemHook> = {
}, },
useHook: ({ fetch }) => useHook: ({ fetch }) =>
function useHook() { function useHook() {
const { setAddressFields } = useCheckoutContext() const { setAddressFields, addressFields } = useCheckoutContext()
return useCallback( return useCallback(
async function addItem(input) { async function addItem(input) {
await fetch({ input }) await fetch({ input })
setAddressFields(input) setAddressFields({ ...addressFields, ...input })
return undefined return undefined
}, },
[setAddressFields] [setAddressFields]

View File

@ -0,0 +1,44 @@
import type { UpdateItemHook } from '@vercel/commerce/types/customer/address'
import type {
MutationHook,
MutationHookContext,
} from '@vercel/commerce/utils/types'
import { useCallback } from 'react'
import useUpdateItem, {
UseUpdateItem,
} from '@vercel/commerce/customer/address/use-update-item'
import { useCheckoutContext } from '@components/checkout/context'
import { CustomerAddressTypes } from '../../types/customer/address'
export default useUpdateItem as UseUpdateItem<typeof handler>
export const handler: MutationHook<UpdateItemHook<CustomerAddressTypes>> = {
fetchOptions: {
url: '/api/customer/address',
method: 'PUT',
},
async fetcher({ input: { item, itemId }, options, fetch }) {
const data = await fetch({
...options,
body: { item, itemId },
})
return data
},
useHook: ({ fetch }) =>
function useHook() {
const { setAddressFields, addressFields } = useCheckoutContext()
return useCallback(
async function updateItem(input) {
const { id, ...rest } = input
await fetch({ input: { item: rest, itemId: id } })
setAddressFields({
...addressFields,
shippingMethod: rest.shippingMethod,
})
return undefined
},
[setAddressFields]
)
},
}

View File

@ -13,6 +13,7 @@ import { handler as useSubmitCheckout } from './checkout/use-submit-checkout'
import { handler as useAddCardItem } from './customer/card/use-add-item' import { handler as useAddCardItem } from './customer/card/use-add-item'
import { handler as useCards } from './customer/card/use-cards' import { handler as useCards } from './customer/card/use-cards'
import { handler as useAddAddressItem } from './customer/address/use-add-item' import { handler as useAddAddressItem } from './customer/address/use-add-item'
import { handler as useUpdateAddressItem } from './customer/address/use-update-item'
export const openCommerceProvider = { export const openCommerceProvider = {
locale: 'en-us', locale: 'en-us',
@ -24,6 +25,7 @@ export const openCommerceProvider = {
card: { useCards, useAddItem: useAddCardItem }, card: { useCards, useAddItem: useAddCardItem },
address: { address: {
useAddItem: useAddAddressItem, useAddItem: useAddAddressItem,
useUpdateItem: useUpdateAddressItem,
}, },
}, },
products: { useSearch }, products: { useSearch },

View File

@ -15,6 +15,7 @@ export type CartItemBody = Core.CartItemBody & {
export type CartTypes = Core.CartTypes & { export type CartTypes = Core.CartTypes & {
itemBody: CartItemBody itemBody: CartItemBody
cart?: Cart
} }
export type CartSchema = Core.CartSchema<CartTypes> export type CartSchema = Core.CartSchema<CartTypes>

View File

@ -1 +1,14 @@
import * as Core from '@vercel/commerce/types/customer/address'
export * from '@vercel/commerce/types/customer/address' export * from '@vercel/commerce/types/customer/address'
export type AddressFields = Core.AddressFields & {
shippingMethod?: {
id: string
fulfillmentGroupId: string
}
}
export type CustomerAddressTypes = Core.CustomerAddressTypes & {
fields: AddressFields
}

View File

@ -10,9 +10,11 @@ import useCheckout from '@framework/checkout/use-checkout'
import useSubmitCheckout from '@framework/checkout/use-submit-checkout' import useSubmitCheckout from '@framework/checkout/use-submit-checkout'
import ShippingWidget from '../ShippingWidget' import ShippingWidget from '../ShippingWidget'
import PaymentWidget from '../PaymentWidget' import PaymentWidget from '../PaymentWidget'
import s from './CheckoutSidebarView.module.css' import ShippingMethodWidget from '../ShippingMethodWidget'
import { useCheckoutContext } from '../context' import { useCheckoutContext } from '../context'
import s from './CheckoutSidebarView.module.css'
const CheckoutSidebarView: FC = () => { const CheckoutSidebarView: FC = () => {
const [loadingSubmit, setLoadingSubmit] = useState(false) const [loadingSubmit, setLoadingSubmit] = useState(false)
const { setSidebarView, closeSidebar } = useUI() const { setSidebarView, closeSidebar } = useUI()
@ -76,6 +78,13 @@ const CheckoutSidebarView: FC = () => {
onClick={() => setSidebarView('SHIPPING_VIEW')} onClick={() => setSidebarView('SHIPPING_VIEW')}
/> />
{checkoutData?.hasShippingMethods && (
<ShippingMethodWidget
isValid={checkoutData?.hasSelectedShippingMethod}
onClick={() => setSidebarView('SHIPPING_METHOD_VIEW')}
/>
)}
<ul className={s.lineItemsList}> <ul className={s.lineItemsList}>
{cartData!.lineItems.map((item: any) => ( {cartData!.lineItems.map((item: any) => (
<CartItem <CartItem
@ -101,10 +110,19 @@ const CheckoutSidebarView: FC = () => {
<span>Taxes</span> <span>Taxes</span>
<span>Calculated at checkout</span> <span>Calculated at checkout</span>
</li> </li>
<li className="flex justify-between py-1"> {checkoutData?.hasSelectedShippingMethod ? (
<span>Shipping</span> <li className="flex justify-between py-1">
<span className="font-bold tracking-wide">FREE</span> <span>Shipping</span>
</li> <span>
{cartData?.checkout?.summary.fulfillmentTotal?.displayAmount}
</span>
</li>
) : (
<li className="flex justify-between py-1">
<span>Shipping</span>
<span className="font-bold tracking-wide">FREE</span>
</li>
)}
</ul> </ul>
<div className="flex justify-between border-t border-accent-2 py-3 font-bold mb-2"> <div className="flex justify-between border-t border-accent-2 py-3 font-bold mb-2">
<span>Total</span> <span>Total</span>

View File

@ -0,0 +1,79 @@
import useCart from '@framework/cart/use-cart'
import useUpdateUpdateAddress from '@framework/customer/address/use-update-item'
import SidebarLayout from '@components/common/SidebarLayout'
import { useUI } from '@components/ui/context'
import { Button } from '@components/ui'
import { useCheckoutContext } from '../context'
const ShippingMethod = () => {
const { setSidebarView } = useUI()
const { data: cart } = useCart()
const { addressFields } = useCheckoutContext()
const updateShippingMethod = useUpdateUpdateAddress()
const shippingGroup = cart?.checkout?.fulfillmentGroups.find(
(group) => group?.type === 'shipping'
)
const handleSubmit = async (event: React.ChangeEvent<HTMLFormElement>) => {
event.preventDefault()
await updateShippingMethod({
id: cart!.id,
...addressFields,
shippingMethod: {
fulfillmentGroupId:
cart!.checkout?.fulfillmentGroups[0]?._id ?? 'groupId',
id: event.target.shippingMethod.value,
},
})
setSidebarView('CHECKOUT_VIEW')
}
return shippingGroup ? (
<form className="h-full" onSubmit={handleSubmit}>
<SidebarLayout handleBack={() => setSidebarView('CHECKOUT_VIEW')}>
<div className="px-4 sm:px-6 flex-1">
<h2 className="pt-1 pb-8 text-2xl font-semibold tracking-wide cursor-pointer inline-block">
Shipping Methods
</h2>
<div>
{shippingGroup.availableFulfillmentOptions.map((option) => (
<div
className="flex flex-row my-3 items-center justify-between"
key={option?.fulfillmentMethod?._id}
>
<fieldset className="flex flex-row items-center">
<input
name="shippingMethod"
className="bg-black"
type="radio"
value={option?.fulfillmentMethod?._id}
defaultChecked={
shippingGroup.selectedFulfillmentOption?.fulfillmentMethod
?._id === option?.fulfillmentMethod?._id
}
/>
<span className="ml-3 text-sm">
{option?.fulfillmentMethod?.displayName ||
'Shipping Method'}
</span>
</fieldset>
<span>{option?.price.displayAmount}</span>
</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">
<Button type="submit" width="100%" variant="ghost">
Continue
</Button>
</div>
</SidebarLayout>
</form>
) : null
}
export default ShippingMethod

View File

@ -0,0 +1 @@
export { default } from './ShippingMethodView'

View File

@ -0,0 +1,4 @@
.root {
@apply border border-accent-2 px-6 py-5 mb-4 text-center
flex items-center cursor-pointer hover:border-accent-4;
}

View File

@ -0,0 +1,28 @@
import { FC } from 'react'
import s from './ShippingMethodWidget.module.css'
import { ChevronRight, CreditCard, Check } from '@components/icons'
interface ComponentProps {
onClick?: () => any
isValid?: boolean
}
const ShippingMethodWidget: FC<ComponentProps> = ({ onClick, isValid }) => {
/* Shipping Method Widget
Only available with checkout set to true and cart has some available shipping methods
This means that the provider does offer checkout functionality. */
return (
<div onClick={onClick} className={s.root}>
<div className="flex flex-1 items-center">
<CreditCard className="w-5 flex" />
<span className="ml-5 text-sm text-center font-medium">
Add Shipping Method
</span>
{/* <span>VISA #### #### #### 2345</span> */}
</div>
<div>{isValid ? <Check /> : <ChevronRight />}</div>
</div>
)
}
export default ShippingMethodWidget

View File

@ -0,0 +1 @@
export { default } from './ShippingMethodWidget'

View File

@ -5,9 +5,9 @@ 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 useAddAddress from '@framework/customer/address/use-add-item'
import { useCheckoutContext } from '../context'
import s from './ShippingView.module.css' import s from './ShippingView.module.css'
import { useCheckoutContext } from '../context'
interface Form extends HTMLFormElement { interface Form extends HTMLFormElement {
cardHolder: HTMLInputElement cardHolder: HTMLInputElement
@ -21,6 +21,7 @@ interface Form extends HTMLFormElement {
zipCode: HTMLInputElement zipCode: HTMLInputElement
city: HTMLInputElement city: HTMLInputElement
country: HTMLSelectElement country: HTMLSelectElement
shippingMethod?: HTMLInputElement
} }
const ShippingView: FC = () => { const ShippingView: FC = () => {
@ -64,6 +65,7 @@ const ShippingView: FC = () => {
Use a different shipping address Use a different shipping address
</span> </span>
</div> </div>
<hr className="border-accent-2 my-6" /> <hr className="border-accent-2 my-6" />
<div className="grid gap-3 grid-flow-row grid-cols-12"> <div className="grid gap-3 grid-flow-row grid-cols-12">
<div className={cn(s.fieldset, 'col-span-6')}> <div className={cn(s.fieldset, 'col-span-6')}>

View File

@ -7,7 +7,7 @@ import React, {
createContext, createContext,
} from 'react' } from 'react'
import type { CardFields } from '@commerce/types/customer/card' import type { CardFields } from '@commerce/types/customer/card'
import type { AddressFields } from '@commerce/types/customer/address' import type { AddressFields } from '@framework/types/customer/address'
export type State = { export type State = {
cardFields: CardFields cardFields: CardFields
@ -86,7 +86,10 @@ export const CheckoutProvider: FC = (props) => {
const cardFields = useMemo(() => state.cardFields, [state.cardFields]) const cardFields = useMemo(() => state.cardFields, [state.cardFields])
const addressFields = useMemo(() => state.addressFields, [state.addressFields]) const addressFields = useMemo(
() => state.addressFields,
[state.addressFields]
)
const value = useMemo( const value = useMemo(
() => ({ () => ({
@ -96,7 +99,13 @@ export const CheckoutProvider: FC = (props) => {
setAddressFields, setAddressFields,
clearCheckoutFields, clearCheckoutFields,
}), }),
[cardFields, addressFields, setCardFields, setAddressFields, clearCheckoutFields] [
cardFields,
addressFields,
setCardFields,
setAddressFields,
clearCheckoutFields,
]
) )
return <CheckoutContext.Provider value={value} {...props} /> return <CheckoutContext.Provider value={value} {...props} />

View File

@ -12,6 +12,7 @@ import { useAcceptCookies } from '@lib/hooks/useAcceptCookies'
import { Sidebar, Button, LoadingDots } from '@components/ui' import { Sidebar, Button, LoadingDots } from '@components/ui'
import PaymentMethodView from '@components/checkout/PaymentMethodView' import PaymentMethodView from '@components/checkout/PaymentMethodView'
import CheckoutSidebarView from '@components/checkout/CheckoutSidebarView' import CheckoutSidebarView from '@components/checkout/CheckoutSidebarView'
import ShippingMethodView from '@components/checkout/ShippingMethodView'
import { CheckoutProvider } from '@components/checkout/context' import { CheckoutProvider } from '@components/checkout/context'
import { MenuSidebarView } from '@components/common/UserNav' import { MenuSidebarView } from '@components/common/UserNav'
import type { Page } from '@commerce/types/page' import type { Page } from '@commerce/types/page'
@ -87,6 +88,7 @@ const SidebarView: React.FC<{
{sidebarView === 'CART_VIEW' && <CartSidebarView />} {sidebarView === 'CART_VIEW' && <CartSidebarView />}
{sidebarView === 'SHIPPING_VIEW' && <ShippingView />} {sidebarView === 'SHIPPING_VIEW' && <ShippingView />}
{sidebarView === 'PAYMENT_VIEW' && <PaymentMethodView />} {sidebarView === 'PAYMENT_VIEW' && <PaymentMethodView />}
{sidebarView === 'SHIPPING_METHOD_VIEW' && <ShippingMethodView />}
{sidebarView === 'CHECKOUT_VIEW' && <CheckoutSidebarView />} {sidebarView === 'CHECKOUT_VIEW' && <CheckoutSidebarView />}
{sidebarView === 'MOBILE_MENU_VIEW' && <MenuSidebarView links={links} />} {sidebarView === 'MOBILE_MENU_VIEW' && <MenuSidebarView links={links} />}
</Sidebar> </Sidebar>

View File

@ -18,7 +18,7 @@ const SubItem = ({ subItem, level = 0 }: SubItemProps) => {
<a <a
className={`block rounded ml-${ className={`block rounded ml-${
level * 2 level * 2
} py-[10px] px-4 text-sm text-secondary`} } py-[10px] px-4 text-sm text-black`}
> >
{subItem.label} {subItem.label}
</a> </a>
@ -28,7 +28,7 @@ const SubItem = ({ subItem, level = 0 }: SubItemProps) => {
href={subItem.url} href={subItem.url}
className={`block rounded ml-${ className={`block rounded ml-${
level * 2 level * 2
} py-[10px] px-4 text-sm text-secondary`} } py-[10px] px-4 text-sm text-black`}
target={subItem.shouldOpenInNewWindow ? '_blank' : ''} target={subItem.shouldOpenInNewWindow ? '_blank' : ''}
rel="noreferrer" rel="noreferrer"
> >

View File

@ -29,16 +29,19 @@ const Navbar: FC<NavbarProps> = ({ links, customNavigation }) => (
</a> </a>
</Link> </Link>
<nav className={s.navMenu}> <nav className={s.navMenu}>
<Link href="/search"> {process.env.COMMERCE_CUSTOMNAVIGATION_ENABLED ? (
<a className={s.link}>All</a>
</Link>
{links?.map((l) => (
<Link href={l.href} key={l.href}>
<a className={s.link}>{l.label}</a>
</Link>
))}
{process.env.COMMERCE_CUSTOMNAVIGATION_ENABLED && (
<CustomNavbar links={customNavigation} /> <CustomNavbar links={customNavigation} />
) : (
<>
<Link href="/search">
<a className={s.link}>All</a>
</Link>
{links?.map((l) => (
<Link href={l.href} key={l.href}>
<a className={s.link}>{l.label}</a>
</Link>
))}
</>
)} )}
</nav> </nav>
</div> </div>