diff options
| author | trisusilo <tri.susilo@altama.co.id> | 2023-09-22 03:25:33 +0000 |
|---|---|---|
| committer | trisusilo <tri.susilo@altama.co.id> | 2023-09-22 03:25:33 +0000 |
| commit | 8b12b43c0c3f9dd2d2743c83c23ed2a3f30fdae0 (patch) | |
| tree | 6ce4958f4000e3db72ceddebe7ffb468eefe395b /src | |
| parent | 74b4e3a9b86f1d3b102ad3f907237f7da1b05009 (diff) | |
| parent | bda91439b6ef4605a579bde8bef603b551aab3dd (diff) | |
Merged in Feature/popup_cart (pull request #73)
Feature/popup cart
Diffstat (limited to 'src')
| -rw-r--r-- | src/contexts/ProductCartContext.js | 21 | ||||
| -rw-r--r-- | src/core/components/elements/Navbar/NavbarDesktop.jsx | 18 | ||||
| -rw-r--r-- | src/lib/cart/components/Cart.jsx | 4 | ||||
| -rw-r--r-- | src/lib/cart/components/Cartheader.jsx | 278 | ||||
| -rw-r--r-- | src/lib/product/api/productSimilarApi.js | 12 | ||||
| -rw-r--r-- | src/lib/product/components/Product/ProductDesktop.jsx | 4 | ||||
| -rw-r--r-- | src/pages/_app.jsx | 77 |
7 files changed, 363 insertions, 51 deletions
diff --git a/src/contexts/ProductCartContext.js b/src/contexts/ProductCartContext.js new file mode 100644 index 00000000..06e97563 --- /dev/null +++ b/src/contexts/ProductCartContext.js @@ -0,0 +1,21 @@ +import { createContext, useCallback, useContext, useEffect, useState } from 'react' + +const ProductCartContext = createContext() + +export const ProductCartProvider = ({ children }) => { + const [productCart, setProductCart] = useState(null) + const [refreshCart, setRefreshCart] = useState(false) + const [isLoading, setIsloading] = useState(false) + + return ( + <ProductCartContext.Provider + value={{ productCart, setProductCart, refreshCart, setRefreshCart, isLoading, setIsloading }} + > + {children} + </ProductCartContext.Provider> + ) +} + +export const useProductCartContext = () => { + return useContext(ProductCartContext) +} diff --git a/src/core/components/elements/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx index 2e4b25fc..3aba55c9 100644 --- a/src/core/components/elements/Navbar/NavbarDesktop.jsx +++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx @@ -10,19 +10,17 @@ import DesktopView from '../../views/DesktopView' import dynamic from 'next/dynamic' import IndoteknikLogo from '@/images/logo.png' import Category from '@/lib/category/components/Category' -import { useContext, useEffect, useState } from 'react' +import { useCallback, useContext, useEffect, useState } from 'react' import useAuth from '@/core/hooks/useAuth' import NavbarUserDropdown from './NavbarUserDropdown' -import { getCountCart } from '@/core/utils/cart' +import { getCartApi, getCountCart } from '@/core/utils/cart' import whatsappUrl from '@/core/utils/whatsappUrl' import { useRouter } from 'next/router' import { getAuth, setAuth } from '@/core/utils/auth' import { createSlug, getIdFromSlug } from '@/core/utils/slug' -import productApi from '@/lib/product/api/productApi' -import { useSession } from 'next-auth/react' -import { AuthContext } from '@/pages/_app' import { TopBannerSkeleton } from '../Skeleton/TopBannerSkeleton' import { useProductContext } from '@/contexts/ProductContext' +import Cardheader from '@/lib/cart/components/Cartheader' const Search = dynamic(() => import('./Search')) const TopBanner = dynamic(() => import('./TopBanner'), { @@ -31,7 +29,6 @@ const TopBanner = dynamic(() => import('./TopBanner'), { const NavbarDesktop = () => { const [isOpenCategory, setIsOpenCategory] = useState(false) - const { authenticated } = useContext(AuthContext) const auth = useAuth() const [cartCount, setCartCount] = useState(0) @@ -43,7 +40,7 @@ const NavbarDesktop = () => { const router = useRouter() const { product } = useProductContext() - + useEffect(() => { if (router.pathname === '/shop/product/[slug]') { setPayloadWa({ @@ -101,7 +98,7 @@ const NavbarDesktop = () => { <div className='flex-1 flex items-center'> <Search /> </div> - <div className='flex gap-x-4'> + <div className='flex gap-x-4 items-center'> <Link href='/my/transactions' target='_blank' @@ -113,7 +110,8 @@ const NavbarDesktop = () => { <br /> Quotation </Link> - <Link + <Cardheader cartCount={cartCount}/> + {/* <Link href='/shop/cart' target='_blank' rel='noreferrer' @@ -132,7 +130,7 @@ const NavbarDesktop = () => { <br /> Belanja </span> - </Link> + </Link> */} <Link target='_blank' rel='noreferrer' diff --git a/src/lib/cart/components/Cart.jsx b/src/lib/cart/components/Cart.jsx index efbcf76b..b5976a1b 100644 --- a/src/lib/cart/components/Cart.jsx +++ b/src/lib/cart/components/Cart.jsx @@ -22,6 +22,7 @@ import LogoSpinner from '@/core/components/elements/Spinner/LogoSpinner' import { getPromotionProgram } from '@/lib/promotinProgram/api/homepageApi' import PromotionType from '@/lib/promotinProgram/components/PromotionType' import { gtagBeginCheckout } from '@/core/utils/googleTag' +import { useProductCartContext } from '@/contexts/ProductCartContext' const Cart = () => { const router = useRouter() @@ -31,6 +32,8 @@ const Cart = () => { const [cart, setCart] = useState(null) + const {setRefreshCart} = useProductCartContext() + useEffect(() => { if (!auth) return }, [auth]) @@ -196,6 +199,7 @@ const Cart = () => { deleteItemCart({ productId }) setDeleteConfirmation(null) setProducts([...productsToUpdate]) + setRefreshCart(true) toast.success('Berhasil menghapus barang dari keranjang') } diff --git a/src/lib/cart/components/Cartheader.jsx b/src/lib/cart/components/Cartheader.jsx new file mode 100644 index 00000000..dd6c276e --- /dev/null +++ b/src/lib/cart/components/Cartheader.jsx @@ -0,0 +1,278 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' +import { getCartApi } from '../api/CartApi' +import currencyFormat from '@/core/utils/currencyFormat' +import Image from '@/core/components/elements/Image/Image' +import { createSlug } from '@/core/utils/slug' +import useAuth from '@/core/hooks/useAuth' +import { useRouter } from 'next/router' +import odooApi from '@/core/api/odooApi' +import { useProductCartContext } from '@/contexts/ProductCartContext' +import whatsappUrl from '@/core/utils/whatsappUrl' + +const { ShoppingCartIcon, PhotoIcon } = require('@heroicons/react/24/outline') +const { default: Link } = require('next/link') + +const Cardheader = (cartCount) => { + const router = useRouter() + const [subTotal, setSubTotal] = useState(null) + const [buttonLoading, SetButtonTerapkan] = useState(false) + const itemLoading = [1, 2, 3] + const auth = useAuth() + const [countCart, setCountCart] = useState(null) + const { productCart, setRefreshCart, setProductCart, refreshCart, isLoading, setIsloading } = + useProductCartContext() + + const [isHovered, setIsHovered] = useState(false) + + const products = useMemo(() => { + return productCart?.products || [] + }, [productCart]) + + const handleMouseEnter = () => { + setIsHovered(true) + getCart() + } + + const handleMouseLeave = () => { + setIsHovered(false) + } + + const getCart = () => { + if (!productCart && auth) { + refreshCartf() + } + } + const refreshCartf = useCallback(async () => { + setIsloading(true) + let cart = await getCartApi() + setProductCart(cart) + setCountCart(cart.productTotal) + setIsloading(false) + }, [setProductCart]) + + useEffect(() => { + if (!products) return + + let calculateTotalPriceBeforeTax = 0 + let calculateTotalTaxAmount = 0 + let calculateTotalDiscountAmount = 0 + for (const product of products) { + if (product.quantity == '') continue + + let priceBeforeTax = product.price.price / 1.11 + calculateTotalPriceBeforeTax += priceBeforeTax * product.quantity + calculateTotalTaxAmount += (product.price.price - priceBeforeTax) * product.quantity + calculateTotalDiscountAmount += + (product.price.price - product.price.priceDiscount) * product.quantity + } + let subTotal = + calculateTotalPriceBeforeTax - calculateTotalDiscountAmount + calculateTotalTaxAmount + setSubTotal(subTotal) + }, [products]) + + useEffect(() => { + if (refreshCart) { + refreshCartf() + } + setRefreshCart(false) + }, [refreshCart, refreshCartf, setRefreshCart]) + + useEffect(() => { + setCountCart(cartCount.cartCount) + }, [cartCount]) + + const handleCheckout = async () => { + SetButtonTerapkan(true) + let checkoutAll = await odooApi('POST', `/api/v1/user/${auth.id}/cart/select-all`) + router.push('/shop/checkout') + } + + return ( + <div className='relative group'> + <div> + <Link + href='/shop/cart' + target='_blank' + rel='noreferrer' + className='flex items-center gap-x-2 !text-gray_r-12/80' + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + > + <div className={`relative ${countCart > 0 && 'mr-2'}`}> + <ShoppingCartIcon className='w-7' /> + {countCart > 0 && ( + <span className='absolute -top-2 -right-2 badge-solid-red rounded-full w-5 h-5 flex items-center justify-center'> + {countCart} + </span> + )} + </div> + <span> + Keranjang + <br /> + Belanja + </span> + </Link> + </div> + <div + className={` ${ + isHovered ? 'block' : 'hidden' + } fixed top-[155px] left-0 w-full h-full bg-black opacity-50 z-10`} + ></div> + <div + className='hidden group-hover:block absolute z-10 left-0 w-96' + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + > + <div className='w-full max-w-md p-2 bg-white border border-gray-200 rounded-lg shadow'> + <div className='p-2 flex justify-between items-center'> + <h5 class='text-base font-semibold leading-none'>Keranjang Belanja</h5> + <Link href='/shop/cart' class='text-sm font-medium text-red-600 underline'> + Lihat Semua + </Link> + </div> + <hr className='mt-3 mb-3 border border-gray-100' /> + <div className='flow-root max-h-[250px] overflow-y-auto'> + {!auth && ( + <div className='justify-center p-4'> + <p className='text-gray-500 text-center '> + Silahkan{' '} + <Link href='/login' className='text-red-600 underline leading-6'> + Login + </Link>{' '} + Untuk Melihat Daftar Keranjang Belanja Anda + </p> + </div> + )} + {isLoading && + itemLoading.map((item) => ( + <div key={item} role='status' class='max-w-sm animate-pulse'> + <div class='flex items-center space-x-4 mb- 2'> + <div class='flex-shrink-0'> + <PhotoIcon class='h-16 w-16 text-gray-500' /> + </div> + <div class='flex-1 min-w-0'> + <div class='h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4'></div> + <div class='h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px] mb-2.5'></div> + <div class='h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5'></div> + </div> + </div> + </div> + ))} + {products.length === 0 && !isLoading && ( + <div className='justify-center p-4'> + <p className='text-gray-500 text-center '> + Tidak Ada Produk di Keranjang Belanja Anda + </p> + </div> + )} + {products.length > 0 && !isLoading && ( + <> + <ul role='list' class='divide-y divide-gray-200 dark:divide-gray-700'> + {products && + products?.map((product, index) => ( + <> + <li class='py-1 sm:py-2'> + <div class='flex items-center space-x-4'> + <div class='flex-shrink-0'> + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' + > + <Image + src={product?.parent?.image} + alt={product?.name} + className='object-contain object-center border border-gray_r-6 h-16 w-16 rounded-md' + /> + </Link> + </div> + <div class='flex-1 min-w-0'> + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' + > + {' '} + <p class='text-caption-2 font-medium text-gray-900 truncate dark:text-white'> + {product.parent.name} + </p> + </Link> + + {product?.price?.discountPercentage > 0 && ( + <div className='flex gap-x-1 items-center mb-2 mt-1'> + <div className='badge-solid-red'> + {product?.price?.discountPercentage}% + </div> + <div className='text-gray_r-11 line-through text-caption-2'> + {currencyFormat(product?.price?.price)} + </div> + </div> + )} + <div className='flex justify-between items-center'> + <div className='font-semibold text-sm text-red-600'> + {product?.price?.priceDiscount > 0 ? ( + currencyFormat(product?.price?.priceDiscount) + ) : ( + <span className='text-gray_r-12/90 font-normal text-caption-1'> + <a + href={whatsappUrl('product', { + name: product.name, + manufacture: product.manufacture?.name, + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ) + })} + className='text-danger-500 underline' + rel='noopener noreferrer' + target='_blank' + > + Call For Price + </a> + </span> + )} + </div> + </div> + </div> + </div> + </li> + </> + ))} + </ul> + <hr /> + </> + )} + </div> + {products.length > 0 && !isLoading && ( + <> + <div className='mt-3'> + <span className='text-gray-400 text-caption-2'>Sub Total Sebelum PPN : </span> + <span className='font-semibold text-red-600'>Rp. {currencyFormat(subTotal)}</span> + </div> + <div className='mt-5 mb-2'> + <button + type='button' + className='btn-solid-red rounded-lg w-full' + onClick={handleCheckout} + disabled={buttonLoading} + > + {buttonLoading ? 'Loading...' : 'Lanjutkan Ke Pembayaran'} + </button> + </div> + </> + )} + </div> + </div> + </div> + ) +} + +export default Cardheader diff --git a/src/lib/product/api/productSimilarApi.js b/src/lib/product/api/productSimilarApi.js index c1bccd59..a008ce5d 100644 --- a/src/lib/product/api/productSimilarApi.js +++ b/src/lib/product/api/productSimilarApi.js @@ -21,10 +21,14 @@ const productSimilarApi = async ({ query, source }) => { const dataProductSimilar = await axios( `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?q=${query}&page=1&orderBy=popular-weekly&operation=OR` ) - dataProductSimilar.data.response.products = [ - ...dataflashSale, - ...dataProductSimilar.data.response.products, - ]; + if (dataflashSale) { + dataProductSimilar.data.response.products = [ + ...dataflashSale, + ...dataProductSimilar.data.response.products + ] + } else { + dataProductSimilar.data.response.products = [...dataProductSimilar.data.response.products] + } return dataProductSimilar.data.response } diff --git a/src/lib/product/components/Product/ProductDesktop.jsx b/src/lib/product/components/Product/ProductDesktop.jsx index 937f2746..6da289bc 100644 --- a/src/lib/product/components/Product/ProductDesktop.jsx +++ b/src/lib/product/components/Product/ProductDesktop.jsx @@ -22,6 +22,7 @@ import ImageNext from 'next/image' import CountDown2 from '@/core/components/elements/CountDown/CountDown2' import { LazyLoadComponent } from 'react-lazy-load-image-component' import ColumnsSLA from './ColumnsSLA' +import { useProductCartContext } from '@/contexts/ProductCartContext' const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { const router = useRouter() @@ -39,6 +40,8 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { const [selectVariantPromoActive, setSelectVariantPromoActive] = useState(null) const [backgorundFlashSale, setBackgorundFlashSale] = useState(null) + const {setRefreshCart , refreshCart} = useProductCartContext() + const getLowestPrice = useCallback(() => { const prices = product.variants.map((variant) => variant.price) const lowest = prices.reduce((lowest, price) => { @@ -116,6 +119,7 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { let source = 'cart' updateCart(variantId, quantity, source) + setRefreshCart(true) setAddCartAlert(true) } diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx index ef737794..0062f7fc 100644 --- a/src/pages/_app.jsx +++ b/src/pages/_app.jsx @@ -11,12 +11,13 @@ import LogoSpinner from '@/core/components/elements/Spinner/LogoSpinner' import { SessionProvider } from 'next-auth/react' import { getAuth } from '@/core/utils/auth' import { ProductProvider } from '@/contexts/ProductContext' +import { ProductCartProvider } from '@/contexts/ProductCartContext' const queryClient = new QueryClient() export const AuthContext = createContext({ - authenticated : false, - setAuthenticated : (auth) => {} + authenticated: false, + setAuthenticated: (auth) => {} }) function MyApp({ Component, pageProps: { session, ...pageProps } }) { @@ -63,43 +64,45 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }) { return ( // <AuthContext.Provider value={{authenticated, setAuthenticated}}> - <SessionProvider session={session}> - <AnimatePresence> - {animateLoader && ( - <motion.div - initial={{ opacity: 0 }} - animate={{ opacity: 1 }} - exit={{ opacity: 0 }} - transition={{ - duration: 0.1 - }} - className='fixed w-screen h-screen z-[500] bg-white flex justify-center items-center' - > - <LogoSpinner /> - </motion.div> - )} - </AnimatePresence> - <Toaster - position='top-center' - containerStyle={toasterStyle} - toastOptions={{ - duration: 3000, - className: 'border border-gray_r-8' - }} - /> - <NextProgress color='#F01C21' options={{ showSpinner: false }} /> - <QueryClientProvider client={queryClient}> - <AnimatePresence - mode='popLayout' - initial={false} - onExitComplete={() => window.scrollTo(0, 0)} + <SessionProvider session={session}> + <AnimatePresence> + {animateLoader && ( + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + exit={{ opacity: 0 }} + transition={{ + duration: 0.1 + }} + className='fixed w-screen h-screen z-[500] bg-white flex justify-center items-center' > - <ProductProvider> + <LogoSpinner /> + </motion.div> + )} + </AnimatePresence> + <Toaster + position='top-center' + containerStyle={toasterStyle} + toastOptions={{ + duration: 3000, + className: 'border border-gray_r-8' + }} + /> + <NextProgress color='#F01C21' options={{ showSpinner: false }} /> + <QueryClientProvider client={queryClient}> + <AnimatePresence + mode='popLayout' + initial={false} + onExitComplete={() => window.scrollTo(0, 0)} + > + <ProductProvider> + <ProductCartProvider> {!animateLoader && <Component {...pageProps} key={router.asPath} />} - </ProductProvider> - </AnimatePresence> - </QueryClientProvider> - </SessionProvider> + </ProductCartProvider> + </ProductProvider> + </AnimatePresence> + </QueryClientProvider> + </SessionProvider> // </AuthContext.Provider> ) } |
