diff --git a/components/common/Layout/Layout.tsx b/components/common/Layout/Layout.tsx index 5d1ef13a6..0233a44af 100644 --- a/components/common/Layout/Layout.tsx +++ b/components/common/Layout/Layout.tsx @@ -8,7 +8,7 @@ import type { Page } from '@commerce/types/page' import { Navbar, Footer } from '@components/common' import type { Category } from '@commerce/types/site' import ShippingView from '@theme/checkout/ShippingView' -import CartSidebarView from '@components/cart/CartSidebarView' +import CartSidebarView from '@theme/cart/CartSidebarView' import { useAcceptCookies } from '@lib/hooks/useAcceptCookies' import { Sidebar, Button, Modal, LoadingDots } from '@components/ui' import PaymentMethodView from '@components/checkout/PaymentMethodView' diff --git a/framework/commerce/types/cart.ts b/framework/commerce/types/cart.ts index e4af878de..70cb621d5 100644 --- a/framework/commerce/types/cart.ts +++ b/framework/commerce/types/cart.ts @@ -54,6 +54,7 @@ export type ProductVariant = { // The variant's depth. If a depth was not explicitly specified on the // variant, this will be the product's depth. depth?: Measurement + stockLevel?: string } // Shopping cart, a.k.a Checkout diff --git a/framework/vendure/utils/normalize.ts b/framework/vendure/utils/normalize.ts index 09c1c6e42..d31eb1d39 100644 --- a/framework/vendure/utils/normalize.ts +++ b/framework/vendure/utils/normalize.ts @@ -46,6 +46,7 @@ export function normalizeCart(order: CartFragment): Cart { sku: l.productVariant.sku, price: l.discountedUnitPriceWithTax / 100, listPrice: l.unitPriceWithTax / 100, + stockLevel: l.productVariant.stockLevel, image: { url: l.featuredAsset?.preview + '?preset=thumb' || '', }, diff --git a/pages/cart.tsx b/pages/cart.tsx index 3279301da..3dfae92e9 100644 --- a/pages/cart.tsx +++ b/pages/cart.tsx @@ -5,7 +5,7 @@ import commerce from '@lib/api/commerce' import { Layout } from '@components/common' import { Button, Text } from '@components/ui' import { Bag, Cross, Check, MapPin, CreditCard } from '@components/icons' -import { CartItem } from '@components/cart' +import { CartItem } from '@theme/cart' export async function getStaticProps({ preview, diff --git a/theme/dap/cart/CartItem/CartItem.module.css b/theme/dap/cart/CartItem/CartItem.module.css new file mode 100644 index 000000000..dd43314fb --- /dev/null +++ b/theme/dap/cart/CartItem/CartItem.module.css @@ -0,0 +1,32 @@ +.root { + @apply flex flex-col py-4; +} + +.root:first-child { + padding-top: 0; +} + +.quantity { + appearance: textfield; + @apply w-8 border-accent-2 border mx-3 rounded text-center text-sm text-black; +} + +.quantity::-webkit-outer-spin-button, +.quantity::-webkit-inner-spin-button { + @apply appearance-none m-0; +} + +.productImage { + position: absolute; + transform: scale(1.9); + width: 100%; + height: 100%; + left: 30% !important; + top: 30% !important; + z-index: 1; +} + +.productName { + @apply font-medium cursor-pointer pb-1; + margin-top: -4px; +} diff --git a/theme/dap/cart/CartItem/CartItem.tsx b/theme/dap/cart/CartItem/CartItem.tsx new file mode 100644 index 000000000..f16cf881b --- /dev/null +++ b/theme/dap/cart/CartItem/CartItem.tsx @@ -0,0 +1,152 @@ +import { ChangeEvent, FocusEventHandler, useEffect, useState } from 'react' +import cn from 'classnames' +import Image from 'next/image' +import Link from 'next/link' +import s from './CartItem.module.css' +import { Trash, Plus, Minus, Cross } from '@components/icons' +import { useUI } from '@components/ui/context' +import type { LineItem } from '@commerce/types/cart' +import usePrice from '@framework/product/use-price' +import useUpdateItem from '@framework/cart/use-update-item' +import useRemoveItem from '@framework/cart/use-remove-item' +import Quantity from '@components/ui/Quantity' + +type ItemOption = { + name: string + nameId: number + value: string + valueId: number +} + +const CartItem = ({ + item, + variant = 'default', + currencyCode, + ...rest +}: { + variant?: 'default' | 'display' + item: LineItem + currencyCode: string +}) => { + const { closeSidebarIfPresent } = useUI() + const [removing, setRemoving] = useState(false) + const [quantity, setQuantity] = useState(item.quantity) + const removeItem = useRemoveItem() + const updateItem = useUpdateItem({ item }) + + const { price } = usePrice({ + amount: item.variant.price * item.quantity, + baseAmount: item.variant.listPrice * item.quantity, + currencyCode, + }) + + const handleChange = async ({ + target: { value }, + }: ChangeEvent) => { + setQuantity(Number(value)) + await updateItem({ quantity: Number(value) }) + } + + const increaseQuantity = async (n = 1) => { + const val = Number(quantity) + n + setQuantity(val) + await updateItem({ quantity: val }) + } + + const handleRemove = async () => { + setRemoving(true) + try { + await removeItem(item) + } catch (error) { + setRemoving(false) + } + } + + // TODO: Add a type for this + const options = (item as any).options + + useEffect(() => { + // Reset the quantity state if the item quantity changes + if (item.quantity !== Number(quantity)) { + setQuantity(item.quantity) + } + }, [item.quantity]) + + return ( +
  • +
    +
    + + closeSidebarIfPresent()} + className={s.productImage} + width={150} + height={150} + src={item.variant.image!.url} + alt={item.variant.image!.altText} + unoptimized + /> + +
    +
    + + closeSidebarIfPresent()} + > + {item.name} + + + {options && options.length > 0 && ( +
    + {options.map((option: ItemOption, i: number) => ( +
    + {option.name} + {option.name === 'Color' ? ( + + ) : ( + + {option.value} + + )} + {i === options.length - 1 ? '' : } +
    + ))} +
    + )} + {variant === 'display' && ( +
    {quantity}x
    + )} +
    +
    + {price} +
    +
    + {variant === 'default' && ( + increaseQuantity(1)} + decrease={() => increaseQuantity(-1)} + max={Number(item.variant.stockLevel)} + /> + )} +
  • + ) +} + +export default CartItem diff --git a/theme/dap/cart/CartItem/index.ts b/theme/dap/cart/CartItem/index.ts new file mode 100644 index 000000000..b5f6dc52f --- /dev/null +++ b/theme/dap/cart/CartItem/index.ts @@ -0,0 +1 @@ +export { default } from './CartItem' diff --git a/theme/dap/cart/CartSidebarView/CartSidebarView.module.css b/theme/dap/cart/CartSidebarView/CartSidebarView.module.css new file mode 100644 index 000000000..c9ffbed50 --- /dev/null +++ b/theme/dap/cart/CartSidebarView/CartSidebarView.module.css @@ -0,0 +1,11 @@ +.root { + min-height: 100vh; +} + +.root.empty { + @apply bg-secondary text-secondary; +} + +.lineItemsList { + @apply py-4 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-accent-2 border-accent-2; +} diff --git a/theme/dap/cart/CartSidebarView/CartSidebarView.tsx b/theme/dap/cart/CartSidebarView/CartSidebarView.tsx new file mode 100644 index 000000000..128b928a8 --- /dev/null +++ b/theme/dap/cart/CartSidebarView/CartSidebarView.tsx @@ -0,0 +1,129 @@ +import cn from 'classnames' +import Link from 'next/link' +import { FC } from 'react' +import s from './CartSidebarView.module.css' +import CartItem from '../CartItem' +import { Button, Text } from '@components/ui' +import { useUI } from '@components/ui/context' +import { Bag, Cross, Check } from '@components/icons' +import useCart from '@framework/cart/use-cart' +import usePrice from '@framework/product/use-price' +import SidebarLayout from '@components/common/SidebarLayout' + +const CartSidebarView: FC = () => { + const { closeSidebar, setSidebarView } = useUI() + const { data, isLoading, isEmpty } = useCart() + + const { price: subTotal } = usePrice( + data && { + amount: Number(data.subtotalPrice), + currencyCode: data.currency.code, + } + ) + const { price: total } = usePrice( + data && { + amount: Number(data.totalPrice), + currencyCode: data.currency.code, + } + ) + const handleClose = () => closeSidebar() + const goToCheckout = () => setSidebarView('CHECKOUT_VIEW') + + const error = null + const success = null + + return ( + + {isLoading || isEmpty ? ( +
    + + + +

    + Your cart is empty +

    +

    + Biscuit oat cake wafer icing ice cream tiramisu pudding cupcake. +

    +
    + ) : error ? ( +
    + + + +

    + We couldn’t process the purchase. Please check your card information + and try again. +

    +
    + ) : success ? ( +
    + + + +

    + Thank you for your order. +

    +
    + ) : ( + <> +
    + + + My Cart + + +
      + {data!.lineItems.map((item: any) => ( + + ))} +
    +
    + +
    +
      +
    • + Subtotal + {subTotal} +
    • +
    • + Taxes + Calculated at checkout +
    • +
    • + Shipping + FREE +
    • +
    +
    + Total + {total} +
    +
    + {process.env.COMMERCE_CUSTOMCHECKOUT_ENABLED ? ( + + ) : ( + + )} +
    +
    + + )} +
    + ) +} + +export default CartSidebarView diff --git a/theme/dap/cart/CartSidebarView/index.ts b/theme/dap/cart/CartSidebarView/index.ts new file mode 100644 index 000000000..0262e448e --- /dev/null +++ b/theme/dap/cart/CartSidebarView/index.ts @@ -0,0 +1 @@ +export { default } from './CartSidebarView' diff --git a/theme/dap/cart/index.ts b/theme/dap/cart/index.ts new file mode 100644 index 000000000..3e53fa34a --- /dev/null +++ b/theme/dap/cart/index.ts @@ -0,0 +1,2 @@ +export { default as CartSidebarView } from './CartSidebarView' +export { default as CartItem } from './CartItem' diff --git a/theme/dap/checkout/CheckoutSidebarView/CheckoutSidebarView.tsx b/theme/dap/checkout/CheckoutSidebarView/CheckoutSidebarView.tsx index 9a87159dd..33a77133d 100644 --- a/theme/dap/checkout/CheckoutSidebarView/CheckoutSidebarView.tsx +++ b/theme/dap/checkout/CheckoutSidebarView/CheckoutSidebarView.tsx @@ -17,7 +17,7 @@ import request from '@commerce/utils/request' const CheckoutSidebarView: FC = () => { const { setSidebarView, closeSidebarIfPresent } = useUI() const { data } = useCart() - + console.log(data, 555) const { price: subTotal } = usePrice( data && { amount: Number(data.subtotalPrice), @@ -109,12 +109,17 @@ const CheckoutSidebarView: FC = () => {
    {/* Once data is correcly filled */} - - {/* */} + {data?.customerId ? ( + + ) : ( + + )} + +
    diff --git a/theme/dap/checkout/ShippingView/ShippingView.tsx b/theme/dap/checkout/ShippingView/ShippingView.tsx index 0afa82a89..f8817b86e 100644 --- a/theme/dap/checkout/ShippingView/ShippingView.tsx +++ b/theme/dap/checkout/ShippingView/ShippingView.tsx @@ -1,10 +1,11 @@ -import { FC, useState } from 'react' +import { FC, useState, useCallback, useEffect } from 'react' import cn from 'classnames' import s from './ShippingView.module.css' import { Button, Input } from '@components/ui' import { useUI } from '@components/ui/context' import SidebarLayout from '@components/common/SidebarLayout' +import { validate } from 'email-validator' import { v4 as uuid } from 'uuid' import { setCustomerForOrderMutation } from '@framework/utils/mutations/set-customer-for-order-mutation' import { setOrderShippingAddressMutation } from '@framework/utils/mutations/set-order-shipping-address-mutation' @@ -19,7 +20,7 @@ const PaymentMethodView: FC = () => { const [streetDetails, setStreetDetails] = useState('') const [apartmentDetails, setApartmentDetails] = useState('') const [loading, setLoading] = useState(false) - + const [disabled, setDisabled] = useState(true) const { setSidebarView } = useUI() const setCustomerForOrder = async () => { @@ -54,10 +55,30 @@ const PaymentMethodView: FC = () => { }) console.log(data, 222) } + const handleValidation = useCallback( + () => { + + setDisabled( + !validate(email) + || firstName.length < 3 + || lastName.length < 3 + || phoneNumber.length < 6 + || streetDetails.length < 5 + ) + }, + [ + firstName, + lastName, + phoneNumber, + email, + streetDetails, + apartmentDetails + ], + ) const handleAddShippingAddress = async (e: React.SyntheticEvent) => { e.preventDefault() - + handleValidation() console.log('Handle Add Shipping Address') try { @@ -71,6 +92,10 @@ const PaymentMethodView: FC = () => { } } + useEffect(() => { + handleValidation() + }, [handleValidation]) + return ( setSidebarView('CHECKOUT_VIEW')}>
    { type="submit" loading={loading} width="100%" + disabled={disabled} > Continue