diff options
93 files changed, 4384 insertions, 771 deletions
diff --git a/public/images/ICON PROMO DISKON.svg b/public/images/ICON PROMO DISKON.svg new file mode 100644 index 00000000..f7c16ca8 --- /dev/null +++ b/public/images/ICON PROMO DISKON.svg @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 24.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 163 51" style="enable-background:new 0 0 163 51;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#ED1E27;} + .st1{fill:#FFFFFF;} + .st2{font-family:'ProximaNova-Extrabld';} + .st3{font-size:30.2617px;} +</style> +<polygon class="st0" points="163,48.3 114.7,0 163,0 "/> +<text transform="matrix(1 0 0 1 137.5257 24.7005)" class="st1 st2 st3">%</text> +</svg> diff --git a/public/images/icon_promo/angklung.svg b/public/images/icon_promo/angklung.svg new file mode 100644 index 00000000..a650d816 --- /dev/null +++ b/public/images/icon_promo/angklung.svg @@ -0,0 +1,9 @@ +<svg width="39" height="38" viewBox="0 0 39 38" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<rect x="0.666992" y="-0.00439453" width="38" height="38" fill="url(#pattern0_3825_7139)"/> +<defs> +<pattern id="pattern0_3825_7139" patternContentUnits="objectBoundingBox" width="1" height="1"> +<use xlink:href="#image0_3825_7139" transform="scale(0.00478469)"/> +</pattern> +<image id="image0_3825_7139" width="209" height="209" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANEAAADRCAMAAABl5KfdAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADNQTFRF7WRs/e/w5zU/9KGm+MHE5BYi+tDT6URO8IKJ5iUx++Dh8pKX73N761Rd9rG14gYT////UqvA5AAAABF0Uk5T/////////////////////wAlrZliAAAIr0lEQVR42uzd25qrKgwA4Cgi4Pn9n3bPdFYVFJBwCHZ/5W7W6ij/iJhEtLD93xp8RV/RV/QVfUXPaQObBF9/mxITGz5d1IztajY19R8sGmC1tZZ9qKiZVldT7BNFC189TQyfJmpg9TfOPkvUtOttg08S9Wpd0aQni3rzFJrH7u/AdbL1kOBDhpwaG2NCn7iL9GCR0M7/0T+ps48QSW28NbYPdMdZxvsPEPUHaHQNy+Moig8QCeuQOs/Ulg89VdSFgDS3erxo76r0z4fqAoeHHyIRerqph4v2E6S/++R0/uQzRQ13BG2ej06PFi3Bh+i4bqlHi+Ayg3ky3Ld+oBf9hJhyFr9tkv72nsHa6ffTUi6+Y9Wasx2ZaBjbNaFxWO6G3UQqGmBNbor5Z3pBKPIUP3Cmzrp186SjEIWlokHNGrS+/5NMxNaMDR4gGte1MIlYBGvmJiuLDJCCsQtp7/m4/ftxnIyJv6sq0oec6LCR9xEyDPKolCjHp1sKkTYp8AXxe+Zf/c8kXONuJLweaaC2wfzie7pfrAOYmx+eTSg8EbR3crL+q5movy+wbz48EbQPJG5PhWbrbprSogTQkSEwewnPNkLb0vlRCujIEFRjhXaW/YyFRWmgY9aX1grReB2J70FXSpQI0jra2YadvE6AULYWlArSThneWLY7X3c0FBWlg7SDpG/hlNxpxXEoWlPNANLjJ3CKjltm2pGEh4L0Uv7gEDFrJghPBW2DZXLQRcOs3WDaCopygbTat01klGKMHcFjQfu21BXJjcoFN6p58FzQv63p/e2spTyzPAkPBv0metK4R24TqVO9FZ4Mcp9anh3BJ4EsomsdBWqCpFqVTBEJS4Uf6oH+rSmZokX2QjjUBukhAUo0O0oxUB20dhGieXT+HaA6KEbku4MO1UG8eaAoBeRfU1JJRAaiEtGBiESEIBoRJYhEhA99pngQhSgiOLWCGlDr3D9AFAE6AhmtAvxX1OF9dVFU+mD5nXeVaq4tisuHpstvHWW3yqLYBK89/Z629L6uKDpjbUzSUQ+uPOoSUnCDNLSuog6xKKmmYH8Ih1edvROLJDZSAKigKLnqcyWFgMqJ8CAmBPQ+kgHquoFWhA9OxfUomCT9/xblrBAVEkVH28o98K43vyWdKCF96H3XpdPmOZkoJR/qt1sS88YPJUQpIHUTPRibpzpGSRnrchcQ6ZufaETZU3CTtNxtPruoQE3BIPG7zecWFSmSWGM81+YziwpVfSwk5+bziopVfS4k9+azivCgwV71GYWQXpJn8zlFSVUfbSd/z1CcNmGQfJvPKIpJHzrLMXrXFNxHybv5fKK4fEhdnnbYiyTCdS75N59NFJng6e9XAPMfhGN6uNl8LlF0xnoiaWPrGuO8nrqChiSHTUjBDdLdncqma0JPzTRRUk1BJ3FcTaGYKLFI0vO4qo+/M3OCKLnqcyXFg/YoRMaLMixeOpMMUD+gtqUCgiooDTqTzlWfFnHE+pDHzaE46DQ99JfNh5PAl7GHiKJAryfvPCSGOs3tYRVEikY8iL1XLM/dLYmFviZjv16poJVEcHuIw0GLci7mu5I8EZGjzWvQOw4gG+j8YjbzPWxnkvbziDuJ7hYNQDbQNbn2kLSfWizo5phCOdBpFtNJkmNH9NEhPsSI8oBOf35bQIQH3S5UgYKg894tJDzodqEu5AbxWQrXOXIh4UH3uQJkBr1ezNa358cF7aQSIIsIfx1qL3scXNdCg4S5QRgMuorwIGl5VAtcp3HPEwZAUFEEkkFayYddlNIdthYCnUVTRHBqi6ulOx74RyoFOomiom3bMhjliSlfZVVRCmSK4vKh5Zr9jKv3mYhuDMuJYkCGqI9M8NiZ1IVFyUVAuujIP7AZ64l0zGcTPUgXzfEpuEHSJuiBHqSJlpSaArPeTf3xCSFAStk5l/rkBmkilVQkCXgVlVooQIeIJVZ9Qt6u1RGADpFCvHrxVSMR04AlCQLQLlrC3shqRI+ncm8AiQC0iybME1vvPQrkwFMEoF3UIq4gxx6R5xIQgHYR4izS9uibHloh4jLWOQl0EaFAKqCw/HMlWn5fKCog8NkcSAOdRS1qTCwFiv+pILTodq1PIikZhB11AYuXkkjpoItoSAUlkTKAdpEKKquHPVQYTcoB2kVBb2uWgbVaFhH45ALtIhbSVW4DdbMQzE0Kf55yygI6IlUeEAbZqj6TtQsM37khD+gQyYD7OZYv4ZgcsQYLfu5rP9Z5QIfoePgMEFUf5gzZGe4Onla3SXxEFyynfdC92ReJed4JwzB38LSJNPUparBNzaEkbQKwXMeWduWAXY7H2ZZPpNVwwkiMr1nKWPvcICVLf88Q2OfcINKKvTtM0sBxBUWRRPNUUdhVG+JS0zqiKNLDvoQDtlRSGkjylU9NSRGalHb7AXBxRZQITUo5Rj2q6hkroiSNmLJnvIiQhA7+IkV0pCHDfaYgER2JZR90AavRypJ+MuB52QhEaFL7mEAItkyk+fEi0usSjQhN6h4vwpKCZywmuFiqiJCkQFEzx70XLYsIR8LdH+JNFRGGhF233dURhZGYCq76wFpblHl+hqjXqOYVZSXBin1aooQoI4nkegyEHaEJMICuK0QRE5B1hioEBKrukMW0QNQhuiAdaLpEmHUASaco0yig6BZpXggEHaNNdAm6Rpy5Q/HhQ12KKH5GkNdW0GuPsU9skReLCk/EFapfEauPEZ2sUc4rev2vUp+MWn8c2NE6BdeCYWelCnLkCuSAztYqiZfKdhIfFaAX3ZHqgbYySXZFUMKb/z2kmqAtT23HvGc51AQlfYMG2F88M/KqN/5ylRRX9ffta8OoKi/rSlyJfFo5KB6wTi1fJTv2IbeHifSFoOc2bh8p2hi3e/iyfahIe/GMcUoN28eKXiszzwdo3LZPFm2NaeKy5iKhbN89Cv+uQwqWrWp72NLmr+gr+oq+oiLtPwEGAG0NPl3kGYGKAAAAAElFTkSuQmCC"/> +</defs> +</svg> diff --git a/public/images/icon_promo/barong.svg b/public/images/icon_promo/barong.svg new file mode 100644 index 00000000..22e51ff9 --- /dev/null +++ b/public/images/icon_promo/barong.svg @@ -0,0 +1,9 @@ +<svg width="39" height="38" viewBox="0 0 39 38" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<rect x="0.333008" y="-0.00439453" width="38" height="38" fill="url(#pattern0_3850_5455)"/> +<defs> +<pattern id="pattern0_3850_5455" patternContentUnits="objectBoundingBox" width="1" height="1"> +<use xlink:href="#image0_3850_5455" transform="scale(0.00478469)"/> +</pattern> +<image id="image0_3850_5455" width="209" height="209" xlink:href="data:image/png;base64,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"/> +</defs> +</svg> diff --git a/public/images/icon_promo/diskon.svg b/public/images/icon_promo/diskon.svg new file mode 100644 index 00000000..23d24875 --- /dev/null +++ b/public/images/icon_promo/diskon.svg @@ -0,0 +1,9 @@ +<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<rect y="-0.00439453" width="38" height="38" fill="url(#pattern0_3853_5915)"/> +<defs> +<pattern id="pattern0_3853_5915" patternContentUnits="objectBoundingBox" width="1" height="1"> +<use xlink:href="#image0_3853_5915" transform="scale(0.00478469)"/> +</pattern> +<image id="image0_3853_5915" width="209" height="209" xlink:href="data:image/png;base64,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"/> +</defs> +</svg> diff --git a/public/images/icon_promo/silat.svg b/public/images/icon_promo/silat.svg new file mode 100644 index 00000000..112ecaff --- /dev/null +++ b/public/images/icon_promo/silat.svg @@ -0,0 +1,9 @@ +<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<rect y="-0.00439453" width="38" height="38" fill="url(#pattern0_3821_6700)"/> +<defs> +<pattern id="pattern0_3821_6700" patternContentUnits="objectBoundingBox" width="1" height="1"> +<use xlink:href="#image0_3821_6700" transform="scale(0.005)"/> +</pattern> +<image id="image0_3821_6700" width="200" height="200" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADNQTFRF+MHE8IKJ6URO5iUx9KGm/e/w5BYi7WRs++Dh5zU/9rG1+tDT73N78pKX61Rd4gYT////3hCh9wAAABF0Uk5T/////////////////////wAlrZliAAADrklEQVR42uzd65KjIBAFYGgF4iXR93/arYw3FNTZjRqOe/pnYtX4VY0a+wKqvUkoQgghhBBCCDkIUpX6lHiqx5UQUzTnhbsQUjdnRnkdRE6F6OsgxakQdx3kVEcjhPwrJHL77K8e2bjD9oe48Bv3LUjkGL1/vfaHRB6vihBCCCGEEEIISR6SO3EKH5Jn3ckpbEjPmFEAIR7Do8BBFoyRAgaJMHoKFESy1fe/QpAgs8jERj6Fg2T5+2OLDvlhvHOTAQUKMjBiFCCIzwgpMJAlY0lJHFJuMOYUkzakSwuvMTxK3SYOMVrqnbqAyd0rKk0K8tEfuBmkUGfEFyDnhM1vAmnsXSDTk4sQQsAhWaRe179xykbs1gNVCg9E2f+Lu79kCSGEEEIIIYSQTciFbU4RSP6Sp8GHdGULqw02ZKq+TBRAyLyINFDgIGEtrKOAQeIlvTcFCpKvlvSs/gASSYP1/ZVuI1PWH/IMv3nuQn4RBz0Qz+pFkUhJz7SlwEOGfL8SaIhftlhSgCDL6sucAgOJFZF8SuIQt1PSGylF4pCq2Svp9ZQ8cUirXuJ2SnpVLZK3qUM+CUJmkNKJiK7QIeXwK08qZMjj5T2paoMKebjY+w8cZMkYKVgQ5daGDB9QELvx5pMhQWb/T3XpvtuceQiku8IjFwwWZEqfBRQkiJ/QDCg4kDkjoKBAQsaCUqUNqbYYPiVr04b8rKKwzhgptkwd0lZqd9kRo5Rpk4ecU3ojhBBCCCGEEEIIIYQQQggh5H+EHD715jgIQwghhBDyC4iNjLRZRMhGv9ZWNhZqWoEQQgghhBBCEoTIwVH8FeShtToIcv0gzHQKlfRr82FDpg6KiQIImbfnDd3XcJBw9qVrmQeDqOi01ZsCBXmuDo1l+bFTb8MddH9LgsSn3urLpt6sfpjYWOJBkMvG9/oG9ZCCBXHjlkTBkpxIEDfbWWlBwYG4YIOoGSVxyGudMaekPvXWfWXd6nZdA0UnDmlz22SbPZ+teRbxBTk5LHZTiNFZU+TwkOEa9IZTESH+DX6k4EGWvx56ChrERJZY75Zeh4LEGD0FClKsl8oyQYIsTj27wdRbl3iKvMqBQcb8WUCBgvgZzSUFCCLLc51RYCASKwF4lMQh1RbDp1iTNqSVTcZE0W3ikDbXz73to6tFiSZNyDmlN0IIIYQQQgghhBBCCCGEEEIIuQICFoQAQOQukBckpAohJaJjarfxsoPlxnbfaYbUJgbBDkIIIYQQQr4SfwQYAF0sNCgAdJuiAAAAAElFTkSuQmCC"/> +</defs> +</svg> diff --git a/public/images/penawaran-terbatas.jpg b/public/images/penawaran-terbatas.jpg Binary files differnew file mode 100644 index 00000000..d07866e6 --- /dev/null +++ b/public/images/penawaran-terbatas.jpg diff --git a/public/images/reject.png b/public/images/reject.png Binary files differnew file mode 100644 index 00000000..623d02e8 --- /dev/null +++ b/public/images/reject.png diff --git a/src-migrate/constants/menu.ts b/src-migrate/constants/menu.ts index d1adebca..e3e7b0c6 100644 --- a/src-migrate/constants/menu.ts +++ b/src-migrate/constants/menu.ts @@ -2,6 +2,9 @@ import { SecondaryNavItemProps } from '~/types/nav'; export const SECONDARY_MENU_ITEMS: SecondaryNavItemProps[] = [ { + label: 'Semua Promo', + href: '/shop/promo', + },{ label: 'Semua Brand', href: '/shop/brands', }, diff --git a/src-migrate/hooks/useUtmSource.ts b/src-migrate/hooks/useUtmSource.ts index a72fae36..43fbdcae 100644 --- a/src-migrate/hooks/useUtmSource.ts +++ b/src-migrate/hooks/useUtmSource.ts @@ -7,7 +7,7 @@ const useUtmSource = () => { const [source, setSource] = useState<string>(); useEffect(() => { - console.log(router.pathname); + // console.log(router.pathname); if (router.pathname) { setSource(UTM_SOURCE[router.pathname as keyof typeof UTM_SOURCE]); diff --git a/src-migrate/modules/cart/components/Item.tsx b/src-migrate/modules/cart/components/Item.tsx index 6ded6373..47893498 100644 --- a/src-migrate/modules/cart/components/Item.tsx +++ b/src-migrate/modules/cart/components/Item.tsx @@ -17,12 +17,14 @@ import CartItemSelect from './ItemSelect' type Props = { item: CartItemProps editable?: boolean + pilihSemuaCart?: boolean } -const CartItem = ({ item, editable = true }: Props) => { +const CartItem = ({ item, editable = true,}: Props) => { + return ( <div className={style.wrapper}> - {item.cart_type === 'promotion' && ( + {item.cart_type === 'promotion' && ( <div className={style.header}> {item.promotion_type?.value && ( <Tooltip label={PROMO_CATEGORY[item.promotion_type?.value].description} placement="top" bgColor='red.600' p={2} rounded={6}> @@ -43,7 +45,9 @@ const CartItem = ({ item, editable = true }: Props) => { )} <div className={style.mainProdWrapper}> - {editable && <CartItemSelect item={item} />} + {editable && ( + <CartItemSelect item={item} /> + )} <div className='w-4' /> <CartItem.Image item={item} /> @@ -87,7 +91,6 @@ const CartItem = ({ item, editable = true }: Props) => { {!editable && <div className={style.quantity}>{item.quantity}</div>} </div> </div> - </div> <div className="flex flex-col"> @@ -100,13 +103,14 @@ const CartItem = ({ item, editable = true }: Props) => { CartItem.Image = function CartItemImage({ item }: { item: CartItemProps }) { const image = item?.image || item?.parent?.image + const imageProgram = item?.image_program ? item.image_program[0] : item?.parent?.image; - return ( + return ( <> {item.cart_type === 'promotion' && ( <div className={style.image}> - {image && <Image src={image} alt={item.name} width={128} height={128} />} - {!image && <div className={style.noImage}>No Image</div>} + {imageProgram && <Image src={imageProgram} alt={item.name} width={128} height={128} />} + {!imageProgram && <div className={style.noImage}>No Image</div>} </div> )} @@ -153,4 +157,4 @@ CartItem.Skeleton = function CartItemSkeleton(props: SkeletonProps & { count: nu )) } -export default CartItem
\ No newline at end of file +export default CartItem diff --git a/src-migrate/modules/cart/components/ItemSelect.tsx b/src-migrate/modules/cart/components/ItemSelect.tsx index b904a1de..2faf5172 100644 --- a/src-migrate/modules/cart/components/ItemSelect.tsx +++ b/src-migrate/modules/cart/components/ItemSelect.tsx @@ -13,23 +13,23 @@ type Props = { const CartItemSelect = ({ item }: Props) => { const auth = getAuth() - const { loadCart } = useCartStore() + const { updateCartItem, cart } = useCartStore() const [isLoad, setIsLoad] = useState<boolean>(false) const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => { - if (typeof auth !== 'object') return - - setIsLoad(true) - await upsertUserCart({ - userId: auth.id, - type: item.cart_type, - id: item.id, - qty: item.quantity, - selected: e.target.checked - }) - await loadCart(auth.id) - setIsLoad(false) + if (typeof auth !== 'object' || !cart) return + + setIsLoad(true); + const updatedCartItems = cart.products.map(cartItem => + cartItem.id === item.id + ? { ...cartItem, selected: e.target.checked } + : cartItem + ); + // Update the entire cart + const updatedCart = { ...cart, products: updatedCartItems }; + updateCartItem(updatedCart); + setIsLoad(false); } return ( diff --git a/src-migrate/modules/cart/stores/useCartStore.ts b/src-migrate/modules/cart/stores/useCartStore.ts index 3d9a0aed..ae551846 100644 --- a/src-migrate/modules/cart/stores/useCartStore.ts +++ b/src-migrate/modules/cart/stores/useCartStore.ts @@ -1,5 +1,5 @@ import { create } from 'zustand'; -import { CartProps } from '~/types/cart'; +import { CartItem, CartProps } from '~/types/cart'; import { getUserCart } from '~/services/cart'; type State = { @@ -16,6 +16,7 @@ type State = { type Action = { loadCart: (userId: number) => Promise<void>; + updateCartItem: (updateCart: CartProps) => void; }; export const useCartStore = create<State & Action>((set, get) => ({ @@ -39,6 +40,14 @@ export const useCartStore = create<State & Action>((set, get) => ({ const summary = computeSummary(cart); set({ summary }); }, + updateCartItem: (updatedCart) => { + const cart = get().cart; + if (!cart) return; + + set({ cart: updatedCart }); + const summary = computeSummary(updatedCart); + set({ summary }); + }, })); const computeSummary = (cart: CartProps) => { diff --git a/src-migrate/modules/footer-banner/index.tsx b/src-migrate/modules/footer-banner/index.tsx index b214493d..86321815 100644 --- a/src-migrate/modules/footer-banner/index.tsx +++ b/src-migrate/modules/footer-banner/index.tsx @@ -10,7 +10,7 @@ const FooterBanner = () => { queryKey: 'footerBanner', queryFn: () => getBanner({ type: 'bottom-search-promotion' }) }) - // ubah dari static menjadid dynamic dengan menggunakan random index + const length = useMemo(() => fetchFooterBanner.data?.length, [fetchFooterBanner.data]); const randomIndex = useMemo(() => getRandomInt(length), [length]); const banner = fetchFooterBanner?.data?.[randomIndex] || false; diff --git a/src-migrate/modules/product-detail/components/AddToCart.tsx b/src-migrate/modules/product-detail/components/AddToCart.tsx index 097db98a..a5284637 100644 --- a/src-migrate/modules/product-detail/components/AddToCart.tsx +++ b/src-migrate/modules/product-detail/components/AddToCart.tsx @@ -1,19 +1,35 @@ -import { Button, useToast } from '@chakra-ui/react' +import BottomPopup from '@/core/components/elements/Popup/BottomPopup' +import style from '../styles/price-action.module.css'; +import { Button, Link, useToast } from '@chakra-ui/react' +import product from 'next-seo/lib/jsonld/product' import { useRouter } from 'next/router' - +import { useEffect, useState } from 'react' +import Image from '~/components/ui/image' import { getAuth } from '~/libs/auth' import { upsertUserCart } from '~/services/cart' +import LazyLoad from 'react-lazy-load'; +import ProductSimilar from '../../../../src/lib/product/components/ProductSimilar'; +import { IProductDetail } from '~/types/product'; +import ImageNext from 'next/image'; +import { useProductCartContext } from '@/contexts/ProductCartContext' +import { createSlug } from '~/libs/slug' +import formatCurrency from '~/libs/formatCurrency' +import { useProductDetail } from '../stores/useProductDetail'; type Props = { variantId: number | null, quantity?: number; source?: 'buy' | 'add_to_cart'; + products : IProductDetail } +type Status = 'idle' | 'loading' | 'success' + const AddToCart = ({ variantId, quantity = 1, - source = 'add_to_cart' + source = 'add_to_cart', + products }: Props) => { const auth = getAuth() const router = useRouter() @@ -22,40 +38,65 @@ const AddToCart = ({ isClosable: true }) - const handleClick = async () => { + const { + askAdminUrl, + } = useProductDetail(); + + const [product, setProducts] = useState(products); + const [status, setStatus] = useState<Status>('idle') + const { productCart, setRefreshCart, setProductCart, refreshCart, isLoading, setIsloading } = + useProductCartContext() + + const productSimilarQuery = [ + product?.name, + `fq=-product_id_i:${product.id}`, + `fq=-manufacture_id_i:${product.manufacture?.id || 0}`, + ].join('&'); + const [addCartAlert, setAddCartAlert] = useState(false); + + const handleButton = async () => { if (typeof auth !== 'object') { const currentUrl = encodeURIComponent(router.asPath) router.push(`/login?next=${currentUrl}`) return; } - + if ( !variantId || isNaN(quantity) || typeof auth !== 'object' ) return; - - toast.promise( - upsertUserCart({ - userId: auth.id, + if (status === 'success') return + setStatus('loading') + await upsertUserCart({ + userId: auth.id, type: 'product', id: variantId, qty: quantity, selected: true, source: source, qtyAppend: true - }), - { - loading: { title: 'Menambahkan ke keranjang', description: 'Mohon tunggu...' }, - success: { title: 'Menambahkan ke keranjang', description: 'Berhasil menambahkan ke keranjang belanja' }, - error: { title: 'Menambahkan ke keranjang', description: 'Gagal menambahkan ke keranjang belanja' }, - } - ) - + }) + setStatus('idle') + setRefreshCart(true); + setAddCartAlert(true); + + toast({ + title: 'Tambah ke keranjang', + description: 'Berhasil menambahkan barang ke keranjang belanja', + status: 'success', + duration: 3000, + isClosable: true, + position: 'top', + }) + if (source === 'buy') { router.push('/shop/checkout?source=buy') } } + useEffect(() => { + if (status === 'success') setTimeout(() => { setStatus('idle') }, 3000) + }, [status]) const btnConfig = { 'add_to_cart': { @@ -69,10 +110,98 @@ const AddToCart = ({ } return ( - <Button onClick={handleClick} colorScheme={btnConfig[source].colorScheme} className='w-full'> - {btnConfig[source].text} - </Button> + <div className='w-full'> + <Button onClick={handleButton} colorScheme={btnConfig[source].colorScheme} className='w-full'> + {btnConfig[source].text} + </Button> + <BottomPopup + className='!container' + title='Berhasil Ditambahkan' + active={addCartAlert} + close={() => { + setAddCartAlert(false); + }} + > + <div className='flex mt-4'> + <div className='w-[10%]'> + <ImageNext + src={product.image} + alt={product.name} + className='h-32 object-contain object-center w-full border border-gray_r-4' + width={80} + height={80} + /> + </div> + <div className='ml-3 flex flex-1 items-start font-medium justify-center flex-col gap-y-1'> + {!!product.manufacture.name ? ( + <Link + href={createSlug('/shop/brands/', product.manufacture.name, product.manufacture.id.toString())} + className=' hover:underline' + color={"red"} + > + {product.manufacture.name} + </Link> + ) : '-'} + <p className='text-ellipsis overflow-hidden'> + {product.name} + </p> + <p> + {product.code} + </p> + {!!product.lowest_price && product.lowest_price.price > 0 && ( + <> + <div className='flex items-end gap-x-2'> + {product.lowest_price.discount_percentage > 0 && ( + <> + <div className='badge-solid-red'> + {Math.floor(product.lowest_price.discount_percentage)}% + </div> + <div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'> + Rp {formatCurrency(product.lowest_price.price || 0)} + </div> + </> + )} + <div className='text-danger-500 font-semibold'> + Rp {formatCurrency(product.lowest_price.price_discount || 0)} + </div> + </div> + </> + )} + + {!!product.lowest_price && product.lowest_price.price === 0 && ( + <span> + Hubungi kami untuk dapatkan harga terbaik,{' '} + <Link + href={askAdminUrl} + target='_blank' + className='font-medium underline' + color={'red'} + > + klik disini + </Link> + </span> + )} + </div> + <div className='ml-3 flex items-center font-normal'> + <Link + href='/shop/cart' + className='flex-1 py-2 text-gray_r-12 btn-yellow' + > + Lihat Keranjang + </Link> + </div> + </div> + <div className='mt-8 mb-4'> + <div className='text-h-sm font-semibold mb-6'> + Kamu Mungkin Juga Suka + </div> + <LazyLoad> + <ProductSimilar query={productSimilarQuery} /> + </LazyLoad> + </div> + </BottomPopup> + </div> ) } -export default AddToCart
\ No newline at end of file +export default AddToCart
\ No newline at end of file diff --git a/src-migrate/modules/product-detail/components/PriceAction.tsx b/src-migrate/modules/product-detail/components/PriceAction.tsx index 81271f6e..9021264e 100644 --- a/src-migrate/modules/product-detail/components/PriceAction.tsx +++ b/src-migrate/modules/product-detail/components/PriceAction.tsx @@ -97,12 +97,14 @@ const PriceAction = ({ product }: Props) => { className={style['quantity-input']} /> <AddToCart + products={product} variantId={activeVariantId} quantity={Number(quantityInput)} /> {!isApproval && ( <AddToCart source='buy' + products={product} variantId={activeVariantId} quantity={Number(quantityInput)} /> diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx index de0522d5..169e1903 100644 --- a/src-migrate/modules/product-detail/components/ProductDetail.tsx +++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx @@ -129,7 +129,7 @@ const ProductDetail = ({ product }: Props) => { )} <div className='h-4 md:h-10' /> - {!!activeVariantId && !isApproval && <ProductPromoSection productId={activeVariantId} />} + {!!activeVariantId && !isApproval && <ProductPromoSection product={product} productId={activeVariantId} />} <div className={style['section-card']}> <h2 className={style['heading']}> diff --git a/src-migrate/modules/product-promo/components/AddToCart.tsx b/src-migrate/modules/product-promo/components/AddToCart.tsx index 87017c14..3d0955e3 100644 --- a/src-migrate/modules/product-promo/components/AddToCart.tsx +++ b/src-migrate/modules/product-promo/components/AddToCart.tsx @@ -9,19 +9,39 @@ import { IPromotion } from '~/types/promotion' import DesktopView from '../../../../src/core/components/views/DesktopView'; import MobileView from '../../../../src/core/components/views/MobileView'; - +import BottomPopup from '@/core/components/elements/Popup/BottomPopup' +import ImageNext from 'next/image'; +import Link from 'next/link' +import LazyLoad from 'react-lazy-load' +import ProductSimilar from '../../../../src/lib/product/components/ProductSimilar'; +import { IProductDetail } from '~/types/product'; +import { useProductCartContext } from '@/contexts/ProductCartContext' +import { createSlug } from '~/libs/slug' +import formatCurrency from '~/libs/formatCurrency' +import { useProductDetail } from '../../product-detail/stores/useProductDetail'; type Props = { promotion: IPromotion + product: IProductDetail } type Status = 'idle' | 'loading' | 'success' -const ProductPromoAddToCart = ({ promotion }: Props) => { +const ProductPromoAddToCart = ({product, promotion }: Props) => { + const auth = getAuth() const toast = useToast() const router = useRouter() + const {askAdminUrl} = useProductDetail(); const [status, setStatus] = useState<Status>('idle') + const { productCart, setRefreshCart, setProductCart, refreshCart, isLoading, setIsloading } = + useProductCartContext() + + const productSimilarQuery = [ + promotion?.name, + `fq=-product_id_i:${promotion.products[0].product_id}`, + ].join('&'); + const [addCartAlert, setAddCartAlert] = useState(false); const handleButton = async () => { if (typeof auth !== 'object') { @@ -42,7 +62,8 @@ const ProductPromoAddToCart = ({ promotion }: Props) => { qtyAppend: true }) setStatus('idle') - + setRefreshCart(true); + setAddCartAlert(true); toast({ title: 'Tambah ke keranjang', description: 'Berhasil menambahkan barang ke keranjang belanja', @@ -92,6 +113,92 @@ const ProductPromoAddToCart = ({ promotion }: Props) => { {status === 'success' && <span>Berhasil</span>} {status !== 'success' && <span>Keranjang</span>} </Button> + <BottomPopup + className='!container' + title='Berhasil Ditambahkan' + active={addCartAlert} + close={() => { + setAddCartAlert(false); + }} + > + <div className='flex mt-4'> + <div className='w-[10%]'> + <ImageNext + src={product.image} + alt={product.name} + className='h-32 object-contain object-center w-full border border-gray_r-4' + width={80} + height={80} + /> + </div> + <div className='ml-3 flex flex-1 items-start font-medium justify-center flex-col gap-y-1'> + {!!product?.manufacture?.name ? ( + <Link + href={createSlug('/shop/brands/', product.manufacture.name, product.manufacture.id.toString())} + className=' hover:underline' + color={"red"} + > + {product.manufacture.name} + </Link> + ) : '-'} + <p className='text-ellipsis overflow-hidden'> + {product.name} + </p> + <p> + {product.code} + </p> + {!!product.lowest_price && product.lowest_price.price > 0 && ( + <> + <div className='flex items-end gap-x-2'> + {product.lowest_price.discount_percentage > 0 && ( + <> + <div className='badge-solid-red'> + {Math.floor(product.lowest_price.discount_percentage)}% + </div> + <div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'> + Rp {formatCurrency(product.lowest_price.price || 0)} + </div> + </> + )} + <div className='text-danger-500 font-semibold'> + Rp {formatCurrency(product.lowest_price.price_discount || 0)} + </div> + </div> + </> + )} + + {!!product.lowest_price && product.lowest_price.price === 0 && ( + <span> + Hubungi kami untuk dapatkan harga terbaik,{' '} + <Link + href={askAdminUrl} + target='_blank' + className='font-medium underline' + color={'red'} + > + klik disini + </Link> + </span> + )} + </div> + <div className='ml-3 flex items-center font-normal'> + <Link + href='/shop/cart' + className='flex-1 py-2 text-gray_r-12 btn-yellow' + > + Lihat Keranjang + </Link> + </div> + </div> + <div className='mt-8 mb-4'> + <div className='text-h-sm font-semibold mb-6'> + Kamu Mungkin Juga Suka + </div> + <LazyLoad> + <ProductSimilar query={productSimilarQuery} /> + </LazyLoad> + </div> + </BottomPopup> </DesktopView> </div> ) diff --git a/src-migrate/modules/product-promo/components/Card.tsx b/src-migrate/modules/product-promo/components/Card.tsx index 56e29e38..5c323276 100644 --- a/src-migrate/modules/product-promo/components/Card.tsx +++ b/src-migrate/modules/product-promo/components/Card.tsx @@ -15,16 +15,16 @@ import clsxm from '~/libs/clsxm' import ProductPromoItem from './Item' import ProductPromoAddToCart from "./AddToCart" import ProductPromoCardCountdown from "./CardCountdown" - +import { IProductDetail } from '~/types/product'; import MobileView from '../../../../src/core/components/views/MobileView'; import DesktopView from '../../../../src/core/components/views/DesktopView'; type Props = { promotion: IPromotion - + product: IProductDetail } -const ProductPromoCard = ({ promotion}: Props) => { +const ProductPromoCard = ({product, promotion}: Props) => { const [products, setProducts] = useState<IProductVariantPromo[]>([]) const [freeProducts, setFreeProducts] = useState<IProductVariantPromo[]>([]) const [error, setError] = useState<string | null>(null) @@ -90,10 +90,10 @@ const ProductPromoCard = ({ promotion}: Props) => { <div className={style.title}>{promotion.name}</div> <Tooltip label={PROMO_CATEGORY[promotion.type.value].description} placement="top" bgColor='red.600' p={1} rounded={6}> - {/* <div className={style.badgeType} > */} - {/* Paket {PROMO_CATEGORY[promotion.type.value].alias} */} + <div className={style.badgeType} > + Paket {PROMO_CATEGORY[promotion.type.value].alias} <InfoIcon className={style.badgeType} size={25} /> - {/* </div> */} + </div> </Tooltip> </div> @@ -132,7 +132,7 @@ const ProductPromoCard = ({ promotion}: Props) => { </div> <div> - <ProductPromoAddToCart promotion={promotion} /> + <ProductPromoAddToCart product={product} promotion={promotion} /> </div> </div> @@ -189,7 +189,7 @@ const ProductPromoCard = ({ promotion}: Props) => { </div> </div> <div> - <ProductPromoAddToCart promotion={promotion} /> + <ProductPromoAddToCart product={product} promotion={promotion} /> </div> </div> diff --git a/src-migrate/modules/product-promo/components/Modal.tsx b/src-migrate/modules/product-promo/components/Modal.tsx index 0de672c2..1722b9df 100644 --- a/src-migrate/modules/product-promo/components/Modal.tsx +++ b/src-migrate/modules/product-promo/components/Modal.tsx @@ -3,8 +3,12 @@ import { Modal } from "~/components/ui/modal" import { useModalStore } from '../stores/useModalStore' import ProductPromoCategoryTab from './CategoryTab' import ProductPromoModalContent from './ModalContent' +import { IProductDetail } from '~/types/product'; -const ProductPromoModal = () => { +type Props = { + product: IProductDetail +} +const ProductPromoModal = ({product}:Props) => { const { active, closeModal } = useModalStore() return ( @@ -17,7 +21,7 @@ const ProductPromoModal = () => { <div className='h-4' /> - <ProductPromoModalContent /> + <ProductPromoModalContent product={product} /> </Modal> ) } diff --git a/src-migrate/modules/product-promo/components/ModalContent.tsx b/src-migrate/modules/product-promo/components/ModalContent.tsx index ab5129f8..256ef61a 100644 --- a/src-migrate/modules/product-promo/components/ModalContent.tsx +++ b/src-migrate/modules/product-promo/components/ModalContent.tsx @@ -6,7 +6,11 @@ import { getVariantPromoByCategory } from "~/services/productVariant" import { useModalStore } from "../stores/useModalStore" import ProductPromoCard from "./Card" -const ProductPromoModalContent = () => { +import { IProductDetail } from '~/types/product'; +type Props = { + product: IProductDetail +} +const ProductPromoModalContent = ({product}:Props) => { const { activeTab, variantId } = useModalStore() const promotionsQuery = useQuery( @@ -24,7 +28,7 @@ const ProductPromoModalContent = () => { <Skeleton isLoaded={!promotionsQuery.isLoading} className='min-h-[70vh] max-h-[70vh]'> <div className="grid grid-cols-1 gap-y-6 pb-6"> {promotions?.data.map((promo) => ( - <ProductPromoCard key={promo.id} promotion={promo} /> + <ProductPromoCard key={promo.id} promotion={promo} product={product} /> ))} {promotions?.data.length === 0 && ( <div className="py-10 rounded-lg h-fit text-center text-body-1 font-semibold text-gray-800 bg-gray-200">Belum ada promo pada kategori ini</div> diff --git a/src-migrate/modules/product-promo/components/Section.tsx b/src-migrate/modules/product-promo/components/Section.tsx index 4e8a7dd5..e1719998 100644 --- a/src-migrate/modules/product-promo/components/Section.tsx +++ b/src-migrate/modules/product-promo/components/Section.tsx @@ -9,12 +9,14 @@ import { IPromotion } from '~/types/promotion' import { useModalStore } from "../stores/useModalStore" import ProductPromoCard from './Card' import ProductPromoModal from "./Modal" +import { IProductDetail } from '~/types/product'; type Props = { productId: number; + product: IProductDetail; } -const ProductPromoSection = ({ productId }: Props) => { +const ProductPromoSection = ({ product, productId }: Props) => { const promotionsQuery = useQuery({ queryKey: [`promotions.highlight`, productId], queryFn: async () => await fetch(`/api/product-variant/${productId}/promotion/highlight`).then((res) => res.json()) as { data: IPromotion[] } @@ -23,14 +25,13 @@ const ProductPromoSection = ({ productId }: Props) => { const promotions = promotionsQuery.data const { openModal } = useModalStore() - return ( <SmoothRender isLoaded={(promotions?.data && promotions?.data.length > 0) || false} height='450px' duration='700ms' > - <ProductPromoModal /> + <ProductPromoModal product={product}/> {promotions?.data && promotions?.data.length > 0 && ( <div className={style.titleWrapper}> @@ -50,7 +51,7 @@ const ProductPromoSection = ({ productId }: Props) => { > {promotions?.data.map((promotion) => ( <div key={promotion.id} className="min-w-[400px] max-w-[400px]"> - <ProductPromoCard promotion={promotion} /> + <ProductPromoCard product={product} promotion={promotion} /> </div> ))} </Skeleton> diff --git a/src-migrate/modules/product-promo/styles/card.module.css b/src-migrate/modules/product-promo/styles/card.module.css index faa3b370..4e294f1c 100644 --- a/src-migrate/modules/product-promo/styles/card.module.css +++ b/src-migrate/modules/product-promo/styles/card.module.css @@ -10,7 +10,7 @@ } .badgeType { - @apply p-2 flex gap-x-1.5 rounded-md border border-danger-500 text-danger-500; + @apply p-2 flex gap-x-1.5 rounded-md border border-danger-500 text-danger-500 items-center; } .productSection { diff --git a/src-migrate/modules/promo/components/FlashSale.tsx b/src-migrate/modules/promo/components/FlashSale.tsx new file mode 100644 index 00000000..c0259396 --- /dev/null +++ b/src-migrate/modules/promo/components/FlashSale.tsx @@ -0,0 +1,20 @@ +import dynamic from "next/dynamic"; +import React from "react"; +import { FlashSaleSkeleton } from "@/lib/flashSale/skeleton/FlashSaleSkeleton"; + +const FlashSale = dynamic( + () => import('@/lib/flashSale/components/FlashSale'), + { + loading: () => <FlashSaleSkeleton />, + } + ); + + const FlashSalePromo = ()=> { + return( + <> + <FlashSale/> + </> + ) + } + + export default FlashSalePromo
\ No newline at end of file diff --git a/src-migrate/modules/promo/components/FlashSaleNonDisplay.tsx b/src-migrate/modules/promo/components/FlashSaleNonDisplay.tsx new file mode 100644 index 00000000..1c5cc86d --- /dev/null +++ b/src-migrate/modules/promo/components/FlashSaleNonDisplay.tsx @@ -0,0 +1,20 @@ +import dynamic from "next/dynamic"; +import React from "react"; +import { FlashSaleSkeleton } from "@/lib/flashSale/skeleton/FlashSaleSkeleton"; + +const FlashSaleNonDisplay = dynamic( + () => import('@/lib/flashSale/components/FlashSaleNonDisplay'), + { + loading: () => <FlashSaleSkeleton />, + } + ); + + const FlashSalePromo = ()=> { + return( + <> + <FlashSaleNonDisplay/> + </> + ) + } + + export default FlashSalePromo
\ No newline at end of file diff --git a/src-migrate/modules/promo/components/Hero.tsx b/src-migrate/modules/promo/components/Hero.tsx new file mode 100644 index 00000000..c5f0afad --- /dev/null +++ b/src-migrate/modules/promo/components/Hero.tsx @@ -0,0 +1,105 @@ +import 'swiper/css'; + +import Image from 'next/image'; +import { useEffect, useMemo } from 'react'; +import { useQuery } from 'react-query'; +import { Swiper, SwiperProps, SwiperSlide } from 'swiper/react'; +import style from '../styles/hero.module.css'; +import 'swiper/css/navigation'; +import 'swiper/css/pagination'; +import { Navigation, Pagination, Autoplay } from 'swiper'; +import MobileView from '../../../../src/core/components/views/MobileView'; +import DesktopView from '@/core/components/views/DesktopView'; +import {bannerApi} from '../../../../src/api/bannerApi' + +interface IPromotionProgram { + headlineBanner: string; + descriptionBanner: string; + image: string ; + name: string; +} + +const swiperBanner: SwiperProps = { + modules:[Navigation, Pagination, Autoplay], + autoplay: { + delay: 6000, + disableOnInteraction: false + }, + loop: true, + className: 'h-[400px] w-full', + slidesPerView: 1, + spaceBetween: 10, + pagination:true, +} +const swiperBannerMob = { + autoplay: { + delay: 6000, + disableOnInteraction: false, + }, + modules: [Pagination, Autoplay], + loop: true, + className: 'border border-gray_r-6 min-h-full', + slidesPerView: 1, +}; + +const Hero = () => { + const heroBanner = useQuery('allPromo', bannerApi({ type: 'banner-semua-promo' })); + + const banners: IPromotionProgram[] = useMemo( + () => heroBanner?.data || [], + [heroBanner.data] + ); + + const swiperBannerMobile = { + ...swiperBannerMob, + pagination: { dynamicBullets: false, clickable: true }, + }; + + return ( + <> + <DesktopView> + <div className={style['wrapper']}> + <Swiper {...swiperBanner}> + {banners.map((banner, index) => ( + <SwiperSlide key={index} className='flex flex-row'> + <div className={style['desc-section']}> + <div className={style['title']}>{banner.headlineBanner? banner.headlineBanner : "Pasti Hemat & Untung Selama Belanja di Indoteknik.com!"}</div> + <div className='h-4' /> + <div className={style['subtitle']}>{banner.descriptionBanner? banner.descriptionBanner : "Cari paket yang kami sediakan dengan penawaran harga & Nikmati kemudahan dalam setiap transaksi dengan fitur lengkap Pembayaran hingga barang sampai!"}</div> + </div> + <div className={style['banner-section']}> + <Image + src={banner.image} + alt={banner.name} + width={666} + height={450} + quality={90} + className='w-full h-full object-fit object-center rounded-2xl' /> + </div> + </SwiperSlide> + ))} + </Swiper> + </div> + </DesktopView> + <MobileView> + <Swiper {...swiperBannerMobile}> + {banners?.map((banner, index) => ( + <SwiperSlide key={index}> + <Image + width={439} + height={150} + quality={100} + src={banner.image} + alt={banner.name} + className='w-full h-full object-cover object-center rounded-2xl' + /> + </SwiperSlide> + ))} + </Swiper> + + </MobileView> + </> + ) +} + +export default Hero
\ No newline at end of file diff --git a/src-migrate/modules/promo/components/HeroDiskon.tsx b/src-migrate/modules/promo/components/HeroDiskon.tsx new file mode 100644 index 00000000..6d38c763 --- /dev/null +++ b/src-migrate/modules/promo/components/HeroDiskon.tsx @@ -0,0 +1,137 @@ +import 'swiper/css'; + +import Image from 'next/image'; +import { useEffect, useMemo } from 'react'; +import { useQuery } from 'react-query'; +import { Swiper, SwiperProps, SwiperSlide } from 'swiper/react'; + +import { getBanner } from '~/services/banner'; +import style from '../styles/hero.module.css'; +import 'swiper/css/navigation'; +import { Autoplay, Navigation, Pagination } from 'swiper'; + +const swiperBanner: SwiperProps = { + modules:[Navigation, Pagination, Autoplay], + autoplay: { + delay: 6000, + disableOnInteraction: false + }, + loop: true, + className: 'h-[400px] w-full', + slidesPerView: 1, + spaceBetween: 10, + navigation:true, +} +const swiperBanner2: SwiperProps = { + modules: [Pagination, Autoplay], + autoplay: { + delay: 5000, + }, + loop: true, + className: 'h-[400px] w-full', + slidesPerView: 1, + spaceBetween: 10, +} + +const Hero = () => { + const bannerQuery = useQuery({ + queryKey: ['banner.all-promo'], + queryFn: () => getBanner({ type: 'banner-promotion' }) + }) + + const banners = useMemo(() => bannerQuery.data || [], [bannerQuery.data]); + + useEffect(() => { + if (banners.length > 1) { + swiperBanner.slidesPerView = 1; + swiperBanner.loop = true; + } + }, [banners]); + + return ( + + <div className="grid grid-cols-3 gap-4"> + <div className="row-span-2 h-[446px] flex items-center "> + <Swiper {...swiperBanner}> + {banners.map((banner, index) => ( + <SwiperSlide key={index}> + <Image + src={banner.image} + alt={banner.name} + width={666} + height={480} + className='w-[446px] h-[446px] object-fill object-center rounded-2xl' + /> + </SwiperSlide> + ))} + </Swiper> + </div> + <div className="w-[400px] h-[217px] "> + <Swiper {...swiperBanner2}> + {banners.map((banner, index) => ( + <SwiperSlide key={index}> + <Image + src={banner.image} + alt={banner.name} + width={666} + height={450} + className='w-[400px] h-[217px] object-cover object-center rounded-2xl ' + /> + </SwiperSlide> + ))} + </Swiper> + </div> + <div className="w-[400px] h-[217px]"> + <Swiper {...swiperBanner2}> + {banners.map((banner, index) => ( + <SwiperSlide key={index}> + <Image + src={banner.image} + alt={banner.name} + width={666} + height={450} + className='w-[400px] h-[217px] object-cover object-center rounded-2xl' + /> + </SwiperSlide> + ))} + </Swiper> + </div> + <div className="w-[400px] h-[217px]"> + <Swiper {...swiperBanner2}> + {banners.map((banner, index) => ( + <SwiperSlide key={index}> + <Image + src={banner.image} + alt={banner.name} + width={666} + height={450} + className='w-[400px] h-[217px] object-cover object-center rounded-2xl' + /> + </SwiperSlide> + ))} + </Swiper> + </div> + <div className="w-[400px] h-[217px]"> + <Swiper {...swiperBanner2}> + {banners.map((banner, index) => ( + <SwiperSlide key={index}> + <Image + src={banner.image} + alt={banner.name} + width={666} + height={450} + className='w-[400px] h-[217px] object-cover object-center rounded-2xl' + /> + </SwiperSlide> + ))} + </Swiper> + </div> + + </div> + + + + ) +} + +export default Hero
\ No newline at end of file diff --git a/src-migrate/modules/promo/components/PromoList.tsx b/src-migrate/modules/promo/components/PromoList.tsx new file mode 100644 index 00000000..4d0db3c2 --- /dev/null +++ b/src-migrate/modules/promo/components/PromoList.tsx @@ -0,0 +1,135 @@ +import React, { useEffect, useState } from 'react'; +import { Button, Skeleton } from '@chakra-ui/react' +import clsxm from "~/libs/clsxm" +import ProductPromoCard from '../../product-promo/components/Card'; +import { fetchPromoItemsSolr } from '../../../../src/api/promoApi'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import SwiperCore, { Navigation, Pagination } from 'swiper'; +import useDevice from '@/core/hooks/useDevice'; +import LogoSpinner from '../../../../src/core/components/elements/Spinner/LogoSpinner'; +import usePromoStore from './promoStore'; +import Link from "next/link" +import { IPromotion } from '~/types/promotion'; +interface PromoListProps { + selectedPromo: string; // Tipe selectedPromo ditetapkan sebagai string +} + +const PromoList: React.FC<PromoListProps> = ({ selectedPromo }) => { + const { + title, + slug, + promoItems, + promoData, + isLoading, + setTitle, + setSlug, + setPromoItems, + setPromoData, + setIsLoading, + } = usePromoStore(); + + const { isMobile, isDesktop } = useDevice(); + + const swiperBanner = { + modules: [Navigation], + className: 'h-[400px] w-full', + slidesPerView: isMobile ? 1.1 : 3.25, + spaceBetween: 10, + navigation:isMobile? true : false, + allowTouchMove:isMobile? false : true, + }; + + useEffect(() => { + if (selectedPromo === 'Bundling') { + setTitle('Kombinasi Kilat Pilihan Kami!'); + setSlug('bundling'); + } else if (selectedPromo === 'Loading') { + setTitle('Belanja Borong Pilihan Kami!'); + setSlug('discount_loading'); + } else if (selectedPromo === 'Merchandise') { + setTitle('Gratis Merchandise Spesial Indoteknik'); + setSlug('merchandise'); + } + }, [selectedPromo, setTitle, setSlug]); + + useEffect(() => { + const fetchPromotions = async () => { + setIsLoading(true); + try { + const items = await fetchPromoItemsSolr(`type_value_s:${slug}`, 0, 10); + setPromoItems(items); + + const promoDataPromises = items.map(async (item) => { + try { + const response = await fetchPromoItemsSolr(`id:${item.id}`, 0, 10); + return response; + } catch (fetchError) { + return []; + } + }); + + const promoDataArray = await Promise.all(promoDataPromises); + const mergedPromoData = promoDataArray.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []); + setPromoData(mergedPromoData); + + } catch (error) { + console.error('Error fetching promo items:', error); + } finally { + setIsLoading(false); + } + }; + + if (slug) { + setIsLoading(true); + setPromoItems([]); + setPromoData([]); + fetchPromotions(); + } + }, [slug, setPromoItems, setPromoData, setIsLoading]); + + return ( + <div className='min-h-[360px]'> + <div className='flex justify-between items-center'> + <h1 className='text-h-sm md:text-h-lg font-semibold py-4'>{title}</h1> + <div> + <Link href={`/shop/promo/${slug}`} className='!text-red-500 font-semibold'> + Lihat Semua + </Link> + </div> + </div> + {isLoading ? ( + <div className="loading-spinner flex justify-center"> + <LogoSpinner width={48} height={48} /> + </div> + ) : ( + <Skeleton + isLoaded={!isLoading} + className={clsxm( + "flex gap-x-4 overflow-x-auto px-4 md:px-0", { + "min-h-[340px]": promoData[0] && promoData?.length > 0 + })} + > + {isDesktop && ( + <Swiper {...swiperBanner}> + {promoData?.map((promotion: IPromotion) => ( + <SwiperSlide key={promotion.id}> + <div className="min-w-36 max-w-[400px] mb-[20px] sm:w-full md:w-full lg:w-full xl:w-full"> + <ProductPromoCard product={promoItems} promotion={promotion} /> + </div> + </SwiperSlide> + ))} + </Swiper> + )} + {isMobile && (promoData?.map((promotion: IPromotion) => ( + <div key={promotion.id} className="min-w-[400px] max-w-[400px]"> + <ProductPromoCard product={promoItems} promotion={promotion} /> + </div> + )))} + + </Skeleton> + )} + </div> + ); +}; + +export default PromoList;
\ No newline at end of file diff --git a/src-migrate/modules/promo/components/PromotinProgram.jsx b/src-migrate/modules/promo/components/PromotinProgram.jsx new file mode 100644 index 00000000..33839944 --- /dev/null +++ b/src-migrate/modules/promo/components/PromotinProgram.jsx @@ -0,0 +1,134 @@ +import React from 'react'; +import Image from 'next/image'; +import { InfoIcon } from "lucide-react"; +import MobileView from '../../../../src/core/components/views/MobileView'; +import DesktopView from '@/core/components/views/DesktopView'; +import { Swiper, SwiperProps, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; +import useDevice from '@/core/hooks/useDevice'; + +const PromotionProgram = ({ selectedPromo, onSelectPromo }) => { + const { isMobile } = useDevice(); + return ( + <> + <div className="text-h-sm md:text-h-lg font-semibold py-4">Serba Serbi Promo</div> + <div className='px-4 sm:px-0'> + {/* <div className='w-full h-full '> + <div + onClick={() => onSelectPromo('Diskon')} + className={`border p-2 flex items-center gap-x-2 rounded-lg cursor-pointer ${selectedPromo === 'Diskon' ? 'bg-red-50 border-red-500 text-red-500' : 'border-gray-200 text-gray-900'}`} + > + <div> + <Image + width={24} + height={24} + quality={100} + src='/images/icon_promo/diskon.svg' + alt='' + className='h-12 w-12 rounded' + /> + </div> + <div> + <div className='flex w-full flex-row items-center justify-start'> + <h1 className={`mr-1 font-semibold text-base ${selectedPromo === 'Diskon' ? 'text-red-500' : 'text-gray-900'}`}>Spesial Diskon</h1> + <InfoIcon className='mt-[1px] text-red-500' size={14} /> + </div> + <p className={`text-xs md:text-sm ${selectedPromo === 'Diskon' ? 'text-red-500' : 'text-gray-500'}`}> + Harga lebih murah dan pasti makin hemat belinya.. + </p> + </div> + </div> + </div> */} + + <Swiper slidesPerView={isMobile ? 1.3 : 3} spaceBetween={10}> + <SwiperSlide> + <div className='w-full h-full '> + <div + onClick={() => onSelectPromo('Bundling')} + className={`border h-full p-1 flex items-center gap-x-2 rounded-lg cursor-pointer ${selectedPromo === 'Bundling' ? 'bg-red-50 border-red-500 text-red-500' : 'border-gray-200 text-gray-900'}`} + > + <div> + <Image + width={24} + height={24} + quality={100} + src='/images/icon_promo/silat.svg' + alt='' + className='h-12 w-12 rounded' + /> + </div> + <div > + <div className='flex w-full flex-row items-center justify-start'> + <h1 className={`mr-1 font-semibold text-base ${selectedPromo === 'Bundling' ? 'text-red-500' : 'text-gray-900'}`}>Paket Silat</h1> + <InfoIcon className='mt-[1px] text-red-500' size={14} /> + </div> + <p className={`text-xs md:text-sm ${selectedPromo === 'Bundling' ? 'text-red-500' : 'text-gray-500'}`}> + Pilihan bundling barang kombinasi Silat. + </p> + </div> + </div> + </div> + </SwiperSlide> + <SwiperSlide> + <div className='w-full h-full '> + <div + onClick={() => onSelectPromo('Loading')} + className={`border p-2 h-full flex items-center gap-x-2 rounded-lg cursor-pointer ${selectedPromo === 'Loading' ? 'bg-red-50 border-red-500 text-red-500' : 'border-gray-200 text-gray-900'}`} + > + <div> + <Image + width={24} + height={24} + quality={100} + src='/images/icon_promo/barong.svg' + alt='' + className='h-12 w-12 rounded' + /> + </div> + <div> + <div className='flex w-full flex-row items-center justify-start'> + <h1 className={`mr-1 font-semibold text-base ${selectedPromo === 'Loading' ? 'text-red-500' : 'text-gray-900'}`}>Paket Barong</h1> + <InfoIcon className='mt-[1px] text-red-500' size={14} /> + </div> + <p className={`text-xs md:text-sm ${selectedPromo === 'Loading' ? 'text-red-500' : 'text-gray-500'}`}> + Beli banyak barang/partai barang borong. + </p> + </div> + </div> + </div> + </SwiperSlide> + <SwiperSlide> + <div className='w-full h-full '> + <div + onClick={() => onSelectPromo('Merchandise')} + className={`border p-2 h-full flex items-center gap-x-2 rounded-lg cursor-pointer ${selectedPromo === 'Merchandise' ? 'bg-red-50 border-red-500 text-red-500' : 'border-gray-200 text-gray-900'}`} + > + <div> + <Image + width={24} + height={24} + quality={100} + src='/images/icon_promo/angklung.svg' + alt='' + className='h-12 w-12 rounded' + /> + </div> + <div > + <div className='flex w-full flex-row items-center justify-start '> + <h1 className={`mr-1 font-semibold text-base ${selectedPromo === 'Merchandise' ? 'text-red-500' : 'text-gray-900'}`}>Paket Angklung</h1> + <InfoIcon className='mt-[1px] text-red-500' size={14} /> + </div> + <p className={` m1 text-xs md:text-sm ${selectedPromo === 'Merchandise' ? 'text-red-500' : 'text-gray-500'}`}> + Gratis barang promosi/merchandise menang langsung. + </p> + </div> + </div> + </div> + </SwiperSlide> + </Swiper> + </div> + </> + ); +}; + +export default PromotionProgram; diff --git a/src-migrate/modules/promo/components/Voucher.tsx b/src-migrate/modules/promo/components/Voucher.tsx new file mode 100644 index 00000000..e5877e51 --- /dev/null +++ b/src-migrate/modules/promo/components/Voucher.tsx @@ -0,0 +1,160 @@ +import { useMemo, useState, useEffect } from 'react'; +import { useQuery } from 'react-query'; +import { Swiper, SwiperProps, SwiperSlide } from 'swiper/react'; +import { getVoucherAll } from '~/services/voucher'; +import style from '../styles/voucher.module.css'; +import Image from 'next/image'; +import { useToast } from '@chakra-ui/react'; +import useDevice from '@/core/hooks/useDevice'; +import useAuth from '@/core/hooks/useAuth'; +import { getVoucher } from '../../../../src/lib/checkout/api/getVoucher'; + +interface Auth { + id: string; +} +interface Voucher { + id: string; + image: string; + name: string; + description: string; + code: string; +} + +const VoucherComponent = () => { + const [listVouchers, setListVouchers] = useState<Voucher[] | null>(null); + const [loadingVoucher, setLoadingVoucher] = useState(true); + const { isMobile } = useDevice(); + const auth = useAuth() as unknown as Auth; + const toast = useToast(); + + useEffect(() => { + if (!listVouchers && auth?.id) { + (async () => { + try { + const dataVoucher = await getVoucher(auth.id); + setListVouchers(dataVoucher); + } finally { + setLoadingVoucher(false); + } + })(); + } + }, [auth?.id, listVouchers]); + + const voucherQuery = useQuery({ + queryKey: ['voucher.all-voucher'], + queryFn: getVoucherAll, + }); + + const swiperVoucher: SwiperProps = { + autoplay: { + delay: 6000, + disableOnInteraction: false, + }, + loop: false, + className: 'h-[160px] w-full', + slidesPerView: isMobile ? 1.2 : 3.2, + spaceBetween: 2, + }; + + const dataVouchers = useMemo(() => voucherQuery.data || [], [voucherQuery.data]); + + const vouchers = auth?.id? listVouchers : dataVouchers; + + + const copyText = (text: string) => { + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text) + .then(() => { + toast({ + title: 'Salin ke papan klip', + description: 'Kode voucher berhasil disalin', + status: 'success', + duration: 3000, + isClosable: true, + position: 'top', + }) + }) + .catch(() => { + fallbackCopyTextToClipboard(text); + }); + } else { + fallbackCopyTextToClipboard(text); + } + } + + const fallbackCopyTextToClipboard = (text: string) => { + const textArea = document.createElement("textarea"); + textArea.value = text; + // Tambahkan style untuk menyembunyikan textArea secara visual + textArea.style.position = 'fixed'; + textArea.style.top = '0'; + textArea.style.left = '0'; + textArea.style.width = '2em'; + textArea.style.height = '2em'; + textArea.style.padding = '0'; + textArea.style.border = 'none'; + textArea.style.outline = 'none'; + textArea.style.boxShadow = 'none'; + textArea.style.background = 'transparent'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + try { + document.execCommand('copy'); + toast({ + title: 'Salin ke papan klip', + description: 'Kode voucher berhasil disalin', + status: 'success', + duration: 3000, + isClosable: true, + position: 'top', + }) + } catch (err) { + console.error('Fallback: Oops, unable to copy', err); + } + document.body.removeChild(textArea); + } + + return ( + <> + <div className={style['title']}>Pakai Voucher Belanja</div> + + <div className='h-6' /> + + {voucherQuery.isLoading && ( + <div className='grid grid-cols-3 gap-x-4 animate-pulse'> + {Array.from({ length: 3 }).map((_, index) => ( + <div key={index} className='w-full h-[160px] bg-gray-200 rounded-xl' /> + ))} + </div> + )} + {!voucherQuery.isLoading && ( + <div className={style['voucher-section']}> + <Swiper {...swiperVoucher}> + {vouchers?.map((voucher) => ( + <SwiperSlide key={voucher.id} className='pb-2'> + <div className={style['voucher-card']}> + <Image src={voucher.image} alt={voucher.name} width={128} height={128} className={style['voucher-image']} /> + + <div className={style['voucher-content']}> + <div className={style['voucher-title']}>{voucher.name}</div> + <div className={style['voucher-desc']}>{voucher.description}</div> + <div className={style['voucher-bottom']}> + <div> + <div className={style['voucher-code-desc']}>Kode Promo</div> + <div className={style['voucher-code']}>{voucher.code}</div> + </div> + <button className={style['voucher-copy']} onClick={() => copyText(voucher.code)}>Salin</button> + </div> + </div> + </div> + </SwiperSlide> + ))} + </Swiper> + </div> + )} + </> + ) +} + +export default VoucherComponent diff --git a/src-migrate/modules/promo/components/promoStore.js b/src-migrate/modules/promo/components/promoStore.js new file mode 100644 index 00000000..c232de00 --- /dev/null +++ b/src-migrate/modules/promo/components/promoStore.js @@ -0,0 +1,16 @@ +import create from 'zustand'; + +const usePromoStore = create((set) => ({ + title: '', + slug: '', + promoItems: [], + promoData: [], + isLoading: true, + setTitle: (title) => set({ title }), + setSlug: (slug) => set({ slug }), + setPromoItems: (promoItems) => set({ promoItems }), + setPromoData: (promoData) => set({ promoData }), + setIsLoading: (isLoading) => set({ isLoading }), +})); + +export default usePromoStore; diff --git a/src-migrate/modules/promo/styles/hero.module.css b/src-migrate/modules/promo/styles/hero.module.css new file mode 100644 index 00000000..a5ba6ecc --- /dev/null +++ b/src-migrate/modules/promo/styles/hero.module.css @@ -0,0 +1,27 @@ +.wrapper { + @apply rounded-xl w-full h-[460px] flex; +} + +.desc-section { + @apply w-full md:w-5/12 + flex flex-col + md:justify-center + p-6 md:pl-10; +} + +.title { + @apply text-title-sm md:text-title-lg + leading-[30px] md:leading-[42px] + font-semibold; +} + +.subtitle { + @apply text-body-2 leading-7 text-gray-700; +} + +.banner-section { + @apply md:w-7/12 + flex flex-col + md:justify-center + md:pr-10; +} diff --git a/src-migrate/modules/promo/styles/voucher.module.css b/src-migrate/modules/promo/styles/voucher.module.css new file mode 100644 index 00000000..22d07f91 --- /dev/null +++ b/src-migrate/modules/promo/styles/voucher.module.css @@ -0,0 +1,43 @@ +.title { + @apply text-h-sm md:text-h-lg font-semibold; +} + +.voucher-section { + @apply w-full; +} + +.voucher-card { + @apply w-full md:w-11/12 h-3/4 rounded-xl border items-center border-gray-200 shadow-md p-4 flex gap-x-4 ; +} + +.voucher-image { + @apply bg-gray-100 rounded-lg w-4/12 h-fit object-contain object-center; +} + +.voucher-content { + @apply flex-1 flex flex-col; +} + +.voucher-title { + @apply font-medium text-body-1 leading-6 mb-1; +} + +.voucher-desc { + @apply text-gray-800 line-clamp-2 text-caption-1; +} + +.voucher-bottom { + @apply flex justify-between mt-2; +} + +.voucher-code-desc { + @apply text-gray-500 text-caption-1; +} + +.voucher-code { + @apply text-red-700 font-medium; +} + +.voucher-copy { + @apply bg-gray-200 hover:bg-danger-500 text-danger-500 hover:text-white transition-colors rounded-lg flex items-center justify-center px-6; +} diff --git a/src-migrate/modules/side-banner/index.tsx b/src-migrate/modules/side-banner/index.tsx index 6214edfb..878b8e70 100644 --- a/src-migrate/modules/side-banner/index.tsx +++ b/src-migrate/modules/side-banner/index.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from "react"; -import Link from "next/link"; -import { useQuery } from "react-query"; -import Image from "~/components/ui/image"; +import Link from "next/link";; +import { useQuery } from "react-query";; +import Image from "~/components/ui/image";; import { getBanner } from "~/services/banner"; import { getRandomInt } from '@/utils/getRandomInt'; @@ -10,7 +10,7 @@ const SideBanner = () => { queryKey: 'sideBanner', queryFn: () => getBanner({ type: 'side-banner-search' }) }); - // ubah dari static menjadid dynamic dengan menggunakan random index + const length = useMemo(() => fetchSideBanner.data?.length, [fetchSideBanner.data]); const randomIndex = useMemo(() => getRandomInt(length), [length]); const banner = fetchSideBanner?.data?.[randomIndex] || false; @@ -25,6 +25,6 @@ const SideBanner = () => { <Image src={banner.image} alt={banner.name} width={315} height={450} className='object-cover object-center rounded-lg' /> )} </> - ); + ) } export default SideBanner; diff --git a/src-migrate/pages/shop/cart/index.tsx b/src-migrate/pages/shop/cart/index.tsx index 7de96425..cfb20284 100644 --- a/src-migrate/pages/shop/cart/index.tsx +++ b/src-migrate/pages/shop/cart/index.tsx @@ -1,87 +1,238 @@ import style from './cart.module.css'; -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import Link from 'next/link'; -import { Button, Toast, Tooltip } from '@chakra-ui/react'; +import { Button, Checkbox, Spinner, Tooltip } from '@chakra-ui/react'; import { toast } from 'react-hot-toast'; import { useRouter } from 'next/router'; import { getAuth } from '~/libs/auth'; import { useCartStore } from '~/modules/cart/stores/useCartStore'; -import CartItem from '~/modules/cart/components/Item'; +import CartItemModule from '~/modules/cart/components/Item'; import CartSummary from '~/modules/cart/components/Summary'; import clsxm from '~/libs/clsxm'; import useDevice from '@/core/hooks/useDevice'; import CartSummaryMobile from '~/modules/cart/components/CartSummaryMobile'; import Image from '~/components/ui/image'; +import { CartItem } from '~/types/cart' +import { deleteUserCart ,upsertUserCart } from '~/services/cart' +import { Trash2Icon } from 'lucide-react'; +import { useProductCartContext } from '@/contexts/ProductCartContext' const CartPage = () => { const router = useRouter(); const auth = getAuth(); - const [isStepApproval, setIsStepApproval] = React.useState(false); + const [isStepApproval, setIsStepApproval] = useState(false); + const [isSelectedAll, setIsSelectedAll] = useState(false); + const [isButtonChek, setIsButtonChek] = useState(false); + const [buttonSelectNow, setButtonSelectNow] = useState(true); + const [isLoad, setIsLoad] = useState<boolean>(false) + const [isLoadDelete, setIsLoadDelete] = useState<boolean>(false) + const { loadCart, cart, summary, updateCartItem } = useCartStore(); + const useDivvice = useDevice(); + const { setRefreshCart } = useProductCartContext() + const [isTop, setIsTop] = useState(true); + const [hasChanged, setHasChanged] = useState(false); + const prevCartRef = useRef<CartItem[] | null>(null); - const { loadCart, cart, summary } = useCartStore(); + useEffect(() => { + const handleScroll = () => { + setIsTop(window.scrollY < 200); + }; - const useDivvice = useDevice(); + window.addEventListener('scroll', handleScroll); + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, []); useEffect(() => { if (typeof auth === 'object' && !cart) { loadCart(auth.id); setIsStepApproval(auth?.feature?.soApproval); } - }, [auth, loadCart, cart]); + }, [auth, loadCart, cart, isButtonChek]); + + useEffect(() => { + if (typeof auth === 'object' && !cart) { + loadCart(auth.id); + setIsStepApproval(auth?.feature?.soApproval); + } + }, [auth, loadCart, cart, isButtonChek]); + + useEffect(() => { + const hasSelectedChanged = () => { + if (prevCartRef.current && cart) { + const prevCart = prevCartRef.current; + return cart.products.some((item, index) => + prevCart[index] && prevCart[index].selected !== item.selected + ); + } + return false; + }; + + if (hasSelectedChanged()) { + setHasChanged(true) + // Perform necessary actions here if selection has changed + }else{ + setHasChanged(false) + } + + // Update the ref to the current cart state + prevCartRef.current = cart ? [...cart.products] : null; + }, [cart]); const hasSelectedPromo = useMemo(() => { if (!cart) return false; - for (const item of cart.products) { - if (item.cart_type === 'promotion' && item.selected) return true; - } - return false; + return cart.products.some(item => item.cart_type === 'promotion' && item.selected); }, [cart]); const hasSelected = useMemo(() => { if (!cart) return false; - for (const item of cart.products) { - if (item.selected) return true; - } - return false; + return cart.products.some(item => item.selected); }, [cart]); const hasSelectNoPrice = useMemo(() => { if (!cart) return false; - for (const item of cart.products) { - if (item.selected && item.price.price_discount == 0) return true; - } - return false; + return cart.products.some(item => item.selected && item.price.price_discount === 0); }, [cart]); - - const handleCheckout = (()=>{ - router.push('/shop/checkout'); - }) - const handleQuotation = (()=>{ - if(hasSelectedPromo || !hasSelected){ + const hasSelectedAll = useMemo(() => { + if (!cart || !Array.isArray(cart.products)) return false; + return cart.products.every(item => item.selected); + }, [cart]); + + + useEffect(() => { + const updateCartItems = async () => { + if (typeof auth === 'object' && cart) { + const upsertPromises = cart.products.map(item => + upsertUserCart({ + userId: auth.id, + type: item.cart_type, + id: item.id, + qty: item.quantity, + selected: item.selected + }) + ); + try { + await Promise.all(upsertPromises); + await loadCart(auth.id); + } catch (error) { + console.error('Failed to update cart items:', error); + } + } + }; + + updateCartItems(); + }, [hasChanged]); + + const handleCheckout = () => { + router.push('/shop/checkout'); + } + + const handleQuotation = () => { + if (hasSelectedPromo || !hasSelected) { toast.error('Maaf, Barang promo tidak dapat dibuat quotation'); - }else{ + } else { router.push('/shop/quotation'); } - }) + } + + const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => { + + // Ensure that cart is not null before attempting to update + if (cart) { + const updatedCart = { + ...cart, + products: cart.products.map(item => ({ + ...item, + selected: !hasSelectedAll + })) + }; + + updateCartItem(updatedCart); // Pass only valid CartProps to updateCartItem + if(hasSelectedAll){ + setIsSelectedAll(false); + }else{ + setIsSelectedAll(true); + } + } + }; + + + const handleDelete = async () => { + if (typeof auth !== 'object' || !cart) return; + + setIsLoadDelete(true) + for (const item of cart.products) { + if(item.selected === true){ + await deleteUserCart(auth.id, [item.cart_id]) + await loadCart(auth.id) + } + } + setIsLoadDelete(false) + setRefreshCart(true) + } return ( <> - <div className={style['title']}>Keranjang Belanja</div> + <div className={`${isTop ? 'border-b-[0px]' : 'border-b-[1px]'} sticky top-[157px] bg-white py-4 border-gray-300 z-50 w-3/4`}> + <div className={`${style['title']}`}>Keranjang Belanja</div> + <div className='h-2' /> + <div className={`flex items-center object-center justify-between `}> + <div className='flex items-center object-center'> + {isLoad && ( + <Spinner className='my-auto' size='sm' /> + )} + {!isLoad && ( + <Checkbox + borderColor='gray.600' + colorScheme='red' + size='lg' + isChecked={hasSelectedAll} + onChange={handleChange} + /> + )} + <p className='p-2 text-caption-2'> + {hasSelectedAll ? "Uncheck all" : "Select all"} + </p> + </div> + <div className='delate all flex items-center object-center'> + <Tooltip + label={clsxm({ + 'Tidak ada item yang dipilih': !hasSelected, + })} + > + <Button + bg='#fadede' + variant='outline' + colorScheme='red' + w='full' + isDisabled={!hasSelected} + onClick={handleDelete} + > + {isLoadDelete && <Spinner size='xs' />} + {!isLoadDelete && <Trash2Icon size={16} />} + <p className='text-sm ml-2'> + Hapus Barang + </p> + </Button> + </Tooltip> + </div> + </div> - <div className='h-6' /> + </div> <div className={style['content']}> <div className={style['item-wrapper']}> <div className={style['item-skeleton']}> - {!cart && <CartItem.Skeleton count={5} height='120px' />} + {!cart && <CartItemModule.Skeleton count={5} height='120px' />} </div> <div className={style['items']}> {cart?.products.map((item) => ( - <CartItem key={item.id} item={item} /> + <CartItemModule key={item.id} item={item} /> ))} {cart?.products?.length === 0 && ( @@ -124,7 +275,7 @@ const CartPage = () => { <CartSummary {...summary} isLoaded={!!cart} /> )} - <div className={isStepApproval ? style['summary-buttons-step-approval'] : style['summary-buttons'] }> + <div className={isStepApproval ? style['summary-buttons-step-approval'] : style['summary-buttons']}> <Tooltip label={ hasSelectedPromo && diff --git a/src-migrate/pages/shop/promo/index.tsx b/src-migrate/pages/shop/promo/index.tsx new file mode 100644 index 00000000..7bb5546d --- /dev/null +++ b/src-migrate/pages/shop/promo/index.tsx @@ -0,0 +1,43 @@ +import dynamic from 'next/dynamic' +import React, { useState } from 'react' +import { LazyLoadComponent } from 'react-lazy-load-image-component' +import Hero from '~/modules/promo/components/Hero' +import PromotionProgram from '~/modules/promo/components/PromotinProgram' +import Voucher from '~/modules/promo/components/Voucher' +import FlashSale from '../../../modules/promo/components/FlashSale' +import FlashSaleNonDisplay from '../../../modules/promo/components/FlashSaleNonDisplay' +const PromoList = dynamic(() => import('../../../modules/promo/components/PromoList')); + + + +const PromoPage = () => { + const [selectedPromo, setSelectedPromo] = useState('Bundling'); + return ( + <> + <LazyLoadComponent> + <Hero /> + </LazyLoadComponent> + <LazyLoadComponent> + <PromotionProgram + selectedPromo={selectedPromo} + onSelectPromo={setSelectedPromo} + /> + <PromoList selectedPromo={selectedPromo} /> + </LazyLoadComponent> + + <LazyLoadComponent> + <FlashSale /> + </LazyLoadComponent> + <h1 className='h-1'></h1> + <LazyLoadComponent> + <FlashSaleNonDisplay /> + </LazyLoadComponent> + <h1 className='h-1'></h1> + <LazyLoadComponent> + <Voucher /> + </LazyLoadComponent> + </> + ) +} + +export default PromoPage
\ No newline at end of file diff --git a/src-migrate/services/promotionProgram.ts b/src-migrate/services/promotionProgram.ts index c8c46c65..8bf2a0bd 100644 --- a/src-migrate/services/promotionProgram.ts +++ b/src-migrate/services/promotionProgram.ts @@ -6,3 +6,11 @@ export const getPromotionProgram = async ( const url = `/api/promotion-program/${programId}`; return await fetch(url).then((res) => res.json()); }; + +export const getPromotionProgramSolr = async () => { + const response = await fetch(`/solr/promotion_programs/select?indent=true&q.op=OR&q=*:*&fq=banner_s:[* TO *]`); + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); +}; diff --git a/src-migrate/services/voucher.ts b/src-migrate/services/voucher.ts new file mode 100644 index 00000000..13d9e2c0 --- /dev/null +++ b/src-migrate/services/voucher.ts @@ -0,0 +1,8 @@ +import odooApi from '~/libs/odooApi'; +import { IVoucher } from '~/types/voucher'; + +export const getVoucherAll = async (): Promise<IVoucher[]> => { + const url = `/api/v1/voucher`; + + return await odooApi('GET', url); +}; diff --git a/src-migrate/types/banner.ts b/src-migrate/types/banner.ts index dbccc378..e1604ad4 100644 --- a/src-migrate/types/banner.ts +++ b/src-migrate/types/banner.ts @@ -1,8 +1,8 @@ export interface IBanner { - background_color: string | false; - group_by_week: number | false; image: string; name: string; sequence: number; - url: string; + url: string | false; + group_by_week: number | false; + background_color: string | false; } diff --git a/src-migrate/types/cart.ts b/src-migrate/types/cart.ts index 5a2cf4a9..4e3c8b99 100644 --- a/src-migrate/types/cart.ts +++ b/src-migrate/types/cart.ts @@ -23,6 +23,7 @@ export type CartProduct = { }; export type CartItem = { + image_program: string; cart_id: number; quantity: number; selected: boolean; diff --git a/src-migrate/types/promotion.ts b/src-migrate/types/promotion.ts index 85190aad..217bba33 100644 --- a/src-migrate/types/promotion.ts +++ b/src-migrate/types/promotion.ts @@ -10,15 +10,18 @@ export interface IPromotion { limit_user: number; limit_trx: number; price: number; + image: string; total_qty: number; products: { product_id: number; qty: number; + name: string; }[]; free_products: { product_id: number; qty: number; }[]; + } export interface IProductVariantPromo { diff --git a/src-migrate/types/promotionProgram.ts b/src-migrate/types/promotionProgram.ts index 205884b6..c02cbfd0 100644 --- a/src-migrate/types/promotionProgram.ts +++ b/src-migrate/types/promotionProgram.ts @@ -5,4 +5,6 @@ export type IPromotionProgram = { end_time: string; applies_to: string; time_left: number; + image:string; + banner:string; }; diff --git a/src-migrate/types/voucher.ts b/src-migrate/types/voucher.ts new file mode 100644 index 00000000..3e90f449 --- /dev/null +++ b/src-migrate/types/voucher.ts @@ -0,0 +1,8 @@ +export interface IVoucher { + id: number; + image: string; + name: string; + code: string; + description: string | false; + remaining_time: string; +} diff --git a/src/api/promoApi.js b/src/api/promoApi.js index 0e82c8b9..3f85db8e 100644 --- a/src/api/promoApi.js +++ b/src/api/promoApi.js @@ -13,11 +13,13 @@ export const fetchPromoItems = async (type) => { } }; -export const fetchPromoItemsSolr = async (type) => { +export const fetchPromoItemsSolr = async (type, start, rows) => { // let query = type ? `type_value_s:${type}` : '*:*'; let sort ='sort=if(exists(sequence_i),0,1) asc, sequence_i asc, if(exists(total_qty_sold_f), total_qty_sold_f, -1) desc'; - let start = 0 - let rows = 100 + // let start = 0 + // let rows = 100 + // let start = 0 + // let rows = 10 try { const queryParams = new URLSearchParams({ q: type }); const response = await fetch(`/solr/promotion_program_lines/select?${queryParams.toString()}&rows=${rows}&start=${start}&${sort}`); diff --git a/src/contexts/ProductCartContext.js b/src/contexts/ProductCartContext.js index 06e97563..3a21d2e0 100644 --- a/src/contexts/ProductCartContext.js +++ b/src/contexts/ProductCartContext.js @@ -6,10 +6,11 @@ export const ProductCartProvider = ({ children }) => { const [productCart, setProductCart] = useState(null) const [refreshCart, setRefreshCart] = useState(false) const [isLoading, setIsloading] = useState(false) + const [productQuotation, setProductQuotation] = useState(null) return ( <ProductCartContext.Provider - value={{ productCart, setProductCart, refreshCart, setRefreshCart, isLoading, setIsloading }} + value={{ productCart, setProductCart, refreshCart, setRefreshCart, isLoading, setIsloading, productQuotation, setProductQuotation}} > {children} </ProductCartContext.Provider> diff --git a/src/core/components/elements/Footer/BasicFooter.jsx b/src/core/components/elements/Footer/BasicFooter.jsx index 6129143d..4beea604 100644 --- a/src/core/components/elements/Footer/BasicFooter.jsx +++ b/src/core/components/elements/Footer/BasicFooter.jsx @@ -259,7 +259,7 @@ const InformationCenter = () => ( <li className='text-gray_r-12/80 flex items-center'> <PhoneArrowUpRightIcon className='w-[18px] mr-2' /> <a href='tel:02129338828' target='_blank' rel='noreferrer'> - (021) 2933-8828 / 29 + (021) 2933-8828 </a> </li> <li className='text-gray_r-12/80 flex items-center'> diff --git a/src/core/components/elements/Footer/PromoOffer.tsx b/src/core/components/elements/Footer/PromoOffer.tsx new file mode 100644 index 00000000..a5432b6a --- /dev/null +++ b/src/core/components/elements/Footer/PromoOffer.tsx @@ -0,0 +1,112 @@ + +import React from "react"; +// import { useGeneralSetting } from "@/common/state-management/general-setting"; +import { FormEvent, useEffect, useState } from "react"; +import toast from "react-hot-toast"; +import style from "../Footer/style/promoOffer.module.css" +const PromoOffer = ()=>{ + // const { data, isLoading, fetchData } = useGeneralSetting(); + const [formData, setFormData] = useState<FormData>({ + email: "", + name: "", + telephone: "", + message: "", + }); + + useEffect(() => { + // fetchData(); + }, []); + + type FormData = { + email: string; + name: string; + telephone: string; + message: string; + }; + + const [errors, setErrors] = useState({ + email: false, + name: false, + message: false, + }); + + +const handleGetOffer = async (e: FormEvent) => { + e.preventDefault(); + let loadingToast; + try { + loadingToast = toast.loading("Mengirimkan formulir..."); + const response = await fetch("/api/contactus", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ ...formData, from: "newsletter" }), + }); + + if (response.ok) { + toast.dismiss(loadingToast); + toast.success("Terima kasih telah menghubungi kami"); + setFormData({ + email: "", + name: "", + telephone: "", + message: "", + }); + } else { + toast.dismiss(loadingToast); + toast.error("Gagal mengirimkan formulir. Silakan coba lagi nanti."); + } + } catch (error) { + toast.dismiss(loadingToast); + console.error("Gagal mengirimkan formulir", error); + toast.error("Terjadi kesalahan. Silakan coba lagi nanti."); + } + }; + + + + + return( + <div className=" py-3 mb-3"> + <div className="md:flex container mx-auto md:justify-between md:items-center md:gap-x-20"> + <div className=""> + <span className="text-black font-semibold text-sm md:text-lg"> + Dapatkan Promo Menarik Setiap Bulan{" "} + </span> + <p> + Promo produk dengan penawaran terbatas setiap bulannya! + </p> + </div> + <div className=" flex-1 flex items-center h-full justify-end text-sm text-slate-950"> + <form onSubmit={handleGetOffer} className={style['form-input']}> + <div className="flex justify-start w-full"> + <div className="flex justify-end"> + <input + type="email" + value={formData.email} + onChange={(e) => + setFormData({ ...formData, email: e.target.value }) + } + className={style['input']} + placeholder="Masukkan email anda disini" + /> + <button + type="submit" + className={style['button']} + > + Dapatkan + </button> + </div> + + + </div> + </form> + + </div> + </div> + </div> + ) + }; + + export default PromoOffer; diff --git a/src/core/components/elements/Footer/style/promoOffer.module.css b/src/core/components/elements/Footer/style/promoOffer.module.css new file mode 100644 index 00000000..3184182d --- /dev/null +++ b/src/core/components/elements/Footer/style/promoOffer.module.css @@ -0,0 +1,39 @@ +.form-input { + @apply + h-full + w-[514px] + text-slate-950 + flex + justify-center; +} + +.input{ + @apply w-[320px] + sm:w-[320px] + md:w-[500px] + xl:w-[514px] + lg:w-[514px] + 2xl:w-[514px] + text-black + py-2 + h-11 + md:py-3 + px-4 + bg-[#FDF1C7] + rounded-3xl + focus:outline-none + ; +} + +.button{ + @apply bg-[#FAD147] + absolute + py-1.5 + rounded-3xl + text-black + md:py-2.5 + px-4 + h-11 + z-0 + ; +}
\ No newline at end of file diff --git a/src/core/components/elements/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx index 9de761a2..6454c414 100644 --- a/src/core/components/elements/Navbar/NavbarDesktop.jsx +++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx @@ -5,16 +5,19 @@ import { createSlug } from '@/core/utils/slug'; import whatsappUrl from '@/core/utils/whatsappUrl'; import IndoteknikLogo from '@/images/logo.png'; import Cardheader from '@/lib/cart/components/Cartheader'; +import Quotationheader from "../../../../../src/lib/quotation/components/Quotationheader.jsx" import Category from '@/lib/category/components/Category'; +import { useProductCartContext } from '@/contexts/ProductCartContext'; import { ChevronDownIcon, DocumentCheckIcon, HeartIcon, + ArrowUpRightIcon, } from '@heroicons/react/24/outline'; import dynamic from 'next/dynamic'; import Image from 'next/image'; import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import DesktopView from '../../views/DesktopView'; import Link from '../Link/Link'; import NavbarUserDropdown from './NavbarUserDropdown'; @@ -27,6 +30,8 @@ import { MenuList, useDisclosure, } from '@chakra-ui/react'; +import style from "./style/NavbarDesktop.module.css"; +import useTransactions from '@/lib/transaction/hooks/useTransactions'; const Search = dynamic(() => import('./Search'), { ssr: false }); const TopBanner = dynamic(() => import('./TopBanner'), { ssr: false }); @@ -36,7 +41,8 @@ const NavbarDesktop = () => { const auth = useAuth(); const [cartCount, setCartCount] = useState(0); - + const [quotationCount, setQuotationCount] = useState(0); + const [pendingTransactions, setPendingTransactions] = useState([]) const [templateWA, setTemplateWA] = useState(null); const [payloadWA, setPayloadWa] = useState(null); const [urlPath, setUrlPath] = useState(null); @@ -45,8 +51,64 @@ const NavbarDesktop = () => { const { product } = useProductContext(); const { isOpen, onOpen, onClose } = useDisclosure(); + + const query = { + context: 'quotation', + site: + (auth?.webRole === null && auth?.site ? auth.site : null), + }; + + const { transactions } = useTransactions({ query }); + const data = transactions?.data?.saleOrders.filter( + (transaction) => transaction.status === 'draft' + ); + + const [showPopup, setShowPopup] = useState(false); + const [isTop, setIsTop] = useState(true); + + const handleTopBannerLoad = useCallback(() => { + const showTimer = setTimeout(() => { + setShowPopup(true); + }, 500); + + const hideTimer = setTimeout(() => { + // setShowPopup(false); + }, 9500); + + return () => { + clearTimeout(showTimer); + clearTimeout(hideTimer); + }; + }, []); + + useEffect(() => { + const handleScroll = () => { + setIsTop(window.scrollY < 100); + }; + + window.addEventListener('scroll', handleScroll); + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, []); + + useEffect(() => { + const handleScroll = () => { + setIsTop(window.scrollY < 100); + }; + + window.addEventListener('scroll', handleScroll); + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, []); useEffect(() => { + setPendingTransactions(data); + }, [transactions.data]); + + + useEffect(() => { if (router.pathname === '/shop/product/[slug]') { setPayloadWa({ name: product?.name, @@ -54,11 +116,11 @@ const NavbarDesktop = () => { url: createSlug('/shop/product/', product?.name, product?.id, true), }); setTemplateWA('product'); - + setUrlPath(router.asPath); } }, [product, router]); - + useEffect(() => { const handleCartChange = () => { const cart = async () => { @@ -67,18 +129,34 @@ const NavbarDesktop = () => { }; cart(); }; - handleCartChange(); - + handleCartChange(); + window.addEventListener('localStorageChange', handleCartChange); - + return () => { window.removeEventListener('localStorageChange', handleCartChange); }; }, []); + useEffect(() => { + const handleQuotationChange = () => { + const quotation = async () => { + setQuotationCount(pendingTransactions?.length); + }; + quotation(); + }; + handleQuotationChange(); + + window.addEventListener('localStorageChange', handleQuotationChange); + + return () => { + window.removeEventListener('localStorageChange', handleQuotationChange); + }; + }, [pendingTransactions]); + return ( <DesktopView> - <TopBanner /> + <TopBanner onLoad={handleTopBannerLoad} /> <div className='py-2 bg-warning-400' id='desktop-nav-top'> <div className='container mx-auto flex justify-between'> <div className='flex items-start gap-5'> @@ -138,17 +216,7 @@ const NavbarDesktop = () => { <Search /> </div> <div className='flex gap-x-4 items-center'> - <Link - href='/my/transactions' - target='_blank' - rel='noreferrer' - className='flex items-center gap-x-2 !text-gray_r-12/80' - > - <DocumentCheckIcon className='w-7' /> - Daftar - <br /> - Quotation - </Link> + <Quotationheader quotationCount={quotationCount} data={pendingTransactions} /> <Cardheader cartCount={cartCount} /> @@ -203,45 +271,76 @@ const NavbarDesktop = () => { </div> </button> <div className='w-6/12 flex px-1 divide-x divide-gray_r-6'> + + <Link + href="/shop/promo" + className={`${ + router.asPath === '/shop/promo' && 'bg-gray_r-3' + } flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition group relative`} // Added relative position + target="_blank" + rel="noreferrer" + > + {showPopup && ( + <div className='w-full h-full relative justify-end items-start'> + <Image + src='/images/penawaran-terbatas.jpg' + alt='penawaran terbatas' + width={1440} + height={160} + quality={100} + // className={`fixed ${isTop ? 'md:top-[145px] lg:top-[160px] ' : 'lg:top-[85px] top-[80px]'} rounded-3xl md:left-1/4 lg:left-1/4 xl:left-1/4 left-2/3 w-40 h-12 p-2 z-50 transition-all duration-300 animate-pulse`} + className={`inline-block relative -top-8 transition-all duration-300 z-20 animate-pulse`} + /> + </div> + )} + <p className="absolute inset-0 flex justify-center items-center group-hover:scale-105 group-hover:text-red-500 transition-transform duration-200 z-10">Semua Promo</p> + </Link> + {/* {showPopup && router.pathname === '/' && ( + <div className={`fixed ${isTop ? 'top-[170px]' : 'top-[90px]'} rounded-3xl left-[700px] w-fit object-center bg-green-50 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20 text-center p-2 z-50 transition-all duration-300`}> + <p className='w-36 h-3'> + Penawaran Terbatas + </p> + </div> + )} */} + + <Link href='/shop/brands' - className={`${ + className={`${ router.asPath === '/shop/brands' && 'bg-gray_r-3' - } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition`} + } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition group`} target='_blank' rel='noreferrer' > - Semua Brand + <p className="group-hover:scale-105 group-hover:text-red-500 transition-transform duration-200">Semua Brand</p> </Link> <Link href='/shop/search?orderBy=stock' className={`${ router.asPath === '/shop/search?orderBy=stock' && 'bg-gray_r-3' - } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition`} + } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition group`} target='_blank' rel='noreferrer' > - Ready Stock + <p className="group-hover:scale-105 group-hover:text-red-500 transition-transform duration-200">Ready Stock</p> </Link> <Link href='https://blog.indoteknik.com/' - className='p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition' + className='p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition group' target='_blank' rel='noreferrer noopener' > - Blog Indoteknik + <p className="group-hover:scale-105 group-hover:text-red-500 transition-transform duration-200">Blog Indoteknik</p> </Link> - <Link + {/* <Link href='/video' - className={`${ - router.asPath === '/video' && 'bg-gray_r-3' - } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition`} + className='p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition' target='_blank' rel='noreferrer' > Indoteknik TV - </Link> + </Link> */} </div> <div className='w-3/12 flex gap-x-1 relative'> diff --git a/src/core/components/elements/Navbar/NavbarMobile.jsx b/src/core/components/elements/Navbar/NavbarMobile.jsx index bcf45e0a..90314671 100644 --- a/src/core/components/elements/Navbar/NavbarMobile.jsx +++ b/src/core/components/elements/Navbar/NavbarMobile.jsx @@ -11,7 +11,7 @@ import Image from 'next/image'; import { useEffect, useState } from 'react'; import MobileView from '../../views/MobileView'; import Link from '../Link/Link'; -// import TopBanner from './TopBanner'; +import TopBanner from './TopBanner'; const Search = dynamic(() => import('./Search')); @@ -39,7 +39,7 @@ const NavbarMobile = () => { return ( <MobileView> - {/* <TopBanner /> */} + <TopBanner /> <nav className='px-4 py-2 pb-3 sticky top-0 z-50 bg-white shadow'> <div className='flex justify-between items-center mb-2'> <Link href='/'> diff --git a/src/core/components/elements/Navbar/TopBanner.jsx b/src/core/components/elements/Navbar/TopBanner.jsx index 722a7501..302db5c4 100644 --- a/src/core/components/elements/Navbar/TopBanner.jsx +++ b/src/core/components/elements/Navbar/TopBanner.jsx @@ -1,39 +1,45 @@ import Image from 'next/image'; -import { useQuery } from 'react-query'; - +import { useQuery } from 'react-query';import useDevice from '@/core/hooks/useDevice' import odooApi from '@/core/api/odooApi'; import SmoothRender from '~/components/ui/smooth-render'; import Link from '../Link/Link'; +import { useEffect } from 'react'; -const TopBanner = () => { +const TopBanner = ({ onLoad }) => { const topBanner = useQuery({ queryKey: 'topBanner', queryFn: async () => await odooApi('GET', '/api/v1/banner?type=top-banner'), refetchOnWindowFocus: false, }); - const backgroundColor = topBanner.data?.[0]?.backgroundColor || 'transparent'; + // const backgroundColor = topBanner.data?.[0]?.backgroundColor || 'transparent'; const hasData = topBanner.data?.length > 0; const data = topBanner.data?.[0] || null; + useEffect(() => { + if (hasData) { + onLoad(); + } + }, [hasData, onLoad]); + return ( <SmoothRender isLoaded={hasData} - height='36px' + // height='36px' duration='700ms' delay='300ms' - style={{ backgroundColor }} - > - <Link href={data?.url}> - <Image - src={data?.image} - alt={data?.name} - width={1440} - height={40} - className='object-cover object-center h-full mx-auto' - /> - </Link> - </SmoothRender> + className='h-auto' + > + <Link + href={data?.url} + className="block bg-cover bg-center h-3 md:h-6 lg:h-[36px]" + style={{ + backgroundImage: `url('${data?.image}')`, + }} + > + </Link> + + </SmoothRender> ); }; diff --git a/src/core/components/elements/Navbar/style/NavbarDesktop.module.css b/src/core/components/elements/Navbar/style/NavbarDesktop.module.css new file mode 100644 index 00000000..9cddb127 --- /dev/null +++ b/src/core/components/elements/Navbar/style/NavbarDesktop.module.css @@ -0,0 +1,14 @@ +/* navbarDesktop.module.css */ +@keyframes blink { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0; + } +} + +.blink { + animation: blink 0.8s infinite; +} + diff --git a/src/core/components/elements/Sidebar/Sidebar.jsx b/src/core/components/elements/Sidebar/Sidebar.jsx index 38fcdef8..55838890 100644 --- a/src/core/components/elements/Sidebar/Sidebar.jsx +++ b/src/core/components/elements/Sidebar/Sidebar.jsx @@ -117,6 +117,9 @@ const Sidebar = ({ active, close }) => { </> )} </div> + <SidebarLink className={itemClassName} href='/shop/promo'> + Semua Promo + </SidebarLink> <SidebarLink className={itemClassName} href='/shop/brands'> Semua Brand </SidebarLink> @@ -128,9 +131,9 @@ const Sidebar = ({ active, close }) => { > Blog Indoteknik </SidebarLink> - <SidebarLink className={itemClassName} href='/video'> + {/* <SidebarLink className={itemClassName} href='/video'> Indoteknik TV - </SidebarLink> + </SidebarLink> */} <SidebarLink className={itemClassName} href='/tentang-kami'> Tentang Indoteknik </SidebarLink> diff --git a/src/core/components/layouts/BasicLayout.jsx b/src/core/components/layouts/BasicLayout.jsx index a4f3a856..ba51022d 100644 --- a/src/core/components/layouts/BasicLayout.jsx +++ b/src/core/components/layouts/BasicLayout.jsx @@ -93,4 +93,4 @@ const BasicLayout = ({ children }) => { ); }; -export default BasicLayout; +export default BasicLayout;
\ No newline at end of file diff --git a/src/lib/brand/components/BrandCard.jsx b/src/lib/brand/components/BrandCard.jsx index 731214ff..2d78d956 100644 --- a/src/lib/brand/components/BrandCard.jsx +++ b/src/lib/brand/components/BrandCard.jsx @@ -8,7 +8,7 @@ const BrandCard = ({ brand }) => { return ( <Link href={createSlug('/shop/brands/', brand.name, brand.id)} - className={`py-1 px-2 rounded border border-gray_r-6 flex justify-center items-center ${ + className={`py-1 px-2 border-gray_r-6 flex justify-center items-center hover:scale-110 transition duration-500 ease-in-out ${ isMobile ? 'h-16' : 'h-24' }`} > @@ -16,9 +16,9 @@ const BrandCard = ({ brand }) => { <Image src={brand.logo} alt={brand.name} - width={128} - height={128} - className='h-full w-full object-contain object-center' + width={50} + height={50} + className='h-full w-[122px] object-contain object-center' /> )} {!brand.logo && ( diff --git a/src/lib/cart/components/Cartheader.jsx b/src/lib/cart/components/Cartheader.jsx index 19f79bc9..6967d180 100644 --- a/src/lib/cart/components/Cartheader.jsx +++ b/src/lib/cart/components/Cartheader.jsx @@ -1,14 +1,20 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { getCartApi } from '../api/CartApi' +import currencyFormat from '@/core/utils/currencyFormat' +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 Image from '@/core/components/elements/Image/Image' +import whatsappUrl from '@/core/utils/whatsappUrl' +import { AnimatePresence, motion } from 'framer-motion' +import style from '../../../../src-migrate/modules/cart/styles/item-promo.module.css' 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) @@ -19,7 +25,7 @@ const Cardheader = (cartCount) => { useProductCartContext() const [isHovered, setIsHovered] = useState(false) - + const [isTop, setIsTop] = useState(true) const products = useMemo(() => { return productCart?.products || [] }, [productCart]) @@ -75,14 +81,26 @@ const Cardheader = (cartCount) => { useEffect(() => { setCountCart(cartCount.cartCount) + setRefreshCart(false) }, [cartCount]) + useEffect(() => { + const handleScroll = () => { + setIsTop(window.scrollY === 0) + } + window.addEventListener('scroll', handleScroll) + return () => { + window.removeEventListener('scroll', handleScroll) + } + }, []) + 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> @@ -109,6 +127,246 @@ const Cardheader = (cartCount) => { </span> </Link> </div> + <AnimatePresence> + {isHovered && ( + <> + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 1, top: isTop ? 230 : 155 }} + exit={{ opacity: 0 }} + transition={{ duration: 0.15, top: { duration: 0.3 } }} + className={`fixed left-0 w-full h-full bg-black/50 z-10`} + /> + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 1, transition: { duration: 0.2 } }} + exit={{ opacity: 0, transition: { duration: 0.3 } }} + className='absolute z-10 left-0 w-96' + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + > + <motion.div + initial={{ height: 0 }} + animate={{ height: 'auto' }} + exit={{ height: 0 }} + className='w-full max-w-md p-2 bg-white border border-gray-200 rounded-lg shadow overflow-hidden' + > + <div className='p-2 flex justify-between items-center'> + <h5 className='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' className='max-w-sm animate-pulse'> + <div className='flex items-center space-x-4 mb- 2'> + <div className='flex-shrink-0'> + <PhotoIcon className='h-16 w-16 text-gray-500' /> + </div> + <div className='flex-1 min-w-0'> + <div className='h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4'></div> + <div className='h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px] mb-2.5'></div> + <div className='h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5'></div> + </div> + </div> + </div> + ))} + {auth && 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> + )} + {auth && products.length > 0 && !isLoading && ( + <> + <ul role='list' className='divide-y divide-gray-200 dark:divide-gray-700'> + {products && + products?.map((product, index) => ( + <> + <li className='py-1 sm:py-2'> + <div className='flex items-center space-x-4'> + <div className='bagian gambar flex-shrink-0'> + {product.cartType === 'promotion' && ( + <Image + src={product.imageProgram[0]} + alt={product.name} + className='object-contain object-center border border-gray_r-6 h-16 w-16 rounded-md' + /> + )} + {product.cartType === 'product' && ( + <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 className='bagian tulisan dan harga flex-1 min-w-0'> + {product.cartType === 'promotion' && ( + <p className='text-caption-2 font-medium text-gray-900 truncate dark:text-white'> + {product.name} + </p> + )} + {product.cartType === 'product' && ( + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' + > + {' '} + <p className='text-caption-2 font-medium text-gray-900 truncate dark:text-white'> + {product.parent.name} + </p> + </Link> + )} + {product?.hasFlashsale && ( + <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> + <div className="flex flex-col w-3/4"> + {product.products?.map((product) => + <div key={product.id} className='md:ml-8 ml-4 mt-2 flex'> + <Link href={createSlug('/shop/product/', product.parent.name, product.parent.id.toString())} className='md:h-12 md:w-12 md:min-w-[48px] h-10 w-10 min-w-[40px] border border-gray-300 rounded '> + {product?.image && <Image src={product.image} alt={product.name} width={40} height={40} className='w-full h-full object-fill' />} + </Link> + + <div className="ml-4 w-full flex flex-col gap-y-1"> + <Link href={createSlug('/shop/product/', product.parent.name, product.parent.id.toString())} className="text-caption-2 font-medium text-gray-900 truncate dark:text-white"> + {product.displayName} + </Link> + + <div className='flex w-full'> + <div className="flex flex-col"> + {/* <div className="text-gray-500 text-caption-1">{product.code}</div> */} + <div> + <span className="text-gray-500 text-caption-1">Berat Barang: </span> + <span className="text-gray-500 text-caption-1">{product.packageWeight} Kg</span> + </div> + </div> + </div> + </div> + + </div> + )} + {product.freeProducts?.map((product) => + <div key={product.id} className='md:ml-8 ml-4 mt-2 flex'> + <Link href={createSlug('/shop/product/', product.parent.name, product.parent.id.toString())} className='md:h-12 md:w-12 md:min-w-[48px] h-10 w-10 min-w-[40px] border border-gray-300 rounded '> + {product?.image && <Image src={product.image} alt={product.name} width={40} height={40} className='w-full h-full object-fill' />} + </Link> + + <div className="ml-4 w-full flex flex-col gap-y-1"> + <Link href={createSlug('/shop/product/', product.parent.name, product.parent.id.toString())} className="text-caption-2 font-medium text-gray-900 truncate dark:text-white"> + {product.displayName} + </Link> + + <div className='flex w-full'> + <div className="flex flex-col"> + {/* <div className="text-gray-500 text-caption-1">{product.code}</div> */} + <div> + <span className="text-gray-500 text-caption-1">Berat Barang: </span> + <span className="text-gray-500 text-caption-1">{product.packageWeight} Kg</span> + </div> + </div> + </div> + </div> + + </div> + )} + </div> + </li> + </> + ))} + </ul> + <hr /> + </> + )} + </div> + {auth && products.length > 0 && !isLoading && ( + <> + <div className='mt-3'> + <span className='text-gray-400 text-caption-2'>Subtotal Sebelum PPN : </span> + <span className='font-semibold text-red-600'>{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> + </> + )} + </motion.div> + </motion.div> + </> + )} + </AnimatePresence> </div> ) } diff --git a/src/lib/category/api/popularProduct.js b/src/lib/category/api/popularProduct.js new file mode 100644 index 00000000..e17e0ae5 --- /dev/null +++ b/src/lib/category/api/popularProduct.js @@ -0,0 +1,31 @@ + +export const fetchPromoItemsSolr = async (category_id_ids) => { + let sort ='sort=qty_sold_f desc'; + try { + const queryParams = new URLSearchParams({ q: category_id_ids }); + const response = await fetch(`/solr/product/select?${queryParams.toString()}&rows=2000&fl=manufacture_name_s,manufacture_id_i,id,display_name_s&${sort}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + const promotions = await map(data.response.docs); + return promotions; + } catch (error) { + console.error("Error fetching promotion data:", error); + return []; + } + }; + + const map = async (promotions) => { + const result = []; + for (const promotion of promotions) { + const data = { + id: promotion.id, + name: promotion.display_name_s, + manufacture_name: promotion.manufacture_name_s, + manufacture_id: promotion.manufacture_id_i, + }; + result.push(data); + } + return result; + };
\ No newline at end of file diff --git a/src/lib/category/components/Category.jsx b/src/lib/category/components/Category.jsx index e6ea5acf..ff958378 100644 --- a/src/lib/category/components/Category.jsx +++ b/src/lib/category/components/Category.jsx @@ -2,10 +2,15 @@ import odooApi from '@/core/api/odooApi' import Link from '@/core/components/elements/Link/Link' import DesktopView from '@/core/components/views/DesktopView' import { createSlug } from '@/core/utils/slug' +import { ChevronRightIcon } from '@heroicons/react/24/outline' +import Image from 'next/image' import { useEffect, useState } from 'react' +import PopularBrand from './PopularBrand' const Category = () => { const [categories, setCategories] = useState([]) + const [openCategories, setOpenCategory] = useState([]); + useEffect(() => { const loadCategories = async () => { @@ -26,46 +31,65 @@ const Category = () => { } loadCategories() }, []) + // console.log("categories",categories) return ( <DesktopView> <div className='category-mega-box'> {categories?.map((category) => ( - <div key={category.id}> + <div key={category.id} className='flex'> <Link href={createSlug('/shop/category/', category.name, category.id)} - className='category-mega-box__parent' + className='category-mega-box__parent flex items-center' > + <div className='w-6 h-6 border mr-2 rounded-full flex justify-center items-center'> + <Image src={category.image} alt='' width={16} height={16} /> + </div> {category.name} </Link> <div className='category-mega-box__child-wrapper'> - <div className='grid grid-cols-3 gap-x-4 gap-y-6 max-h-full overflow-auto'> + <div className='grid grid-cols-3 gap-x-4 gap-y-6 max-h-full !w-[590px] overflow-auto'> {category.childs.map((child1Category) => ( - <div key={child1Category.id}> + <div key={child1Category.id} className='w-full'> <Link href={createSlug('/shop/category/', child1Category.name, child1Category.id)} - className='category-mega-box__child-one mb-4' + className='category-mega-box__child-one mb-4 w-full h-8 flex justify-center line-clamp-2' > {child1Category.name} </Link> - <div className='flex flex-col gap-y-3'> - {child1Category.childs.map((child2Category) => ( - <Link - href={createSlug( - '/shop/category/', - child2Category.name, - child2Category.id - )} - className='category-mega-box__child-two' - key={child2Category.id} - > - {child2Category.name} - </Link> + <div className='flex flex-col gap-y-3 w-full'> + {child1Category.childs.map((child2Category, index) => ( + (index < 4) && ( + <Link + href={createSlug('/shop/category/', child2Category.name, child2Category.id)} + className='category-mega-box__child-two truncate' + key={child2Category.id} + > + {child2Category.name} + </Link> + ) ))} + {child1Category.childs.length > 5 && ( + <div className='flex hover:bg-gray_r-8/35 rounded-10'> + <Link + href={createSlug('/shop/category/', child1Category.name, child1Category.id)} + className='category-mega-box__child-one flex items-center gap-4 font-bold hover:ml-4' + > + <p className='mt-2 mb-0 text-danger-500 font-semibold'>Lihat Semua</p> + <ChevronRightIcon className='w-4 text-danger-500 font-bold' /> + </Link> + </div> + )} </div> </div> ))} </div> + <div className='category-mega-box__child-wrapper !w-[260px] !flex !flex-col !gap-4'> + <PopularBrand category={category} /> + <div className='flex w-60 h-20 object-cover'> + <Image src='https://erp.indoteknik.com/api/image/x_banner.banner/x_banner_image/397' alt='' width={275} height={4} /> + </div> + </div> </div> </div> ))} diff --git a/src/lib/category/components/PopularBrand.jsx b/src/lib/category/components/PopularBrand.jsx new file mode 100644 index 00000000..dca731e8 --- /dev/null +++ b/src/lib/category/components/PopularBrand.jsx @@ -0,0 +1,83 @@ +import odooApi from '@/core/api/odooApi' +import React, { useEffect, useState } from 'react' +import axios from 'axios'; +import { useQuery } from 'react-query' +import Link from '@/core/components/elements/Link/Link' +import { createSlug } from '@/core/utils/slug' +import Image from 'next/image' +import { ChevronRightIcon } from '@heroicons/react/24/outline' +import useProductSearch from '../../../lib/product/hooks/useProductSearch'; +import { SolrResponse } from "~/types/solr"; +import { fetchPromoItemsSolr } from '../api/popularProduct' + +const SOLR_HOST = process.env.SOLR_HOST + +const PopularBrand = ({ category }) => { + const [topBrands, setTopBrands] = useState([]); + + const fetchTopBrands = async () => { + try { + const items = await fetchPromoItemsSolr(`category_id_ids:(${category.categoryDataIds.join(' OR ')})`); + // console.log("id",items) + // Fungsi untuk deduplikasi dan mengambil 12 nama brand teratas + const getTop12UniqueBrands = (prod) => { + const brandSet = new Set(); + const topBrands = []; + + for (const product of prod) { + if (!brandSet.has(product.manufacture_name)) { + brandSet.add(product.manufacture_name); + topBrands.push({ name: product.manufacture_name, id: product.manufacture_id }); + }else{ + } + if (topBrands.length === 18) break; + } + return topBrands; + } + + // Menggunakan hasil pencarian produk + const products = items; + const top12UniqueBrands = getTop12UniqueBrands(products); + + // console.log('top12UniqueBrands', top12UniqueBrands); + setTopBrands(top12UniqueBrands); + } catch (error) { + console.error("Error fetching data from Solr", error); + throw error; + } + } + + useEffect(() => { + fetchTopBrands(); + }, [category]); + + return ( + <div className='flex flex-col'> + <div className='grid grid-cols-3 max-h-full w-full gap-2'> + {topBrands.map((brand, index) => ( + <div key={index} className='w-full flex items-center justify-center pb-2'> + <Link + href={createSlug('/shop/brands/', brand.name, brand.id)} + className='category-mega-box__child-one w-8 h-full flex items-center justify-center ' + > + <Image src={`https://erp.indoteknik.com/api/image/x_manufactures/x_logo_manufacture/${brand.id}` } alt={`${brand.name}`} width={104} height={44} objectFit='cover' /> + </Link> + </div> + ))} + </div> + {/* {topBrands.length > 8 && ( + <div className='flex hover:bg-gray_r-8/35 rounded-10'> + <Link + href={createSlug('/shop/category/', category.name, category.id)} + className='category-mega-box__child-one flex items-center gap-4 font-bold hover:ml-4' + > + <p className='mt-2 mb-0 text-danger-500 font-semibold'>Lihat Semua Brand</p> + <ChevronRightIcon className='w-4 text-danger-500 font-bold' /> + </Link> + </div> + )} */} + </div> + ) +} + +export default PopularBrand; diff --git a/src/lib/checkout/api/checkoutApi.js b/src/lib/checkout/api/checkoutApi.js index 24f1868a..fd982fff 100644 --- a/src/lib/checkout/api/checkoutApi.js +++ b/src/lib/checkout/api/checkoutApi.js @@ -1,28 +1,20 @@ -import odooApi from '@/core/api/odooApi' -import { getAuth } from '@/core/utils/auth' +import odooApi from '@/core/api/odooApi'; +import { getAuth } from '@/core/utils/auth'; export const checkoutApi = async ({ data }) => { - const auth = getAuth() + const auth = getAuth(); const dataCheckout = await odooApi( 'POST', `/api/v1/partner/${auth.partnerId}/sale_order/checkout`, data - ) - return dataCheckout -} + ); + return dataCheckout; +}; -export const getProductsCheckout = async (voucher, query) => { - const id = getAuth()?.id - let products - if(voucher && query){ - products = await odooApi('GET',`/api/v1/user/${id}/sale_order/checkout?voucher=${voucher}&source=buy`) - }else if (voucher){ - products = await odooApi('GET',`/api/v1/user/${id}/sale_order/checkout?voucher=${voucher}`) - }else if (query) { - products = await odooApi('GET',`/api/v1/user/${id}/sale_order/checkout?source=buy`) - }else{ - products = await odooApi('GET',`/api/v1/user/${id}/sale_order/checkout`) - } - - return products -} +export const getProductsCheckout = async (query) => { + const queryParam = new URLSearchParams(query); + const userId = getAuth()?.id; + const url = `/api/v1/user/${userId}/sale_order/checkout?${queryParam.toString()}`; + const result = await odooApi('GET', url); + return result; +}; diff --git a/src/lib/checkout/api/getVoucher.js b/src/lib/checkout/api/getVoucher.js index 15c6abbb..779cef43 100644 --- a/src/lib/checkout/api/getVoucher.js +++ b/src/lib/checkout/api/getVoucher.js @@ -1,25 +1,28 @@ -import odooApi from '@/core/api/odooApi' +import odooApi from '@/core/api/odooApi'; import { getAuth } from '@/core/utils/auth' -export const getVoucher = async (id, source) => { - let dataVoucher = null - if(source){ - dataVoucher = await odooApi('GET', `/api/v1/user/${id}/voucher?source=${source}`) - }else { - dataVoucher = await odooApi('GET', `/api/v1/user/${id}/voucher`) - } - return dataVoucher -} +export const getVoucher = async (id, query) => { + const queryParam = new URLSearchParams(query); + const url = `/api/v1/user/${id}/voucher?${queryParam.toString()}`; + const dataVoucher = await odooApi('GET', url); + return dataVoucher; +}; export const findVoucher = async (code, id, source) => { - let dataVoucher = null - if(source){ - dataVoucher = await odooApi('GET', `/api/v1/user/${id}/voucher?code=${code}&source=${source}`) - }else{ - dataVoucher = await odooApi('GET', `/api/v1/user/${id}/voucher?code=${code}`) + let dataVoucher = null; + if (source) { + dataVoucher = await odooApi( + 'GET', + `/api/v1/user/${id}/voucher?code=${code}&source=${source}` + ); + } else { + dataVoucher = await odooApi( + 'GET', + `/api/v1/user/${id}/voucher?code=${code}` + ); } - return dataVoucher -} + return dataVoucher; +}; export const getVoucherNew = async (source) => { diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 9bc0257e..09a791ee 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -44,9 +44,16 @@ const Checkout = () => { const auth = useAuth(); const [activeVoucher, SetActiveVoucher] = useState(null); - - const { data: cartCheckout } = useQuery('cartCheckout-' + activeVoucher, () => - getProductsCheckout(activeVoucher, query) + const [activeVoucherShipping, setActiveVoucherShipping] = useState(null); + + const { data: cartCheckout } = useQuery( + ['cartCheckout', activeVoucher, activeVoucherShipping], + () => + getProductsCheckout({ + source: query, + voucher: activeVoucher, + voucher_shipping: activeVoucherShipping, + }) ); const [selectedAddress, setSelectedAddress] = useState({ @@ -104,6 +111,7 @@ const Checkout = () => { const [bottomPopupTnC, SetBottomPopupTnC] = useState(null); const [itemTnC, setItemTnC] = useState(null); const [listVouchers, SetListVoucher] = useState(null); + const [listVoucherShippings, SetListVoucherShipping] = useState(null); const [discountVoucher, SetDiscountVoucher] = useState(0); const [codeVoucher, SetCodeVoucher] = useState(null); const [findCodeVoucher, SetFindVoucher] = useState(null); @@ -120,14 +128,23 @@ const Checkout = () => { const voucher = async () => { if (!listVouchers) { try { - let source = 'source=' + query; - let dataVoucher = await getVoucherNew(source); + setLoadingVoucher(true); + let dataVoucher = await getVoucher(auth?.id, { + source: query, + }); SetListVoucher(dataVoucher); + + let dataVoucherShipping = await getVoucher(auth?.id, { + source: query, + type: 'shipping', + }); + SetListVoucherShipping(dataVoucherShipping); } finally { setLoadingVoucher(false); } } }; + const VoucherCode = async (code) => { const source = 'code=' + code + '&source=' + query; // let dataVoucher = await findVoucher(code, auth.id, query); @@ -315,7 +332,7 @@ const Checkout = () => { useEffect(() => { const GT = cartCheckout?.grandTotal + - Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000; + Math.round(parseInt(finalShippingAmt * 1.1) / 1000) * 1000; const finalGT = GT < 0 ? 0 : GT; setGrandTotal(finalGT); }, [biayaKirim, cartCheckout?.grandTotal, activeVoucher]); @@ -361,6 +378,7 @@ const Checkout = () => { delivery_service_type: selectedExpedisiService, flash_sale: hasFlashSale, // dibuat negasi untuk ngetest kebalikan nilai false voucher: activeVoucher, + voucher_shipping: activeVoucherShipping, type: 'sale_order', }; @@ -460,6 +478,11 @@ const Checkout = () => { return false; }, [products]); + const voucherShippingAmt = cartCheckout?.discountVoucherShipping || 0; + const discShippingAmt = Math.min(biayaKirim, voucherShippingAmt); + + const finalShippingAmt = biayaKirim - discShippingAmt; + return ( <> <BottomPopup @@ -569,8 +592,145 @@ const Checkout = () => { </div> )} - <hr className='mt-10 my-4 border-gray_r-10' /> - <div className=''> + <hr className='mt-8 mb-4 border-gray_r-8' /> + + {listVoucherShippings && listVoucherShippings?.length > 0 && ( + <div> + <h3 className='font-semibold mb-4'>Promo Gratis Ongkir</h3> + {listVoucherShippings?.map((item) => ( + <div key={item.id} className='relative'> + <div + className={`border border-solid mb-5 w-full hover:cursor-pointer p-2 pl-4 pr-4 `} + > + {item.canApply && ( + <div + class='p-2 mb-4 text-sm text-green-800 rounded-lg bg-green-50 dark:text-green-400' + role='alert' + > + <p> + Potensi potongan sebesar{' '} + <span className='text-green font-bold'> + {currencyFormat(item.discountVoucher)} + </span> + </p> + </div> + )} + {!item.canApply && ( + <div + class='p-2 mb-4 text-sm text-red-800 rounded-lg bg-red-50' + role='alert' + onClick={() => handlingTnC(item)} + > + <p> + Voucher tidak bisa di gunakan,{' '} + <span className='text-red font-bold'> + Baca Selengkapnya ! + </span> + </p> + </div> + )} + + <div className={`flex gap-x-3 relative`}> + {item.canApply === false && ( + <div className='absolute w-full h-full bg-gray_r-3/40 top-0 left-0 z-50' /> + )} + <div className='hidden md:w-[250px] md:block'> + <Image + src={item.image} + alt={item.name} + className={`object-cover`} + /> + </div> + <div className='w-full'> + <div className='flex justify-between gap-x-2 mb-1 items-center'> + <div className=''> + <h3 className='font-semibold'>{item.name}</h3> + <div className='mt-1'> + <span className='text-sm line-clamp-3'> + {item.description}{' '} + </span> + </div> + </div> + <div className='flex justify-end'> + <label class='relative inline-flex items-center cursor-pointer'> + <input + type='checkbox' + value='' + class='sr-only peer' + checked={activeVoucherShipping === item.code} + onChange={() => + setActiveVoucherShipping( + activeVoucherShipping === item.code + ? null + : item.code + ) + } + /> + <div class="w-11 h-6 bg-gray-200 rounded-full peer dark:bg-gray-700 peer-focus:ring-4 peer-focus:ring-green-300 dark:peer-focus:ring-green-800 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-green-600"></div> + </label> + </div> + </div> + <hr className='mt-2 my-2 border-gray_r-8' /> + <div className='flex justify-between items-center'> + <p className='text-justify text-sm md:text-xs'> + Kode Voucher :{' '} + <span className='text-red-500 font-semibold'> + {item.code} + </span> + </p> + <p className='text-sm md:text-xs'> + {activeVoucher === item.code && ( + <span className=' text-green-600'> + Voucher digunakan{' '} + </span> + )} + </p> + </div> + <div className='flex items-center mt-3'> + <svg + aria-hidden='true' + fill='none' + stroke='currentColor' + stroke-width='1.5' + viewBox='0 0 24 24' + className='w-5 text-black' + > + <path + d='M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z' + stroke-linecap='round' + stroke-linejoin='round' + ></path> + </svg> + <div className='flex justify-between items-center'> + <div className='text-left ml-3 text-sm '> + Berakhir dalam{' '} + <span className='text-red-600'> + {item.remainingTime} + </span>{' '} + lagi,{' '} + </div> + <div + className='text-sm ml-2 text-red-600' + onClick={() => handlingTnC(item)} + > + Baca S&K + </div> + </div> + </div> + </div> + </div> + <div className='mt-3'> + <p className='text-justify text-sm '></p> + </div> + </div> + </div> + ))} + </div> + )} + + <hr className='mt-8 mb-4 border-gray_r-8' /> + + <div> {!loadingVoucher && listVouchers?.length === 0 ? ( <div className='flex items-center justify-center mt-4 mb-4'> <div className='text-center'> @@ -755,14 +915,7 @@ const Checkout = () => { </div> </div> <div className='mt-3'> - <p className='text-justify text-sm '> - {/* {item.canApply === false - ? 'Tambah ' + - currencyFormat(item.differenceToApply) + - ' untuk pakai promo ini' - : 'Potensi potongan sebesar ' + - currencyFormat(hitungDiscountVoucher(item.code))} */} - </p> + <p className='text-justify text-sm '></p> </div> </div> </div> @@ -928,14 +1081,18 @@ const Checkout = () => { </div> <div className='flex gap-x-2 justify-between'> <div className='text-gray_r-11'> - Biaya Kirim <p className='text-xs mt-3'>{etdFix}</p> - </div> - <div> - {currencyFormat( - Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 - )} + Biaya Kirim <p className='text-xs mt-1'>{etdFix}</p> </div> + <div>{currencyFormat(biayaKirim)}</div> </div> + {activeVoucherShipping && voucherShippingAmt && ( + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>Diskon Kirim</div> + <div className='text-danger-500'> + - {currencyFormat(discShippingAmt)} + </div> + </div> + )} </div> )} @@ -964,7 +1121,7 @@ const Checkout = () => { <div className='mt-4 mb-4'> <button type='button' - onClick={() => { + onClick={async () => { SetBottomPopup(true); voucher(); }} @@ -1220,14 +1377,18 @@ const Checkout = () => { <div className='flex gap-x-2 justify-between'> <div className='text-gray_r-11'> Biaya Kirim - <p className='text-xs mt-3'>{etdFix}</p> - </div> - <div> - {currencyFormat( - Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 - )} + <p className='text-xs mt-1'>{etdFix}</p> </div> + <div>{currencyFormat(biayaKirim)}</div> </div> + {activeVoucherShipping && voucherShippingAmt && ( + <div className='flex gap-x-2 justify-between'> + <div className='text-gray_r-11'>Diskon Kirim</div> + <div className='text-danger-500'> + - {currencyFormat(discShippingAmt)} + </div> + </div> + )} </div> )} diff --git a/src/lib/flashSale/components/FlashSaleNonDisplay.jsx b/src/lib/flashSale/components/FlashSaleNonDisplay.jsx new file mode 100644 index 00000000..8dc15b05 --- /dev/null +++ b/src/lib/flashSale/components/FlashSaleNonDisplay.jsx @@ -0,0 +1,66 @@ +import Image from 'next/image'; +import { useEffect, useState } from 'react'; + +import CountDown from '@/core/components/elements/CountDown/CountDown'; +import productSearchApi from '@/lib/product/api/productSearchApi'; +import ProductSlider from '@/lib/product/components/ProductSlider'; + +import flashSaleApi from '../api/flashSaleApi'; +import { FlashSaleSkeleton } from '../skeleton/FlashSaleSkeleton'; + +const FlashSaleNonDisplay = () => { + const [flashSales, setFlashSales] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const loadFlashSales = async () => { + const dataFlashSales = await flashSaleApi(); + setFlashSales(dataFlashSales); + setIsLoading(false); + }; + loadFlashSales(); + }, []); + + if (isLoading) { + return <FlashSaleSkeleton />; + } + + return ( + flashSales?.length > 0 && ( + <div className='px-4 sm:px-0 grid grid-cols-1 gap-y-8'> + {flashSales.map((flashSale, index) => ( + <div key={index}> + <div className='flex gap-x-3 mb-4 justify-between sm:justify-start'> + <div className='font-medium sm:text-h-lg mt-1.5'> + Penawaran Terbatas + </div> + </div> + + <div className='relative'> + <FlashSaleProduct flashSaleId={flashSale.pricelistId} /> + </div> + </div> + ))} + </div> + ) + ); +}; + +const FlashSaleProduct = ({ flashSaleId }) => { + const [products, setProducts] = useState(null); + + useEffect(() => { + const loadProducts = async () => { + const dataProducts = await productSearchApi({ + query: `fq=-flashsale_id_i:${flashSaleId}&fq=flashsale_price_f:[1 TO *]&limit=500&orderBy=flashsale-discount-desc`, + operation: 'AND', + }); + setProducts(dataProducts.response); + }; + loadProducts(); + }, [flashSaleId]); + + return <ProductSlider products={products} />; +}; + +export default FlashSaleNonDisplay; diff --git a/src/lib/home/api/CategoryPilihanApi.js b/src/lib/home/api/CategoryPilihanApi.js new file mode 100644 index 00000000..8a0b38d3 --- /dev/null +++ b/src/lib/home/api/CategoryPilihanApi.js @@ -0,0 +1,8 @@ +import odooApi from '@/core/api/odooApi' + +const categoryPilihanApi = async () => { + const dataCategoryPilihan = await odooApi('GET', '/api/v1/lob_homepage') + return dataCategoryPilihan +} + +export default categoryPilihanApi diff --git a/src/lib/home/api/categoryManagementApi.js b/src/lib/home/api/categoryManagementApi.js new file mode 100644 index 00000000..b70d60ce --- /dev/null +++ b/src/lib/home/api/categoryManagementApi.js @@ -0,0 +1,8 @@ +import odooApi from '@/core/api/odooApi' + +const categoryManagementApi = async () => { + const dataCategoryManagement = await odooApi('GET', '/api/v1/categories_management') + return dataCategoryManagement +} + +export default categoryManagementApi diff --git a/src/lib/home/components/CategoryDynamic.jsx b/src/lib/home/components/CategoryDynamic.jsx new file mode 100644 index 00000000..0cc43d91 --- /dev/null +++ b/src/lib/home/components/CategoryDynamic.jsx @@ -0,0 +1,108 @@ +import React, { useEffect, useState } from 'react'; +import useCategoryManagement from '../hooks/useCategoryManagement'; +import NextImage from 'next/image'; +import Link from "next/link"; +import { createSlug } from '@/core/utils/slug'; +import odooApi from '@/core/api/odooApi'; +import { Skeleton} from '@chakra-ui/react' + +const CategoryDynamic = () => { + const { categoryManagement } = useCategoryManagement(); + const [categoryData, setCategoryData] = useState({}); + const [subCategoryData, setSubCategoryData] = useState({}); + + useEffect(() => { + const fetchCategoryData = async () => { + if (categoryManagement && categoryManagement.data) { + const updatedCategoryData = {}; + const updatedSubCategoryData = {}; + + for (const category of categoryManagement.data) { + const countLevel1 = await odooApi('GET', `/api/v1/category/numFound?parent_id=${category.categoryIdI}`); + + updatedCategoryData[category.categoryIdI] = countLevel1?.numFound; + + for (const subCategory of countLevel1.children) { + updatedSubCategoryData[subCategory.id] = subCategory.numFound; + } + } + + setCategoryData(updatedCategoryData); + setSubCategoryData(updatedSubCategoryData); + } + }; + + fetchCategoryData(); + }, [categoryManagement.isLoading]); + + return ( + <div> + {categoryManagement && categoryManagement.data?.map((category) => { + const countLevel1 = categoryData[category.categoryIdI] || 0; + + return ( + <Skeleton key={category.id} isLoaded={categoryManagement}> + <div key={category.id}> + <div className='bagian-judul flex flex-row justify-start items-center gap-3 mb-4 mt-4'> + <div className='font-semibold sm:text-h-lg mr-2'>{category.name}</div> + <Skeleton isLoaded={countLevel1 !=0}> + <p className={`text-gray_r-10 text-sm`}>{countLevel1} Produk tersedia</p> + </Skeleton> + <Link href={createSlug('/shop/category/', category?.name, category?.categoryIdI)} className="!text-red-500 font-semibold">Lihat Semua</Link> + </div> + <div className='grid grid-cols-3 gap-2'> + {category.categories.map((subCategory) => { + const countLevel2 = subCategoryData[subCategory.idLevel2] || 0; + + return ( + <div key={subCategory.id} className='border rounded justify-start items-start'> + <div className='p-3'> + <div className='flex flex-row border rounded mb-2 justify-start items-center'> + <NextImage + src={subCategory.image ? subCategory.image : "/images/noimage.jpeg"} + alt={subCategory.name} + width={90} + height={30} + className='object-fit' + /> + <div className='bagian-judul flex flex-col justify-center items-start gap-2 ml-2'> + <div className='font-semibold text-lg mr-2'>{subCategory.name}</div> + <Skeleton isLoaded={countLevel2 != 0}> + <p className={`text-gray_r-10 text-sm`}> + {countLevel2} Produk tersedia + </p> + </Skeleton> + <Link href={createSlug('/shop/category/', subCategory?.name, subCategory?.idLevel2)} className="!text-red-500 font-semibold">Lihat Semua</Link> + </div> + </div> + <div className='grid grid-cols-2 gap-2 overflow-y-auto max-h-[240px]'> + {subCategory.childFrontendIdI.map((childCategory) => ( + <div key={childCategory.id}> + <Link href={createSlug('/shop/category/', childCategory?.name, childCategory?.idLevel3)} className="flex flex-row gap-2 border rounded group hover:border-red-500"> + <NextImage + src={childCategory.image ? childCategory.image : "/images/noimage.jpeg"} + alt={childCategory.name} + width={40} + height={40} + /> + <div className='bagian-judul flex flex-col justify-center items-center gap-2 break-words line-clamp-2 group-hover:text-red-500'> + <div className='font-semibold line-clamp-2 group-hover:text-red-500 text-sm mr-2'>{childCategory.name}</div> + </div> + </Link> + </div> + ))} + </div> + </div> + </div> + ); + })} + </div> + </div> + </Skeleton> + ); + })} + </div> + ); +}; + +export default CategoryDynamic; diff --git a/src/lib/home/components/CategoryDynamicMobile.jsx b/src/lib/home/components/CategoryDynamicMobile.jsx new file mode 100644 index 00000000..c1433a2d --- /dev/null +++ b/src/lib/home/components/CategoryDynamicMobile.jsx @@ -0,0 +1,101 @@ +import React, { useEffect, useState } from 'react'; +import useCategoryManagement from '../hooks/useCategoryManagement'; +import NextImage from 'next/image'; +import Link from "next/link"; +import { createSlug } from '@/core/utils/slug'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; + +const CategoryDynamicMobile = () => { + const { categoryManagement } = useCategoryManagement() + const [selectedCategory, setSelectedCategory] = useState({}); + + useEffect(() => { + const loadPromo = async () => { + try { + if (categoryManagement.data?.length > 0) { + const initialSelections = categoryManagement.data.reduce((acc, category) => { + if (category.categories.length > 0) { + acc[category.id] = category.categories[0].idLevel2; + } + return acc; + }, {}); + setSelectedCategory(initialSelections); + } + } catch (loadError) { + // console.error("Error loading promo items:", loadError); + } + }; + + loadPromo(); + }, [categoryManagement.data]); + + const handleCategoryLevel2Click = (categoryIdI, idLevel2) => { + setSelectedCategory(prev => ({ + ...prev, + [categoryIdI]: idLevel2 + })); + }; + + return ( + <div className='p-4'> + {categoryManagement.data && categoryManagement.data.map((category) => ( + <div key={category.id}> + <div className='bagian-judul flex flex-row justify-between items-center gap-3 mb-4 mt-4'> + <div className='font-semibold sm:text-h-sm mr-2'>{category.name}</div> + <Link href={createSlug('/shop/category/', category?.name, category?.categoryIdI)} className="!text-red-500 font-semibold text-sm">Lihat Semua</Link> + </div> + <Swiper slidesPerView={2.3} spaceBetween={10}> + {category.categories.map((index) => ( + <SwiperSlide key={index.id}> + <div + onClick={() => handleCategoryLevel2Click(category.id, index?.idLevel2)} + className={`border flex justify-start items-center max-w-48 max-h-16 rounded ${selectedCategory[category.id] === index?.idLevel2 ? 'bg-red-50 border-red-500 text-red-500' : 'border-gray-200 text-gray-900'}`} + > + <div className='p-1 flex justify-start items-center'> + <div className='flex flex-row justify-center items-center'> + <NextImage + src={index.image ? index.image : "/images/noimage.jpeg"} + alt={index.name} + width={30} + height={30} + className='object-' + /> + <div className='bagian-judul flex flex-col justify-center items-start gap-1 ml-2'> + <div className='font-semibold text-[10px] line-clamp-1'>{index.name}</div> + <p className='text-gray_r-10 text-[10px]'>999 rb+ Produk</p> + </div> + </div> + </div> + </div> + </SwiperSlide> + ))} + </Swiper> + <div className='p-3 mt-2 border'> + <div className='grid grid-cols-2 gap-2 overflow-y-auto max-h-[240px]'> + {category.categories.map((index) => ( + selectedCategory[category.id] === index?.idLevel2 && index.childFrontendIdI.map((x) => ( + <div key={x.id}> + <Link href={createSlug('/shop/category/', x?.name, x?.idLevel3)} className="flex flex-row gap-1 border rounded group hover:border-red-500"> + <NextImage + src={x.image ? x.image : "/images/noimage.jpeg"} + alt={x.name} + width={40} + height={40} + /> + <div className='bagian-judul flex flex-col justify-center items-start gap-1 break-words line-clamp-2 group-hover:text-red-500'> + <div className='font-semibold line-clamp-2 group-hover:text-red-500 text-[10px]'>{x.name}</div> + </div> + </Link> + </div> + )) + ))} + </div> + </div> + </div> + ))} + </div> + ); +}; + +export default CategoryDynamicMobile; diff --git a/src/lib/home/components/CategoryPilihan.jsx b/src/lib/home/components/CategoryPilihan.jsx new file mode 100644 index 00000000..6568621c --- /dev/null +++ b/src/lib/home/components/CategoryPilihan.jsx @@ -0,0 +1,120 @@ +import Image from 'next/image' +import useCategoryHome from '../hooks/useCategoryHome' +import Link from '@/core/components/elements/Link/Link' +import { createSlug } from '@/core/utils/slug' +import { useEffect, useState } from 'react'; +import { bannerApi } from '../../../api/bannerApi'; +const { useQuery } = require('react-query') +import { HeroBannerSkeleton } from '../../../components/skeleton/BannerSkeleton'; +import useCategoryPilihan from '../hooks/useCategoryPilihan'; +import useDevice from '@/core/hooks/useDevice' +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; + +const CategoryPilihan = ({ id, categories }) => { + const { isDesktop, isMobile } = useDevice() + const { categoryPilihan } = useCategoryPilihan(); + const heroBanner = useQuery('categoryPilihan', bannerApi({ type: 'banner-category-list' })); + return ( + <section> + {isDesktop && ( + <div> + <div className='flex flex-row items-center mb-4'> + <div className='font-semibold sm:text-h-lg mr-2'>LOB Kategori Pilihan</div> + <p className='text-gray_r-10 text-sm'>200 Rb+ Produk Unggulan & 800+ Brand Rekomendasi tersedia!</p> + </div> + {heroBanner.data && + heroBanner.data?.length > 0 && ( + <div className='flex w-full h-full justify-center mb-4 bg-cover bg-center'> + <Link key={heroBanner.data[0].id} href={heroBanner.data[0].url}> + <Image + width={1260} + height={170} + quality={100} + src={heroBanner.data[0].image} + alt={heroBanner.data[0].name} + className='h-full object-cover w-full' + /> + </Link> + </div> + )} + <div className="group/item grid grid-cols-6 gap-y-2 w-full h-full col-span-2 "> + {categoryPilihan?.data?.map((category) => ( + <div key={category.id} className="KartuInti h-48 w-60 max-w-sm lg:max-w-full flex flex-col border-[1px] border-gray-200 relative group"> + <div className='KartuB absolute h-48 w-60 inset-0 flex items-center justify-center '> + <div className="group/edit flex items-center justify-end h-48 w-60 flex-col group-hover/item:visible"> + <div className=' h-36 flex justify-end items-end'> + <Image className='group-hover:scale-105 transition-transform duration-300 ' src={category?.image? category?.image : '/images/noimage.jpeg'} width={120} height={120} alt={category?.name} /> + </div> + <h2 className="text-gray-700 content-center h-12 border-t-[1px] px-1 w-60 border-gray-200 font-normal text-sm text-center">{category?.industries}</h2> + </div> + </div> + <div className='KartuA relative inset-0 flex h-36 w-60 items-center justify-center opacity-0 group-hover:opacity-75 group-hover:bg-[#E20613] transition-opacity '> + <Link + href={createSlug('/shop/lob/', category?.industries, category?.id)} + className='category-mega-box__parent text-white rounded-lg' + > + Lihat semua + </Link> + </div> + </div> + ))} + </div> + </div> + )} + {isMobile && ( + <div className='p-4'> + <div className='flex flex-row items-center mb-4'> + <div className='font-semibold sm:text-h-md mr-2'>LOB Kategori Pilihan</div> + {/* <p className='text-gray_r-10 text-sm'>200 Rb+ Produk Unggulan & 800+ Brand Rekomendasi tersedia!</p> */} + </div> + <div className='flex'> + {heroBanner.data && + heroBanner.data?.length > 0 && ( + <div className=' object-fill '> + <Link key={heroBanner.data[0].id} href={heroBanner.data[0].url}> + <Image + width={439} + height={150} + quality={100} + src={heroBanner.data[0].image} + alt={heroBanner.data[0].name} + className='object-cover' + /> + </Link> + </div> + )} + </div> + <Swiper slidesPerView={2.1} spaceBetween={10}> + {categoryPilihan?.data?.map((category) => ( + <SwiperSlide key={category.id}> + <div key={category.id} className="KartuInti mt-2 h-48 w-48 max-w-sm lg:max-w-full flex flex-col border-[1px] border-gray-200 relative group"> + <div className='KartuB absolute h-48 w-48 inset-0 flex items-center justify-center '> + <div className="group/edit flex items-center justify-end h-48 w-48 flex-col group-hover/item:visible"> + <div className=' h-36 flex justify-end items-end'> + <Image className='group-hover:scale-105 transition-transform duration-300 ' src={category?.image? category?.image : '/images/noimage.jpeg'} width={120} height={120} alt={category?.name} /> + </div> + <h2 className="text-gray-700 content-center h-12 border-t-[1px] px-1 w-48 border-gray-200 font-normal text-sm text-center">{category?.industries}</h2> + </div> + </div> + <div className='KartuA relative inset-0 flex h-36 w-48 items-center justify-center opacity-0 group-hover:opacity-75 group-hover:bg-[#E20613] transition-opacity '> + <Link + href={createSlug('/shop/lob/', category?.industries, category?.id)} + className='category-mega-box__parent text-white rounded-lg' + > + Lihat semua + </Link> + </div> + </div> + </SwiperSlide> + ))} + + </Swiper> + + </div> + )} + </section> + ) +} + +export default CategoryPilihan diff --git a/src/lib/home/components/PreferredBrand.jsx b/src/lib/home/components/PreferredBrand.jsx index ec09aa4e..ae12505d 100644 --- a/src/lib/home/components/PreferredBrand.jsx +++ b/src/lib/home/components/PreferredBrand.jsx @@ -1,4 +1,5 @@ import { Swiper, SwiperSlide } from 'swiper/react' +import { Navigation, Pagination, Autoplay } from 'swiper'; import { useCallback, useEffect, useState } from 'react' import usePreferredBrand from '../hooks/usePreferredBrand' import PreferredBrandSkeleton from './Skeleton/PreferredBrandSkeleton' @@ -38,7 +39,23 @@ const PreferredBrand = () => { const { preferredBrands } = usePreferredBrand(query) const { isMobile, isDesktop } = useDevice() - + const swiperBanner = { + modules:[Navigation, Pagination, Autoplay], + autoplay: { + delay: 4000, + disableOnInteraction: false + }, + loop: true, + className: 'h-[70px] md:h-[100px] w-full', + slidesPerView: isMobile ? 4 : 8, + spaceBetween: isMobile ? 12 : 0, + pagination: { + dynamicBullets: true, + dynamicMainBullets: isMobile ? 6 : 8, + clickable: true, + } + } + const preferredBrandsData = manufactures ? manufactures.slice(0, 20) : [] return ( <div className='px-4 sm:px-0'> <div className='flex justify-between items-center mb-4'> @@ -49,18 +66,20 @@ const PreferredBrand = () => { </Link> )} </div> - {manufactures.isLoading && <PreferredBrandSkeleton />} - {!manufactures.isLoading && ( - <Swiper slidesPerView={isMobile ? 3.5 : 7.5} spaceBetween={isMobile ? 12 : 24} freeMode> - {manufactures.map((manufacture) => ( - <SwiperSlide key={manufacture.id}> - <BrandCard brand={manufacture} /> - </SwiperSlide> - ))} - </Swiper> - )} + <div className='border rounded border-gray_r-6'> + {manufactures.isLoading && <PreferredBrandSkeleton />} + {!manufactures.isLoading && ( + <Swiper {...swiperBanner}> + {preferredBrandsData.map((manufacture) => ( + <SwiperSlide key={manufacture.id}> + <BrandCard brand={manufacture} /> + </SwiperSlide> + ))} + </Swiper> + )} + </div> </div> ) } -export default PreferredBrand +export default PreferredBrand
\ No newline at end of file diff --git a/src/lib/home/components/PromotionProgram.jsx b/src/lib/home/components/PromotionProgram.jsx index b204df8e..99258d94 100644 --- a/src/lib/home/components/PromotionProgram.jsx +++ b/src/lib/home/components/PromotionProgram.jsx @@ -13,10 +13,14 @@ const BannerSection = () => { <div className='flex justify-between items-center mb-4 '> <div className='font-semibold sm:text-h-lg'>Promo Tersedia</div> {isDesktop && ( - <div></div> - // <Link href='/shop/promo' className='!text-red-500 font-semibold'> - // Lihat Semua - // </Link> + <Link href='/shop/promo' className='!text-red-500 font-semibold'> + Lihat Semua + </Link> + )} + {isMobile && ( + <Link href='/shop/promo' className='!text-red-500 font-semibold sm:text-h-sm'> + Lihat Semua + </Link> )} </div> {isDesktop && (promotionProgram.data && diff --git a/src/lib/home/hooks/useCategoryManagement.js b/src/lib/home/hooks/useCategoryManagement.js new file mode 100644 index 00000000..c1dda585 --- /dev/null +++ b/src/lib/home/hooks/useCategoryManagement.js @@ -0,0 +1,13 @@ +import categoryManagementApi from '../api/categoryManagementApi' +import { useQuery } from 'react-query' + +const useCategoryManagement = () => { + const fetchCategoryManagement = async () => await categoryManagementApi() + const { isLoading, data } = useQuery('categoryManagementApi', fetchCategoryManagement) + + return { + categoryManagement: { data, isLoading } + } +} + +export default useCategoryManagement
\ No newline at end of file diff --git a/src/lib/home/hooks/useCategoryPilihan.js b/src/lib/home/hooks/useCategoryPilihan.js new file mode 100644 index 00000000..12a86f7e --- /dev/null +++ b/src/lib/home/hooks/useCategoryPilihan.js @@ -0,0 +1,13 @@ +import categoryPilihanApi from '../api/CategoryPilihanApi' +import { useQuery } from 'react-query' + +const useCategoryPilihan = () => { + const fetchCategoryPilihan = async () => await categoryPilihanApi() + const { isLoading, data } = useQuery('categoryPilihanApi', fetchCategoryPilihan) + + return { + categoryPilihan: { data, isLoading } + } +} + +export default useCategoryPilihan
\ No newline at end of file diff --git a/src/lib/lob/components/Breadcrumb.jsx b/src/lib/lob/components/Breadcrumb.jsx new file mode 100644 index 00000000..5722fd39 --- /dev/null +++ b/src/lib/lob/components/Breadcrumb.jsx @@ -0,0 +1,55 @@ +import odooApi from '@/core/api/odooApi' +import { createSlug } from '@/core/utils/slug' +import { + Breadcrumb as ChakraBreadcrumb, + BreadcrumbItem, + BreadcrumbLink, + Skeleton +} from '@chakra-ui/react' +import Link from 'next/link' +import React from 'react' +import { useQuery } from 'react-query' + +/** + * Render a breadcrumb component. + * + * @param {object} categoryId - The ID of the category. + * @return {JSX.Element} The breadcrumb component. + */ +const Breadcrumb = ({ categoryId }) => { + const breadcrumbs = useQuery( + `lob-breadcrumbs/${categoryId}`, + async () => await odooApi('GET', `/api/v1/lob_homepage/${categoryId}/category_id`) + ) + return ( + <div className='container mx-auto py-4 md:py-6'> + <Skeleton isLoaded={!breadcrumbs.isLoading} className='w-2/3'> + <ChakraBreadcrumb> + <BreadcrumbItem> + <BreadcrumbLink as={Link} href='/' className='!text-danger-500 whitespace-nowrap'> + Home + </BreadcrumbLink> + </BreadcrumbItem> + + {breadcrumbs?.data?.map((category, index) => ( + <BreadcrumbItem key={index} isCurrentPage={index === breadcrumbs.data.length - 1}> + {index === breadcrumbs.data.length - 1 ? ( + <BreadcrumbLink className='whitespace-nowrap'>{category.industries}</BreadcrumbLink> + ) : ( + <BreadcrumbLink + as={Link} + href={createSlug('/shop/lob/', category.industries, category.id)} + className='!text-danger-500 whitespace-nowrap' + > + {category.industries} + </BreadcrumbLink> + )} + </BreadcrumbItem> + ))} + </ChakraBreadcrumb> + </Skeleton> + </div> + ) +} + +export default Breadcrumb diff --git a/src/lib/product/components/CategorySection.jsx b/src/lib/product/components/CategorySection.jsx new file mode 100644 index 00000000..2af3db10 --- /dev/null +++ b/src/lib/product/components/CategorySection.jsx @@ -0,0 +1,104 @@ +import Image from "next/image"; +import Link from 'next/link'; +import { createSlug } from '@/core/utils/slug'; +import useDevice from '@/core/hooks/useDevice'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; +import { useQuery } from 'react-query'; +import { useRouter } from 'next/router'; +import { + ChevronDownIcon, + ChevronUpIcon, // Import ChevronUpIcon for toggling + DocumentCheckIcon, + HeartIcon, +} from '@heroicons/react/24/outline'; +import { useState } from 'react'; // Import useState +import { getIdFromSlug } from '@/core/utils/slug' + +const CategorySection = ({ categories }) => { + const { isDesktop, isMobile } = useDevice(); + const [isOpenCategory, setIsOpenCategory] = useState(false); // State to manage category visibility + + const handleToggleCategories = () => { + setIsOpenCategory(!isOpenCategory); + }; + + + const displayedCategories = isOpenCategory ? categories : categories.slice(0, 10); + + return ( + <section> + {isDesktop && ( + <div className="group/item grid grid-cols-5 gap-y-2 gap-x-2 w-full h-full col-span-2 "> + {displayedCategories.map((category) => ( + <Link href={createSlug('/shop/category/', category?.name, category?.id)} key={category?.id} passHref> + <div className="group transition-colors duration-300 "> + <div className="KartuInti h-12 w-26 max-w-sm lg:max-w-full flex flex-col border-[2px] border-gray-200 group-hover:border-red-400 rounded relative "> + <div className="flex items-center justify-start h-full px-1 flex-row "> + <Image className="h-full" src={category?.image1920 ? category?.image1920 : '/images/noimage.jpeg'} width={56} height={48} alt={category?.name} /> + <h2 className="text-gray-700 group-hover:text-[#E20613] line-clamp-2 content-center h-fit w-60 px-1 font-semibold text-sm text-start">{category?.name}</h2> + </div> + </div> + </div> + </Link> + ))} + </div> + )} + {isDesktop && categories.length > 10 && ( + <div className="w-full flex justify-center mt-4"> + <button + onClick={handleToggleCategories} + className="flex justify-end mt-4 text-red-500 font-bold px-4 py-2 rounded" + > + {isOpenCategory ? 'Sembunyikan' : 'Lihat semua'} + {isOpenCategory ? ( + <ChevronUpIcon className="ml-auto w-5 font-bold" /> + ) : ( + <ChevronDownIcon className="ml-auto w-5 font-bold" /> + )} + </button> + </div> + )} + + {isMobile && ( + <div className="py-4"> + <Swiper slidesPerView={2.3} spaceBetween={10}> + {displayedCategories.map((category) => ( + <SwiperSlide key={category?.id}> + <Link href={createSlug('/shop/category/', category?.name, category?.id)} passHref> + <div className="group transition-colors duration-300"> + <div className="KartuInti min-h-16 max-h-16 w-26 max-w-sm lg:max-w-full flex flex-col border-[2px] border-gray-200 group-hover:bg-red-200 group-hover:border-red-400 rounded relative"> + <div className="flex items-center justify-center h-full px-1 flex-row"> + <Image + src={category?.image1920 ? category?.image1920 : '/images/noimage.jpeg'} + width={56} + height={48} + alt={category?.name} + /> + <h2 className="text-gray-700 group-hover:text-[#E20613] line-clamp-2 content-center h-fit w-60 px-1 font-semibold text-sm text-start"> + {category?.name} + </h2> + </div> + </div> + </div> + </Link> + </SwiperSlide> + ))} + </Swiper> + {categories.length > 10 && ( + <div className="w-full flex justify-end mt-4"> + <button + onClick={handleToggleCategories} + className="flex justify-end mt-4 bg-red-500 text-white text-sm px-4 py-2 rounded" + > + {isOpenCategory ? 'Sembunyikan Semua' : 'Lihat Semua'} + </button> + </div> + )} + </div> + )} + </section> + ) +} + +export default CategorySection diff --git a/src/lib/product/components/LobSectionCategory.jsx b/src/lib/product/components/LobSectionCategory.jsx new file mode 100644 index 00000000..03d6e8c0 --- /dev/null +++ b/src/lib/product/components/LobSectionCategory.jsx @@ -0,0 +1,81 @@ +import Image from "next/image"; +import Link from 'next/link'; +import { createSlug } from '@/core/utils/slug'; +import useDevice from '@/core/hooks/useDevice'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; +import { useQuery } from 'react-query'; +import { useRouter } from 'next/router'; +import { + ChevronDownIcon, + ChevronUpIcon, // Import ChevronUpIcon for toggling + DocumentCheckIcon, + HeartIcon, +} from '@heroicons/react/24/outline'; +import { useState } from 'react'; // Import useState +import { getIdFromSlug } from '@/core/utils/slug' + +const LobSectionCategory = ({ categories }) => { + const { isDesktop, isMobile } = useDevice(); + const [isOpenCategory, setIsOpenCategory] = useState(false); // State to manage category visibility + + const handleToggleCategories = () => { + setIsOpenCategory(!isOpenCategory); + }; + + const displayedCategories = categories[0]?.categoryIds; + + return ( + <section> + {isDesktop && ( + <div className="group/item grid grid-flow-col gap-y-2 gap-x-4 w-full h-full"> + {displayedCategories?.map((category) => ( + <Link + href={createSlug('/shop/category/', category?.name, category?.id)} + key={category?.id} + passHref + className="block hover:scale-105 transition-transform duration-300 bg-cover bg-center h-[144px]" + style={{ + backgroundImage: `url('${category?.image ? category?.image : 'https://erp.indoteknik.com/web/image?model=x_banner.banner&id=5&field=x_banner_image&unique=09202023100557'}')`, + }} + > + </Link> + ))} + </div> + )} + + {isMobile && ( + <div className="py-4"> + <Swiper slidesPerView={1.2} spaceBetween={10}> + {displayedCategories?.map((category) => ( + <SwiperSlide key={category?.id}> + <Link + href={createSlug('/shop/category/', category?.name, category?.id)} + key={category?.id} + passHref + className="block bg-cover bg-center h-[144px]" + style={{ + backgroundImage: `url('${category?.image ? category?.image : 'https://erp.indoteknik.com/web/image?model=x_banner.banner&id=5&field=x_banner_image&unique=09202023100557'}')`, + }} + > + </Link> + </SwiperSlide> + ))} + </Swiper> + {categories.length > 10 && ( + <div className="w-full flex justify-end mt-4"> + <button + onClick={handleToggleCategories} + className="flex justify-end mt-4 bg-red-500 text-white text-sm px-4 py-2 rounded" + > + {isOpenCategory ? 'Sembunyikan Semua' : 'Lihat Semua'} + </button> + </div> + )} + </div> + )} + </section> + ) +} + +export default LobSectionCategory diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx index 98732407..35e2a665 100644 --- a/src/lib/product/components/ProductCard.jsx +++ b/src/lib/product/components/ProductCard.jsx @@ -14,7 +14,15 @@ import useUtmSource from '~/hooks/useUtmSource'; const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { const router = useRouter(); const utmSource = useUtmSource(); + const [discount, setDiscount] = useState(0); + let voucherPastiHemat = 0; + + if (product?.voucherPastiHemat ? product?.voucherPastiHemat.length : voucherPastiHemat > 0) { + const stringVoucher = product?.voucherPastiHemat[0]; + const validJsonString = stringVoucher.replace(/'/g, '"'); + voucherPastiHemat = JSON.parse(validJsonString); + } const callForPriceWhatsapp = whatsappUrl('product', { name: product.name, @@ -38,42 +46,64 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { ), }; + const hitungDiscountVoucher = () => { + let countDiscount = 0; + if (voucherPastiHemat.discount_type === 'percentage') { + countDiscount = + product?.lowestPrice.priceDiscount * + (voucherPastiHemat.discount_amount / 100); + if ( + voucherPastiHemat.max_discount > 0 && + countDiscount > voucherPastiHemat.max_discount + ) { + countDiscount = voucherPastiHemat.max_discount; + } + } else { + countDiscount = voucherPastiHemat.discount_amount; + } + + setDiscount(countDiscount); + }; + + useEffect(() => { + hitungDiscountVoucher(); + }, []); + if (variant == 'vertical') { return ( - <div className='rounded shadow-sm border border-gray_r-4 bg-white h-[300px] md:h-[350px]'> + <div className='rounded shadow-sm border border-gray_r-4 bg-white h-[330px] md:h-[380px]'> <Link href={URL.product} className='border-b border-gray_r-4 relative'> - <div className="relative"> - <Image - src={image} - alt={product?.name} - className="gambarA w-full object-contain object-center h-36 sm:h-48" - /> - <div className="absolute top-0 right-0 flex mt-3"> - <div className="gambarB "> - {product?.isSni && ( - <ImageNext - src="/images/sni-logo.png" - alt="SNI Logo" - className="w-4 h-5 object-contain object-top sm:h-6" - width={50} - height={50} - /> - )} - </div> - <div className="gambarC "> - {product?.isTkdn && ( - <ImageNext - src="/images/TKDN.png" - alt="TKDN" - className="w-11 h-6 object-contain object-top ml-1 mr-1 sm:h-6" - width={50} - height={50} - /> - )} + <div className='relative'> + <Image + src={image} + alt={product?.name} + className='gambarA w-full object-contain object-center h-36 sm:h-48' + /> + <div className='absolute top-0 right-0 flex mt-3'> + <div className='gambarB '> + {product?.isSni && ( + <ImageNext + src='/images/sni-logo.png' + alt='SNI Logo' + className='w-4 h-5 object-contain object-top sm:h-6' + width={50} + height={50} + /> + )} + </div> + <div className='gambarC '> + {product?.isTkdn && ( + <ImageNext + src='/images/TKDN.png' + alt='TKDN' + className='w-11 h-6 object-contain object-top ml-1 mr-1 sm:h-6' + width={50} + height={50} + /> + )} + </div> </div> </div> - </div> - {router.pathname != '/' && product?.flashSale?.id > 0 && ( <div className='absolute bottom-0 w-full grid'> @@ -178,6 +208,13 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { )} </div> )} + {discount > 0 && product?.flashSale?.id < 1 && ( + <div className='flex gap-x-1 mb-1 text-sm'> + <div className='inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20'> + Voucher : {currencyFormat(discount)} + </div> + </div> + )} <div className='flex w-full items-center gap-x-1 '> {product?.stockTotal > 0 && ( @@ -200,37 +237,37 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { <div className='flex bg-white'> <div className='w-4/12'> <Link href={URL.product} className='relative'> - <div className="relative"> - <Image - src={image} - alt={product?.name} - className="gambarA w-full object-contain object-center h-36 sm:h-48" - /> - <div className="absolute top-0 right-0 flex mt-3"> - <div className="gambarB "> - {product?.isSni && ( - <ImageNext - src="/images/sni-logo.png" - alt="SNI Logo" - className="w-4 h-5 object-contain object-top sm:h-6" - width={50} - height={50} - /> - )} - </div> - <div className="gambarC "> - {product?.isTkdn && ( - <ImageNext - src="/images/TKDN.png" - alt="TKDN" - className="w-11 h-6 object-contain object-top ml-1 sm:h-6" - width={50} - height={50} - /> - )} + <div className='relative'> + <Image + src={image} + alt={product?.name} + className='gambarA w-full object-contain object-center h-36 sm:h-48' + /> + <div className='absolute top-0 right-0 flex mt-3'> + <div className='gambarB '> + {product?.isSni && ( + <ImageNext + src='/images/sni-logo.png' + alt='SNI Logo' + className='w-4 h-5 object-contain object-top sm:h-6' + width={50} + height={50} + /> + )} + </div> + <div className='gambarC '> + {product?.isTkdn && ( + <ImageNext + src='/images/TKDN.png' + alt='TKDN' + className='w-11 h-6 object-contain object-top ml-1 sm:h-6' + width={50} + height={50} + /> + )} + </div> </div> </div> - </div> {product.variantTotal > 1 && ( <div className='absolute badge-gray bottom-1.5 left-1.5'> {product.variantTotal} Varian @@ -319,6 +356,14 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { </div> )} + {discount > 0 && product?.flashSale?.id < 1 && ( + <div className='flex gap-x-1 mb-1 text-sm'> + <div className='inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20'> + Voucher : {currencyFormat(discount)} + </div> + </div> + )} + <div className='flex w-full items-center gap-x-1 '> {product?.stockTotal > 0 && ( <div className='badge-solid-red'>Ready Stock</div> diff --git a/src/lib/product/components/ProductFilterDesktop.jsx b/src/lib/product/components/ProductFilterDesktop.jsx index a8073036..1933c5f0 100644 --- a/src/lib/product/components/ProductFilterDesktop.jsx +++ b/src/lib/product/components/ProductFilterDesktop.jsx @@ -107,7 +107,11 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu const slug = Array.isArray(router.query.slug) ? router.query.slug[0] : router.query.slug; if (slug) { - router.push(`${prefixUrl}/${slug}?${params}`) + if(prefixUrl.includes('category') || prefixUrl.includes('lob')){ + router.push(`${prefixUrl}?${params}`) + }else{ + router.push(`${prefixUrl}/${slug}?${params}`) + } } else { router.push(`${prefixUrl}?${params}`) } diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx index b1a5d409..a83e5e1e 100644 --- a/src/lib/product/components/ProductSearch.jsx +++ b/src/lib/product/components/ProductSearch.jsx @@ -26,6 +26,10 @@ import ProductSearchSkeleton from './Skeleton/ProductSearchSkeleton'; import SideBanner from '~/modules/side-banner'; import FooterBanner from '~/modules/footer-banner'; +import CategorySection from './CategorySection'; +import LobSectionCategory from './LobSectionCategory'; +import { getIdFromSlug } from '@/core/utils/slug' +import { data } from 'autoprefixer'; const ProductSearch = ({ query, @@ -37,11 +41,109 @@ const ProductSearch = ({ const { page = 1 } = query; const [q, setQ] = useState(query?.q || '*'); const [search, setSearch] = useState(query?.q || '*'); + const [finalQuery, setFinalQuery] = useState({}); + const [queryFinal, setQueryFinal] = useState({}); + const [dataCategoriesProduct, setDataCategoriesProduct] = useState([]) + const [dataCategoriesLob, setDataCategoriesLob] = useState([]) + const categoryId = getIdFromSlug(prefixUrl) + const [data, setData] = useState([]) + const [dataLob, setDataLob] = useState([]); const [limit, setLimit] = useState(query?.limit || 30); - const [orderBy, setOrderBy] = useState(router.query?.orderBy || 'popular'); + const [orderBy, setOrderBy] = useState(router.query?.orderBy); if (defaultBrand) query.brand = defaultBrand.toLowerCase(); + const dataIdCategories = [] + useEffect(() => { + if(prefixUrl.includes('category')){ + const loadProduct = async () => { + const getCategoriesId = await odooApi('GET', `/api/v1/category/numFound?parent_id=${categoryId}`); + if (getCategoriesId) { + setDataCategoriesProduct(getCategoriesId); + } + }; + loadProduct(); + }else if(prefixUrl.includes('lob')){ + const loadProduct = async () => { + const lobData = await odooApi('GET', `/api/v1/lob_homepage/${categoryId}/category_id`); + + if (lobData) { + setDataLob(lobData); + } + }; + loadProduct(); + + } + }, [categoryId]); + + const collectIds = (category) => { + const ids = []; + function recurse(cat) { + if (cat && cat.id) { + ids.push(cat.id); + } + if (Array.isArray(cat.children)) { + cat.children.forEach(recurse); + } + } + recurse(category); + return ids; + }; + + useEffect(() => { + if(prefixUrl.includes('category')){ + const ids = collectIds(dataCategoriesProduct); + const newQuery = { + fq: `category_id_ids:(${ids.join(' OR ')})`, + page : router.query.page? router.query.page : 1, + brand : router.query.brand? router.query.brand : '', + category : router.query.category? router.query.category : '', + priceFrom : router.query.priceFrom? router.query.priceFrom : '', + priceTo : router.query.priceTo? router.query.priceTo : '', + limit : router.query.limit? router.query.limit : '', + orderBy : router.query.orderBy? router.query.orderBy : '' + }; + setFinalQuery(newQuery); + } else if (prefixUrl.includes('lob')){ + + const fetchCategoryData = async () => { + if (dataLob[0]?.categoryIds) { + + for (const cate of dataLob[0].categoryIds) { + + dataIdCategories.push(cate.childId) + } + + + const mergedArray = dataIdCategories.flat(); + + const newQuery = { + fq: `category_id_ids:(${mergedArray.join(' OR ')})`, + category : router.query.category? router.query.category : '', + page : router.query.page? router.query.page : 1, + brand : router.query.brand? router.query.brand : '', + priceFrom : router.query.priceFrom? router.query.priceFrom : '', + priceTo : router.query.priceTo? router.query.priceTo : '', + limit : router.query.limit? router.query.limit : '', + orderBy : router.query.orderBy? router.query.orderBy : '' + }; + + setFinalQuery(newQuery); + + } + }; + fetchCategoryData(); + } + }, [dataCategoriesProduct, dataLob]); + + useEffect(() => { + if (prefixUrl.includes('category') || prefixUrl.includes('lob')) { + setQueryFinal({ ...finalQuery, q, limit, orderBy }); + } else { + setQueryFinal({ ...query, q, limit, orderBy }); + } + }, [prefixUrl,dataCategoriesProduct, query, finalQuery]); + const { productSearch } = useProductSearch({ - query: { ...query, q, limit, orderBy }, + query: queryFinal, operation: 'AND', }); const [products, setProducts] = useState(null); @@ -53,22 +155,24 @@ const ProductSearch = ({ const numRows = [30, 50, 80, 100]; const [brandValues, setBrand] = useState( !router.pathname.includes('brands') - ? query.brand - ? query.brand.split(',') + ? router.query.brand + ? router.query.brand.split(',') : [] : [] ); const [categoryValues, setCategory] = useState( - query?.category?.split(',') || [] + router.query?.category?.split(',') || router.query?.category?.split(',') ); - const [priceFrom, setPriceFrom] = useState(query?.priceFrom || null); - const [priceTo, setPriceTo] = useState(query?.priceTo || null); + + const [priceFrom, setPriceFrom] = useState(router.query?.priceFrom || null); + const [priceTo, setPriceTo] = useState(router.query?.priceTo || null); const pageCount = Math.ceil(productSearch.data?.response.numFound / limit); const productStart = productSearch.data?.responseHeader.params.start; const productRows = limit; const productFound = productSearch.data?.response.numFound; - + const [dataCategories, setDataCategories] = useState([]) + useEffect(() => { if (productFound == 0 && query.q && !spellings) { searchSpellApi({ query: query.q }).then((response) => { @@ -98,7 +202,7 @@ const ProductSearch = ({ }); } }, [productFound, query, spellings]); - + let id = [] useEffect(() => { const checkIfBrand = async () => { const brand = await axios( @@ -115,6 +219,21 @@ const ProductSearch = ({ checkIfBrand(); } }, [q]); + + useEffect(() => { + if(prefixUrl.includes('category')){ + const loadCategories = async () => { + const getCategories = await odooApi('GET', `/api/v1/category/child?parent_id=${categoryId}`) + if(getCategories){ + setDataCategories(getCategories) + } + } + loadCategories() + } + }, []) + + + const brands = []; for ( @@ -148,6 +267,7 @@ const ProductSearch = ({ const orderOptions = [ + { value: '', label: 'Pilih Filter' }, { value: 'price-asc', label: 'Harga Terendah' }, { value: 'price-desc', label: 'Harga Tertinggi' }, { value: 'popular', label: 'Populer' }, @@ -223,7 +343,7 @@ const ProductSearch = ({ q: router.query.q, orderBy: orderBy, brand: brandValues.join(','), - category: categoryValues.join(','), + category: categoryValues?.join(','), priceFrom, priceTo, }; @@ -262,7 +382,6 @@ const ProductSearch = ({ }; const isNotReadyStockPage = router.asPath !== '/shop/search?orderBy=stock'; - return ( <> <MobileView> @@ -323,6 +442,8 @@ const ProductSearch = ({ SpellingComponent )} </div> + <LobSectionCategory categories={dataLob}/> + <CategorySection categories={dataCategories}/> {productFound > 0 && ( <div className='flex items-center gap-x-2 mb-5 justify-between'> @@ -363,6 +484,7 @@ const ProductSearch = ({ pageCount={pageCount} currentPage={parseInt(page)} url={`${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} + // url={prefixUrl.includes('category') || prefixUrl.includes('lob')? `${prefixUrl}?${toQuery(_.omit(finalQuery, ['page']))}` : `${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} className='mt-6 mb-2' /> @@ -411,7 +533,10 @@ const ProductSearch = ({ <SideBanner /> </div> + <div className='w-9/12 pl-6'> + <LobSectionCategory categories={dataLob}/> + <CategorySection categories={dataCategories}/> {bannerPromotionHeader && bannerPromotionHeader?.image && ( <div className='mb-3'> <Image @@ -549,6 +674,7 @@ const ProductSearch = ({ pageCount={pageCount} currentPage={parseInt(page)} url={`${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} + // url={prefixUrl.includes('category') || prefixUrl.includes('lob')? `${prefixUrl}?${toQuery(_.omit(finalQuery, ['page']))}` : `${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} className='!justify-end' /> </div> diff --git a/src/lib/quotation/components/Quotation.jsx b/src/lib/quotation/components/Quotation.jsx index 8855c6c4..0ad042de 100644 --- a/src/lib/quotation/components/Quotation.jsx +++ b/src/lib/quotation/components/Quotation.jsx @@ -9,6 +9,7 @@ import _ from 'lodash'; import { deleteItemCart, getCart, getItemCart } from '@/core/utils/cart'; import currencyFormat from '@/core/utils/currencyFormat'; import { toast } from 'react-hot-toast'; +import { useProductCartContext } from '@/contexts/ProductCartContext'; // import checkoutApi from '@/lib/checkout/api/checkoutApi' import { useRouter } from 'next/router'; import VariantGroupCard from '@/lib/variant/components/VariantGroupCard'; @@ -38,11 +39,12 @@ const { getProductsCheckout } = require('@/lib/checkout/api/checkoutApi'); const Quotation = () => { const router = useRouter(); const auth = useAuth(); - + const { data: cartCheckout } = useQuery('cartCheckout', () => getProductsCheckout() - ); +); +const { setRefreshCart } = useProductCartContext(); const SELF_PICKUP_ID = 32; const [products, setProducts] = useState(null); @@ -67,17 +69,19 @@ const Quotation = () => { const [selectedExpedisiService, setselectedExpedisiService] = useState(null); const [etd, setEtd] = useState(null); const [etdFix, setEtdFix] = useState(null); - + const [isApproval, setIsApproval] = useState(false); - + const expedisiValidation = useRef(null); - + const [selectedAddress, setSelectedAddress] = useState({ shipping: null, invoicing: null, }); - + const [addresses, setAddresses] = useState(null); + + const [note_websiteText, setselectedNote_websiteText] = useState(''); useEffect(() => { if (!auth) return; @@ -262,6 +266,12 @@ const Quotation = () => { } if (!products || products.length == 0) return; + + if (isApproval && note_websiteText == '') { + toast.error('Maaf, Note wajib dimasukkan.'); + return; + } + setIsLoading(true); const productOrder = products.map((product) => ({ product_id: product.id, @@ -276,16 +286,19 @@ const Quotation = () => { carrier_id: selectedCarrierId, estimated_arrival_days: splitDuration(etd), delivery_service_type: selectedExpedisiService, + note_website : note_websiteText, }; - console.log('data checkout', data); + const isSuccess = await checkoutApi({ data }); - console.log('isSuccess', isSuccess); + ; setIsLoading(false); if (isSuccess?.id) { for (const product of products) deleteItemCart({ productId: product.id }); router.push(`/shop/quotation/finish?id=${isSuccess.id}`); + setRefreshCart(true); return; } + toast.error('Gagal melakukan transaksi, terjadi kesalahan internal'); }; @@ -442,8 +455,25 @@ const Quotation = () => { </Link>{' '} yang berlaku </p> + <hr className='my-4 border-gray_r-6' /> + + <div className='flex gap-x-2 justify-start mb-4'> + <div className=''>Note</div> + {isApproval && ( + <div className='text-caption-1 text-red-500 items-center flex'>*harus diisi</div> + )} + </div> + <div className='text-caption-2 text-gray_r-11'> + <textarea + rows="4" + cols="50" + className={`w-full p-1 rounded border border-gray_r-6`} + onChange={(e) => setselectedNote_websiteText(e.target.value)} + /> + </div> </div> - + + <Divider /> <div className='flex gap-x-3 p-4'> @@ -576,6 +606,27 @@ const Quotation = () => { yang berlaku </p> + <div> + <hr className='my-4 border-gray_r-6' /> + + <div className='flex gap-x-1 flex-col mb-4'> + <div className='flex flex-row gap-x-1'> + <div className=''>Note</div> + {isApproval && ( + <div className='text-caption-1 text-red-500 items-center flex'>*harus diisi</div> + )} + </div> + <div className='text-caption-2 text-gray_r-11'> + <textarea + rows="4" + cols="50" + className={`w-full p-1 rounded border border-gray_r-6`} + onChange={(e) => setselectedNote_websiteText(e.target.value)} + /> + </div> + </div> + </div> + <hr className='my-4 border-gray_r-6' /> <button diff --git a/src/lib/quotation/components/Quotationheader.jsx b/src/lib/quotation/components/Quotationheader.jsx new file mode 100644 index 00000000..4529c977 --- /dev/null +++ b/src/lib/quotation/components/Quotationheader.jsx @@ -0,0 +1,265 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +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 Image from '@/core/components/elements/Image/Image'; +import whatsappUrl from '@/core/utils/whatsappUrl'; +import { AnimatePresence, motion } from 'framer-motion'; +import style from '../../../../src-migrate/modules/cart/styles/item-promo.module.css'; +import useTransactions from '../../transaction/hooks/useTransactions'; +import currencyFormat from '@/core/utils/currencyFormat'; +const { DocumentCheckIcon, PhotoIcon } = require('@heroicons/react/24/outline'); +const { default: Link } = require('next/link'); + +const Quotationheader = (quotationCount) => { + const auth = useAuth(); + const query = { + context: 'quotation', + site: auth?.webRole === null && auth?.site ? auth.site : null, + }; + + const router = useRouter(); + const [subTotal, setSubTotal] = useState(null); + const [buttonLoading, SetButtonTerapkan] = useState(false); + const itemLoading = [1, 2, 3]; + const [countQuotation, setCountQuotation] = useState(null); + const { productCart, setProductCart, refreshCart, setRefreshCart, isLoading, setIsloading, productQuotation, setProductQuotation } = + useProductCartContext(); + + const [isHovered, setIsHovered] = useState(false); + const [isTop, setIsTop] = useState(true); + + const qotation = useMemo(() => { + return productQuotation || []; + }, [productQuotation]); + + const handleMouseEnter = () => { + setIsHovered(true); + getCart(); + }; + + const handleMouseLeave = () => { + setIsHovered(false); + }; + + const getCart = () => { + if (!productQuotation && auth) { + refreshCartf(); + } + }; + let { transactions } = useTransactions({ query }); + + const refreshCartf = useCallback(async () => { + setIsloading(true); + let pendingTransactions = transactions?.data?.saleOrders.filter(transaction => transaction.status === 'draft'); + setProductQuotation(pendingTransactions); + setCountQuotation(pendingTransactions?.length ? pendingTransactions?.length : pendingTransactions?.length); + setIsloading(false); + }, [setProductQuotation, setIsloading]); + + useEffect(() => { + if (!qotation) return + + let calculateTotalDiscountAmount = 0 + for (const product of qotation) { + // if (qotation.quantity == '') continue + calculateTotalDiscountAmount += product.amountUntaxed + } + let subTotal = calculateTotalDiscountAmount + setSubTotal(subTotal) + }, [qotation]) + + useEffect(() => { + if (refreshCart) { + refreshCartf(); + } + setRefreshCart(false); + }, [ refreshCartf, setRefreshCart]); + + useEffect(() => { + setCountQuotation(quotationCount.quotationCount); + setProductQuotation(quotationCount.data); + }, [quotationCount]); + + useEffect(() => { + const handleScroll = () => { + setIsTop(window.scrollY === 0); + }; + window.addEventListener('scroll', handleScroll); + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, []); + + const handleCheckout = async () => { + SetButtonTerapkan(true); + let checkoutAll = await odooApi('POST', `/api/v1/user/${auth.id}/cart/select-all`); + router.push('/my/quotations'); + }; + + return ( + <div className='relative group'> + <div> + <Link + href='/my/quotations' + target='_blank' + rel='noreferrer' + className='flex items-center gap-x-2 !text-gray_r-12/80' + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + > + <div className={`relative ${countQuotation > 0 && 'mr-2'}`}> + <DocumentCheckIcon className='w-7' /> + {countQuotation > 0 && ( + <span className='absolute -top-2 -right-2 badge-solid-red rounded-full w-5 h-5 flex items-center justify-center'> + {countQuotation} + </span> + )} + </div> + <span> + List + <br /> + Quotation + </span> + </Link> + </div> + <AnimatePresence> + {isHovered && ( + <> + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 1, top: isTop ? 230 : 155 }} + exit={{ opacity: 0 }} + transition={{ duration: 0.15, top: { duration: 0.3 } }} + className={`fixed left-0 w-full h-full bg-black/50 z-10`} + /> + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 1, transition: { duration: 0.2 } }} + exit={{ opacity: 0, transition: { duration: 0.3 } }} + className='absolute z-10 left-0 w-96' + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + > + <motion.div + initial={{ height: 0 }} + animate={{ height: 'auto' }} + exit={{ height: 0 }} + className='w-full max-w-md p-2 bg-white border border-gray-200 rounded-lg shadow overflow-hidden' + > + <div className='p-2 flex justify-between items-center'> + <h5 className='text-base font-semibold leading-none'>Daftar Quotation</h5> + </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 Quotation Anda + </p> + </div> + )} + {isLoading && + itemLoading.map((item) => ( + <div key={item} role='status' className='max-w-sm animate-pulse'> + <div className='flex items-center space-x-4 mb- 2'> + <div className='flex-shrink-0'> + <PhotoIcon className='h-16 w-16 text-gray-500' /> + </div> + <div className='flex-1 min-w-0'> + <div className='h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4'></div> + <div className='h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px] mb-2.5'></div> + <div className='h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5'></div> + </div> + </div> + </div> + ))} + {auth && qotation.length === 0 && !isLoading && ( + <div className='justify-center p-4'> + <p className='text-gray-500 text-center '> + Tidak Ada Quotation + </p> + </div> + )} + {auth && qotation.length > 0 && !isLoading && ( + <> + <ul role='list' className='divide-y divide-gray-200 dark:divide-gray-700'> + {qotation && + qotation?.map((product, index) => ( + <> + <li className='py-1 sm:py-2'> + <div className='flex justify-between border p-2 flex-col gap-y-2 hover:border-red-500'> + <Link + href={`/my/quotations/${product?.id}`} + className='hover:border-red-500' + > + <div className='flex justify-between mb-2'> + <div className='flex flex-row items-center'> + <p className='tanggal text-xs opacity-80 mr-[2px]'>Sales : </p> + <p className='tanggal text-xs text-red-500 font-semibold'>{product.sales}</p> + </div> + <div className='flex flex-row items-center'> + <p className='text-xs opacity-80 mr-[2px]'>Status :</p> + <p className='badge-red h-fit text-xs whitespace-nowrap'>Pending Quotation</p> + </div> + </div> + <div className='flex justify-between mb-2'> + <div className='flex flex-col items-start'> + <p className=' text-xs opacity-80 mr-[2px]'>No. Transaksi</p> + <p className=' text-sm text-red-500 font-semibold'> {product.name}</p> + </div> + <div className='flex flex-col items-end'> + <p className='text-xs opacity-80 mr-[2px]'>No. Purchase Order</p> + <p className='font-semibold text-sm text-red-500'> {product.purchaseOrderName ? product.purchaseOrderName : '-'}</p> + </div> + </div> + {/* <div className='my-0.5 h-0.5 bg-gray-200'></div> */} + <hr className='mt-3 mb-3 border border-gray-100' /> + <div className='bagian bawah flex justify-between mt-2'> + <p className='font-semibold text-sm'>Total</p> + <p className='font-semibold text-sm'>{currencyFormat(product.amountUntaxed)}</p> + </div> + </Link> + </div> + </li> + </> + ))} + </ul> + <hr /> + </> + )} + </div> + {auth && qotation.length > 0 && !isLoading && ( + <> + <div className='mt-3 ml-1'> + <span className='text-gray-400 text-caption-2'>Subtotal Sebelum PPN : </span> + <span className='font-semibold text-red-600'>{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...' : 'Lihat Semua'} + </button> + </div> + </> + )} + </motion.div> + </motion.div> + </> + )} + </AnimatePresence> + </div> + ); +}; + +export default Quotationheader; diff --git a/src/lib/transaction/api/rejectProductApi.js b/src/lib/transaction/api/rejectProductApi.js new file mode 100644 index 00000000..e03c7975 --- /dev/null +++ b/src/lib/transaction/api/rejectProductApi.js @@ -0,0 +1,9 @@ +import odooApi from '@/core/api/odooApi' + +const rejectProductApi = async ({ idSo, idProduct, reason }) => { + const dataCheckout = await odooApi('POST', `/api/v1/sale_order/${idSo}/reject/${idProduct}`, { + reason_reject: reason + }); + return dataCheckout +} +export default rejectProductApi
\ No newline at end of file diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx index c6152ca9..85f6163b 100644 --- a/src/lib/transaction/components/Transaction.jsx +++ b/src/lib/transaction/components/Transaction.jsx @@ -1,4 +1,6 @@ import Spinner from '@/core/components/elements/Spinner/Spinner'; +import NextImage from 'next/image'; +import rejectImage from '../../../../public/images/reject.png'; import useTransaction from '../hooks/useTransaction'; import TransactionStatusBadge from './TransactionStatusBadge'; import Divider from '@/core/components/elements/Divider/Divider'; @@ -34,8 +36,14 @@ import useAuth from '@/core/hooks/useAuth'; import StepApproval from './stepper'; import aprpoveApi from '../api/approveApi'; import rejectApi from '../api/rejectApi'; +import rejectProductApi from '../api/rejectProductApi'; +import { useRouter } from 'next/router'; const Transaction = ({ id }) => { + const router = useRouter(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedProduct, setSelectedProduct] = useState(null); + const [reason, setReason] = useState(''); const auth = useAuth(); const { transaction } = useTransaction({ id }); @@ -141,6 +149,18 @@ const Transaction = ({ id }) => { [transaction.data] ); + const memoizeVariantGroupCardReject = useMemo( + () => ( + <div className='p-4 pt-0 flex flex-col gap-y-3'> + <VariantGroupCard + variants={transaction.data?.productsRejectLine} + buyMore + /> + </div> + ), + [transaction.data] + ); + if (transaction.isLoading) { return ( <div className='flex justify-center my-6'> @@ -153,6 +173,37 @@ const Transaction = ({ id }) => { setIdAWB(null); }; + const openModal = (product) => { + setSelectedProduct(product); + setIsModalOpen(true); + }; + + const closeModal = () => { + setIsModalOpen(false); + setSelectedProduct(null); + setReason(''); + }; + + const handleRejectProduct = async () => { + try { + if (!reason.trim()) { + toast.error('Masukkan alasan terlebih dahulu'); + return; + } else { + let idSo = transaction?.data.id; + let idProduct = selectedProduct?.id; + await rejectProductApi({ idSo, idProduct, reason }); + closeModal(); + toast.success('Produk berhasil di reject'); + setTimeout(() => { + window.location.reload(); + }, 1500); + } + } catch (error) { + toast.error('Gagal reject produk. Silakan coba lagi.'); + } + }; + return ( transaction.data?.name && ( <> @@ -301,40 +352,6 @@ const Transaction = ({ id }) => { <Divider /> - {!auth?.feature.soApproval && ( - <div className='p-4 flex flex-col gap-y-4'> - <DescriptionRow label='Purchase Order'> - {transaction.data?.purchaseOrderName || '-'} - </DescriptionRow> - <div className='flex items-center'> - <p className='text-gray_r-11 leading-none'>Dokumen PO</p> - <button - type='button' - className='btn-light py-1.5 px-3 ml-auto' - onClick={ - transaction.data?.purchaseOrderFile - ? () => downloadPurchaseOrder(transaction.data) - : openUploadPo - } - > - {transaction.data?.purchaseOrderFile ? 'Download' : 'Upload'} - </button> - </div> - </div> - )} - - <Divider /> - - <div className='font-medium p-4'>Detail Produk</div> - - {memoizeVariantGroupCard} - - <Divider /> - - <SectionAddress address={transaction.data?.address} /> - - <Divider /> - <div className='p-4'> <p className='font-medium'>Invoice</p> <div className='flex flex-col gap-y-3 mt-4'> @@ -366,6 +383,58 @@ const Transaction = ({ id }) => { <Divider /> + {!auth?.feature.soApproval && ( + <div className='p-4 flex flex-col gap-y-4'> + <DescriptionRow label='Purchase Order'> + {transaction.data?.purchaseOrderName || '-'} + </DescriptionRow> + <div className='flex items-center'> + <p className='text-gray_r-11 leading-none'>Dokumen PO</p> + <button + type='button' + className='inline-block text-danger-500' + onClick={ + transaction.data?.purchaseOrderFile + ? () => downloadPurchaseOrder(transaction.data) + : transaction?.data.invoices.length < 1 + ? openUploadPo + : '' + } + > + {transaction?.data?.purchaseOrderFile + ? 'Download' + : transaction?.data.invoices.length < 1 + ? 'Upload' + : '-'} + </button> + </div> + </div> + )} + + <Divider /> + + <div className='font-medium p-4'>Detail Produk</div> + {transaction?.data?.products.length > 0 ? ( + <div>{memoizeVariantGroupCard}</div> + ) : ( + <div className='badge-red text-sm px-2 ml-4'> + Semua produk telah di reject + </div> + )} + + {transaction?.data?.productsRejectLine.length > 0 && ( + <div> + <div className='font-medium p-4'>Detail Produk Reject</div> + {memoizeVariantGroupCardReject} + </div> + )} + + <Divider /> + + <SectionAddress address={transaction.data?.address} /> + + <Divider /> + <div className='p-4 pt-0'> {transaction.data?.status == 'draft' && auth?.feature.soApproval && ( @@ -533,12 +602,16 @@ const Transaction = ({ id }) => { onClick={ transaction.data?.purchaseOrderFile ? () => downloadPurchaseOrder(transaction.data) - : openUploadPo + : transaction?.data.invoices.length < 1 + ? openUploadPo + : '' } > {transaction?.data?.purchaseOrderFile ? 'Download' - : 'Upload'} + : transaction?.data.invoices.length < 1 + ? 'Upload' + : '-'} </button> </div> </> @@ -562,183 +635,333 @@ const Transaction = ({ id }) => { /> </div> </div> - - <div className='text-h-sm font-semibold mt-10 mb-4'> - Pengiriman - </div> - <div className='grid grid-cols-3 gap-1'> - {transaction?.data?.pickings?.map((airway) => ( - <button - className='shadow rounded-md p-3 text-gray_r-12 font-normal flex justify-between items-center text-left' - key={airway?.id} - onClick={() => setIdAWB(airway?.id)} - > - <div> - <span className='text-sm text-gray_r-11'> - No Resi : {airway?.trackingNumber || '-'}{' '} - </span> - <p className='mt-1 font-medium'>{airway?.name}</p> - </div> - <div className='flex gap-x-2'> - <div className='text-sm text-gray-600 badge-green leading-[1.5] mt-1'> - {airway?.delivered ? 'Pesanan Tiba' : 'Sedang Dikirim'} - </div> - <ChevronRightIcon className='w-5 stroke-2' /> + <div className='flex '> + <div className='w-1/2'> + <div className='text-h-sm font-semibold mt-10 mb-4'> + Pengiriman + </div> + {transaction?.data?.pickings.length == 0 && ( + <div className='badge-red text-sm'> + Belum ada pengiriman </div> - </button> - ))} + )} + <div className='grid grid-cols-1 gap-1 w-2/3'> + {transaction?.data?.pickings?.map((airway) => ( + <button + className='shadow rounded-md p-3 text-gray_r-12 font-normal flex justify-between items-center text-left h-20' + key={airway?.id} + onClick={() => setIdAWB(airway?.id)} + > + <div> + <span className='text-sm text-gray_r-11'> + No Resi : {airway?.trackingNumber || '-'}{' '} + </span> + <p className='mt-1 font-medium'>{airway?.name}</p> + </div> + <div className='flex gap-x-2'> + <div className='text-sm text-gray-600 badge-green leading-[1.5] mt-1'> + {airway?.delivered + ? 'Pesanan Tiba' + : 'Sedang Dikirim'} + </div> + <ChevronRightIcon className='w-5 stroke-2' /> + </div> + </button> + ))} + </div> + </div> + <div className='invoice w-1/2 '> + <div className='text-h-sm font-semibold mt-10 mb-4 '> + Invoice + </div> + {transaction.data?.invoices?.length === 0 && ( + <div className='badge-red text-sm'>Belum ada invoice</div> + )} + <div className='grid grid-cols-1 gap-1 w-2/3 '> + {transaction.data?.invoices?.map((invoice, index) => ( + <Link href={`/my/invoices/${invoice.id}`} key={index}> + <div className='shadow rounded-md p-4 text-gray_r-12 font-normal flex justify-between'> + <div> + <p className='mb-1'>{invoice?.name}</p> + <div className='flex items-center gap-x-1'> + {invoice.amountResidual > 0 ? ( + <div className='badge-red'>Belum Lunas</div> + ) : ( + <div className='badge-green'>Lunas</div> + )} + <p className='text-caption-2 text-gray_r-11'> + {currencyFormat(invoice.amountTotal)} + </p> + </div> + </div> + <ChevronRightIcon className='w-5 stroke-2' /> + </div> + </Link> + ))} + </div> + </div> </div> - {transaction?.data?.pickings.length == 0 && ( - <div className='badge-red text-sm'>Belum ada pengiriman</div> - )} - <div className='text-h-sm font-semibold mt-10 mb-4'> + <div className='text-h-sm font-semibold mt-4 mb-4'> Rincian Pembelian </div> - <table className='table-data'> - <thead> - <tr> - <th>Nama Produk</th> - {/* <th>Diskon</th> */} - <th>Jumlah</th> - <th>Harga</th> - <th>Subtotal</th> - </tr> - </thead> - <tbody> - {transaction?.data?.products?.map((product) => ( - <tr key={product.id}> - <td className='flex'> - <Link - href={createSlug( - '/shop/product/', - product?.parent.name, - product?.parent.id - )} - className='w-[20%] flex-shrink-0' - > - <div className='relative'> - <Image - src={product?.parent?.image} - alt={product?.name} - className='object-contain object-center border border-gray_r-6 h-32 w-full rounded-md' - /> - <div className='absolute top-0 right-4 flex mt-3'> - <div className='gambarB '> - {product.isSni && ( - <ImageNext - src='/images/sni-logo.png' - alt='SNI Logo' - className='w-2 h-4 object-contain object-top sm:h-4' - width={50} - height={50} - /> - )} - </div> - <div className='gambarC '> - {product.isTkdn && ( - <ImageNext - src='/images/TKDN.png' - alt='TKDN' - className='w-5 h-4 object-contain object-top ml-1 sm:h-4' - width={50} - height={50} - /> - )} - </div> - </div> - </div> - </Link> - <div className='px-2 text-left'> + {transaction?.data?.products?.length > 0 ? ( + <table className='table-data'> + <thead> + <tr> + <th>Nama Produk</th> + {/* <th>Diskon</th> */} + <th>Jumlah</th> + <th>Harga</th> + <th>Subtotal</th> + <th></th> + </tr> + </thead> + <tbody> + {transaction?.data?.products?.map((product) => ( + <tr key={product.id}> + <td className='flex'> <Link href={createSlug( '/shop/product/', product?.parent.name, product?.parent.id )} - className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' + className='w-[20%] flex-shrink-0' > - {product?.parent?.name} + <div className='relative'> + <Image + src={product?.parent?.image} + alt={product?.name} + className='object-contain object-center border border-gray_r-6 h-32 w-full rounded-md' + /> + <div className='absolute top-0 right-4 flex mt-3'> + <div className='gambarB '> + {product.isSni && ( + <ImageNext + src='/images/sni-logo.png' + alt='SNI Logo' + className='w-2 h-4 object-contain object-top sm:h-4' + width={50} + height={50} + /> + )} + </div> + <div className='gambarC '> + {product.isTkdn && ( + <ImageNext + src='/images/TKDN.png' + alt='TKDN' + className='w-5 h-4 object-contain object-top ml-1 sm:h-4' + width={50} + height={50} + /> + )} + </div> + </div> + </div> </Link> - <div className='text-gray_r-11 mt-2'> - {product?.code}{' '} - {product?.attributes.length > 0 - ? `| ${product?.attributes.join(', ')}` - : ''} + <div className='px-2 text-left'> + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' + > + {product?.parent?.name} + </Link> + <div className='text-gray_r-11 mt-2'> + {product?.code}{' '} + {product?.attributes.length > 0 + ? `| ${product?.attributes.join(', ')}` + : ''} + </div> </div> - </div> - </td> - {/* <td> - {product.price.discountPercentage > 0 - ? `${product.price.discountPercentage}%` - : ''} - </td> */} - <td>{product.quantity}</td> - <td> - {/* {product.price.discountPercentage > 0 && ( - <div className='line-through mb-1 text-caption-1 text-gray_r-12/70'> - {currencyFormat(product.price.price)} + </td> + {/* <td> + {product.price.discountPercentage > 0 + ? `${product.price.discountPercentage}%` + : ''} + </td> */} + <td>{product.quantity}</td> + <td> + {/* {product.price.discountPercentage > 0 && ( + <div className='line-through mb-1 text-caption-1 text-gray_r-12/70'> + {currencyFormat(product.price.price)} + </div> + )} */} + <div> + {currencyFormat(product.price.priceDiscount)} </div> - )} */} - <div>{currencyFormat(product.price.priceDiscount)}</div> - </td> - <td>{currencyFormat(product.price.subtotal)}</td> - </tr> - ))} - </tbody> - </table> - - <div className='flex justify-end mt-4'> - <div className='w-1/4 grid grid-cols-2 gap-y-3 text-gray_r-12/80'> - <div className='text-right'>Subtotal</div> - <div className='text-right font-medium'> - {currencyFormat(transaction.data?.amountUntaxed)} - </div> + </td> + <td>{currencyFormat(product.price.subtotal)}</td> + {/* {auth?.feature.soApproval && (auth.webRole == 2 || auth.webRole == 3) && (transaction.data.isReaject == false) && ( */} + {auth?.feature.soApproval && + (auth.webRole == 2 || auth.webRole == 3) && + router.asPath.includes('/my/quotations/') && + transaction.data?.status == 'draft' && ( + <td> + <button + className='bg-red-500 text-white py-1 px-3 rounded' + onClick={() => openModal(product)} + > + Reject + </button> + </td> + )} + </tr> + ))} + </tbody> + </table> + ) : ( + <div className='badge-red text-sm'> + Semua produk telah di reject + </div> + )} - <div className='text-right'>PPN 11%</div> - <div className='text-right font-medium'> - {currencyFormat(transaction.data?.amountTax)} + {isModalOpen && ( + <div className='fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center'> + <div + className='bg-white p-4 rounded w-96 + ease-in-out opacity-100 + transform transition-transform duration-300 scale-100' + > + <h2 className='text-lg mb-2'>Berikan Alasan</h2> + <textarea + value={reason} + onChange={(e) => setReason(e.target.value)} + className='w-full p-2 border rounded' + rows='4' + ></textarea> + <div className='mt-4 flex justify-end'> + <button + className='bg-gray-300 text-black py-1 px-3 rounded mr-2' + onClick={closeModal} + > + Batal + </button> + <button + className='bg-red-500 text-white py-1 px-3 rounded' + onClick={handleRejectProduct} + > + Reject + </button> + </div> </div> + </div> + )} - <div className='text-right whitespace-nowrap'> - Biaya Pengiriman - </div> - <div className='text-right font-medium'> - {currencyFormat(transaction.data?.deliveryAmount)} - </div> + {transaction?.data?.products?.map((product) => ( + <div className='flex justify-end mt-4' key={product.id}> + <div className='w-1/4 grid grid-cols-2 gap-y-3 text-gray_r-12/80'> + <div className='text-right'>Subtotal</div> + <div className='text-right font-medium'> + {currencyFormat(transaction.data?.amountUntaxed)} + </div> + + <div className='text-right'>PPN 11%</div> + <div className='text-right font-medium'> + {currencyFormat(transaction.data?.amountTax)} + </div> - <div className='text-right'>Grand Total</div> - <div className='text-right font-medium text-gray_r-12'> - {currencyFormat(transaction.data?.amountTotal)} + <div className='text-right whitespace-nowrap'> + Biaya Pengiriman + </div> + <div className='text-right font-medium'> + {currencyFormat(transaction.data?.deliveryAmount)} + </div> + + <div className='text-right'>Grand Total</div> + <div className='text-right font-medium text-gray_r-12'> + {currencyFormat(transaction.data?.amountTotal)} + </div> </div> </div> - </div> + ))} - <div className='text-h-sm font-semibold mt-10 mb-4'>Invoice</div> - <div className='grid grid-cols-3 gap-4'> - {transaction.data?.invoices?.map((invoice, index) => ( - <Link href={`/my/invoices/${invoice.id}`} key={index}> - <div className='shadow rounded-md p-4 text-gray_r-12 font-normal flex justify-between'> - <div> - <p className='mb-2'>{invoice?.name}</p> - <div className='flex items-center gap-x-1'> - {invoice.amountResidual > 0 ? ( - <div className='badge-red'>Belum Lunas</div> - ) : ( - <div className='badge-green'>Lunas</div> - )} - <p className='text-caption-2 text-gray_r-11'> - {currencyFormat(invoice.amountTotal)} - </p> - </div> - </div> - <ChevronRightIcon className='w-5 stroke-2' /> - </div> - </Link> - ))} - </div> - {transaction.data?.invoices?.length === 0 && ( - <div className='badge-red text-sm'>Belum ada invoice</div> + {transaction?.data?.productsRejectLine.length > 0 && ( + <div className='text-h-sm font-semibold mt-10 mb-4'> + Rincian Produk Reject + </div> + )} + {transaction?.data?.productsRejectLine.length > 0 && ( + <table className='table-data'> + <thead> + <tr> + <th>Nama Produk</th> + {/* <th>Diskon</th> */} + <th>Jumlah</th> + <th>Harga</th> + <th>Subtotal</th> + </tr> + </thead> + <tbody> + {transaction?.data?.productsRejectLine?.map((product) => ( + <tr key={product.id}> + <td className='flex'> + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='w-[20%] flex-shrink-0' + > + <Image + src={product?.parent?.image} + alt={product?.name} + className='object-contain object-center border border-gray_r-6 h-32 w-full rounded-md' + /> + </Link> + <div className='px-2 text-left'> + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' + > + {product?.parent?.name} + </Link> + <div className='text-gray_r-11 mt-2'> + {product?.code}{' '} + {product?.attributes.length > 0 + ? `| ${product?.attributes.join(', ')}` + : ''} + </div> + </div> + </td> + {/* <td> + {product.price.discountPercentage > 0 + ? `${product.price.discountPercentage}%` + : ''} + </td> */} + <td>{product.quantity}</td> + <td> + {/* {product.price.discountPercentage > 0 && ( + <div className='line-through mb-1 text-caption-1 text-gray_r-12/70'> + {currencyFormat(product.price.price)} + </div> + )} */} + <div> + {currencyFormat(product.price.priceDiscount)} + </div> + </td> + <td className='flex justify-center'> + <NextImage + src={rejectImage} + alt='Reject' + width={90} + height={30} + /> + </td> + </tr> + ))} + </tbody> + </table> )} </div> </div> diff --git a/src/lib/variant/components/VariantCard.jsx b/src/lib/variant/components/VariantCard.jsx index 9f65fc3c..68cdf54f 100644 --- a/src/lib/variant/components/VariantCard.jsx +++ b/src/lib/variant/components/VariantCard.jsx @@ -1,18 +1,26 @@ import { useRouter } from 'next/router' import { toast } from 'react-hot-toast' - +import useAuth from '@/core/hooks/useAuth'; import Image from '@/core/components/elements/Image/Image' import Link from '@/core/components/elements/Link/Link' import { createSlug } from '@/core/utils/slug' import currencyFormat from '@/core/utils/currencyFormat' import { updateItemCart } from '@/core/utils/cart' import whatsappUrl from '@/core/utils/whatsappUrl' +import {useState } from 'react'; +import rejectProductApi from '../../../lib/transaction/api/rejectProductApi' +// import {useTransaction} from 'C:\Users\Indoteknik\next-indoteknik\src\lib\transaction\hooks\useTransaction.js' +import useTransaction from '../../../lib/transaction/hooks/useTransaction'; import ImageNext from 'next/image'; -import { useMemo, useEffect, useState } from 'react'; const VariantCard = ({ product, openOnClick = true, buyMore = false }) => { const router = useRouter() - + const id = router.query.id + const auth = useAuth(); + const { transaction } = useTransaction({id}); + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedProduct, setSelectedProduct] = useState(null); + const [reason, setReason] = useState(''); @@ -24,11 +32,42 @@ const VariantCard = ({ product, openOnClick = true, buyMore = false }) => { }) return } - + const checkoutItem = () => { router.push(`/shop/checkout?product_id=${product.id}&qty=${product.quantity}`) } - + + const openModal = (product) => { + setSelectedProduct(product); + setIsModalOpen(true); + }; + + const closeModal = () => { + setIsModalOpen(false); + setSelectedProduct(null); + setReason(''); + }; + + const handleRejectProduct = async () => { + try { + if (!reason.trim()) { + toast.error('Masukkan alasan terlebih dahulu'); + return; + }else{ + let idSo = transaction?.data.id + let idProduct = selectedProduct.id + await rejectProductApi({ idSo, idProduct, reason}); + closeModal(); + toast.success("Produk berhasil di reject") + setTimeout(() => { + window.location.reload(); + }, 1500); + } + } catch (error) { + toast.error('Gagal reject produk. Silakan coba lagi.'); + } + }; + const Card = () => ( <div className='flex gap-x-3'> <div className='w-4/12 flex items-center gap-x-2'> @@ -115,7 +154,7 @@ const VariantCard = ({ product, openOnClick = true, buyMore = false }) => { <Link href={createSlug('/shop/product/', product.parent.name, product.parent.id)}> <Card /> </Link> - {buyMore && ( + {buyMore && (!transaction?.data?.productsRejectLine.some(pr => pr.id === product.id)) && ( <div className='flex justify-end gap-x-2 mb-2'> <button type='button' @@ -124,14 +163,48 @@ const VariantCard = ({ product, openOnClick = true, buyMore = false }) => { > Tambah Keranjang </button> - <button - type='button' - onClick={checkoutItem} - className='btn-solid-red py-2 px-3 text-caption-1' - > - Beli Lagi - </button> + {/* {auth?.feature.soApproval && (auth.webRole == 2 || auth.webRole == 3) && (transaction.data.isReaject == false) && ( */} + {auth?.feature.soApproval && (auth.webRole == 2 || auth.webRole == 3) && (router.asPath.includes("/my/quotations/")) && ( + !transaction?.data?.productsRejectLine.some(pr => pr.id === product.id) && ( + <button + className="bg-red-500 text-white py-1 px-3 rounded" + onClick={() => openModal(product)} + > + Reject + </button> + ) + )} + {isModalOpen && ( + <div className='fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center'> + <div className='bg-white p-4 rounded w-96 + ease-in-out opacity-100 + transform transition-transform duration-300 scale-100'> + <h2 className='text-lg mb-2'>Berikan Alasan</h2> + <textarea + value={reason} + onChange={(e) => setReason(e.target.value)} + className='w-full p-2 border rounded' + rows='4' + ></textarea> + <div className='mt-4 flex justify-end'> + <button + className='bg-gray-300 text-black py-1 px-3 rounded mr-2' + onClick={closeModal} + > + Batal + </button> + <button + className='bg-red-500 text-white py-1 px-3 rounded' + onClick={handleRejectProduct} + > + Reject + </button> + </div> + </div> + </div> + )} </div> + )} </> ) diff --git a/src/pages/api/shop/product-homepage.js b/src/pages/api/shop/product-homepage.js index 02c01ee0..61732c77 100644 --- a/src/pages/api/shop/product-homepage.js +++ b/src/pages/api/shop/product-homepage.js @@ -36,7 +36,8 @@ const respoonseMap = (productHomepage, products) => { name: productHomepage.name_s, image: productHomepage.image_s, url: productHomepage.url_s, - products: products + products: products, + categoryIds: productHomepage.category_id_ids, } return productMapped diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index b6b8c795..87d62a1a 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -3,6 +3,7 @@ import axios from 'axios'; import camelcaseObjectDeep from 'camelcase-object-deep'; export default async function handler(req, res) { + const { q = '*', page = 1, @@ -20,6 +21,9 @@ export default async function handler(req, res) { let paramOrderBy = ''; switch (orderBy) { + case 'flashsale-discount-desc': + paramOrderBy += 'flashsale_discount_f DESC'; + break; case 'price-asc': paramOrderBy += 'price_tier1_v2_f ASC'; break; @@ -39,10 +43,13 @@ export default async function handler(req, res) { paramOrderBy += 'flashsale_price_f ASC'; break; default: - paramOrderBy += 'product_rating_f DESC, price_discount_f DESC'; + paramOrderBy += ''; break; } - + + let checkQ = q.trim().split(/\s+/); + let newQ = checkQ.length > 1 ? escapeSolrQuery(q) + '*' : escapeSolrQuery(q); + let offset = (page - 1) * limit; let parameter = [ 'facet.field=manufacture_name_s', @@ -51,14 +58,15 @@ export default async function handler(req, res) { 'indent=true', `facet.query=${escapeSolrQuery(q)}`, `q.op=${operation}`, - `q=${escapeSolrQuery(q)}`, + `q=${newQ}`, 'qf=name_s', `start=${parseInt(offset)}`, `rows=${limit}`, `sort=${paramOrderBy}`, - `fq=-publish_b:false`, + `fq=-publish_b:false, product_rating_f:[8 TO *], price_tier1_v2_f:[1 TO *]`, ]; + if (priceFrom > 0 || priceTo > 0) { parameter.push( `fq=price_tier1_v2_f:[${priceFrom == '' ? '*' : priceFrom} TO ${ @@ -77,7 +85,10 @@ export default async function handler(req, res) { parameter.push( `fq=${brand .split(',') - .map((manufacturer) => `manufacture_name:"${encodeURIComponent(manufacturer)}"`) + .map( + (manufacturer) => + `manufacture_name:"${encodeURIComponent(manufacturer)}"` + ) .join(' OR ')}` ); if (category) @@ -99,6 +110,7 @@ export default async function handler(req, res) { let result = await axios( process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&') ); + try { result.data.response.products = productMappingSolr( result.data.response.docs, diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 8af963fb..d649ee17 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -1,6 +1,5 @@ import dynamic from 'next/dynamic'; -import { useRef } from 'react'; - +import { useEffect, useRef, useState } from 'react'; import { HeroBannerSkeleton } from '@/components/skeleton/BannerSkeleton'; import { PopularProductSkeleton } from '@/components/skeleton/PopularProductSkeleton'; import Seo from '@/core/components/Seo'; @@ -11,8 +10,10 @@ import { FlashSaleSkeleton } from '@/lib/flashSale/skeleton/FlashSaleSkeleton'; import PreferredBrandSkeleton from '@/lib/home/components/Skeleton/PreferredBrandSkeleton'; import PromotinProgram from '@/lib/promotinProgram/components/HomePage'; import PagePopupIformation from '~/modules/popup-information'; -import useProductDetail from '~/modules/product-detail/stores/useProductDetail'; +import CategoryPilihan from '../lib/home/components/CategoryPilihan'; +import odooApi from '@/core/api/odooApi'; import { getAuth } from '~/libs/auth'; +// import { getAuth } from '~/libs/auth'; const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout') @@ -44,9 +45,9 @@ const FlashSale = dynamic( } ); -const ProgramPromotion = dynamic(() => - import('@/lib/home/components/PromotionProgram') -); +// const ProgramPromotion = dynamic(() => +// import('@/lib/home/components/PromotionProgram') +// ); const BannerSection = dynamic(() => import('@/lib/home/components/BannerSection') @@ -54,12 +55,23 @@ const BannerSection = dynamic(() => const CategoryHomeId = dynamic(() => import('@/lib/home/components/CategoryHomeId') ); + +const CategoryDynamic = dynamic(() => + import('@/lib/home/components/CategoryDynamic') +); + +const CategoryDynamicMobile = dynamic(() => +import('@/lib/home/components/CategoryDynamicMobile') +); + const CustomerReviews = dynamic(() => import('@/lib/review/components/CustomerReviews') ); const ServiceList = dynamic(() => import('@/lib/home/components/ServiceList')); -export default function Home() { + + +export default function Home({categoryId}) { const bannerRef = useRef(null); const wrapperRef = useRef(null); @@ -70,6 +82,18 @@ export default function Home() { bannerRef.current?.querySelector(':first-child')?.clientHeight + 'px'; }; + useEffect(() => { + const loadCategories = async () => { + const getCategories = await odooApi('GET', '/api/v1/category/child?partner_id='+{categoryId}) + if(getCategories){ + setDataCategories(getCategories) + } + } + loadCategories() + }, []) + + const [dataCategories, setDataCategories] = useState([]) + return ( <BasicLayout> <Seo @@ -107,20 +131,24 @@ export default function Home() { </div> <div className='my-16 flex flex-col gap-y-8'> + <div className='my-16 flex flex-col gap-y-8'> <ServiceList /> <div id='flashsale'> <PreferredBrand /> </div> {!auth?.feature?.soApproval && ( <> - <ProgramPromotion /> <FlashSale /> + {/* <ProgramPromotion /> <FlashSale /> */} </> )} <PromotinProgram /> + <CategoryPilihan categories={dataCategories}/> + <CategoryDynamic/> <CategoryHomeId /> <BannerSection /> <CustomerReviews /> </div> + </div> </div> </DesktopView> @@ -128,7 +156,7 @@ export default function Home() { <DelayRender renderAfter={200}> <HeroBanner /> </DelayRender> - <div className='flex flex-col gap-y-12 my-6'> + <div className='flex flex-col gap-y-4 my-6'> <DelayRender renderAfter={400}> <ServiceList /> </DelayRender> @@ -140,7 +168,7 @@ export default function Home() { {!auth?.feature?.soApproval && ( <> <DelayRender renderAfter={400}> - <ProgramPromotion /> + {/* <ProgramPromotion /> */} </DelayRender> <DelayRender renderAfter={600}> <FlashSale /> @@ -150,6 +178,10 @@ export default function Home() { <DelayRender renderAfter={600}> <PromotinProgram /> </DelayRender> + <DelayRender renderAfter={600}> + <CategoryPilihan categories={dataCategories}/> + <CategoryDynamicMobile/> + </DelayRender> <DelayRender renderAfter={800}> <PopularProduct /> </DelayRender> @@ -164,4 +196,4 @@ export default function Home() { </MobileView> </BasicLayout> ); -} +}
\ No newline at end of file diff --git a/src/pages/my/recomendation/components/products-recomendatison.jsx b/src/pages/my/recomendation/components/products-recomendatison.jsx index d39d2a99..7da2fab1 100644 --- a/src/pages/my/recomendation/components/products-recomendatison.jsx +++ b/src/pages/my/recomendation/components/products-recomendatison.jsx @@ -80,7 +80,7 @@ const ProductsRecomendation = ({ id }) => { } }); - console.log('ini result', searchProduct.data.response); + // console.log('ini result', searchProduct.data.response); } return resultMapping; @@ -112,7 +112,7 @@ const ProductsRecomendation = ({ id }) => { setIsLoading(false); } else { setIsLoading(false); - console.log('No excel data available'); + // console.log('No excel data available'); } }; @@ -129,7 +129,7 @@ const ProductsRecomendation = ({ id }) => { const jsonData = XLSX.utils.sheet_to_json(worksheet); setExcelData(jsonData); - console.log('ini json data', jsonData); + // console.log('ini json data', jsonData); setIsLoading(false); }; @@ -146,13 +146,13 @@ const ProductsRecomendation = ({ id }) => { products[foundIndex].result.code = variant?.code; products[foundIndex].result.name = variant?.name; } else { - console.log('Data not found.'); + // console.log('Data not found.'); } setIsOpen(false); }; const handlingOtherRec = ({ product }) => { - console.log('ini product', product); + // console.log('ini product', product); const result = async () => await searchRecomendation({ product, index: 0, operator: 'OR' }); diff --git a/src/pages/shop/category/[slug].jsx b/src/pages/shop/category/[slug].jsx index 1afe30bf..11840d47 100644 --- a/src/pages/shop/category/[slug].jsx +++ b/src/pages/shop/category/[slug].jsx @@ -5,6 +5,8 @@ import { useRouter } from 'next/router'; import Seo from '@/core/components/Seo'; import { getIdFromSlug, getNameFromSlug } from '@/core/utils/slug'; import Breadcrumb from '@/lib/category/components/Breadcrumb'; +import { useEffect, useState } from 'react'; +import odooApi from '@/core/api/odooApi'; const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout') @@ -12,10 +14,14 @@ const BasicLayout = dynamic(() => const ProductSearch = dynamic(() => import('@/lib/product/components/ProductSearch') ); +const CategorySection = dynamic(() => + import('@/lib/product/components/CategorySection') +) export default function CategoryDetail() { const router = useRouter(); const { slug = '', page = 1 } = router.query; + const [dataCategories, setDataCategories] = useState([]) const categoryName = getNameFromSlug(slug); const categoryId = getIdFromSlug(slug); @@ -43,8 +49,9 @@ export default function CategoryDetail() { <Breadcrumb categoryId={categoryId} /> + {!_.isEmpty(router.query) && ( - <ProductSearch query={query} prefixUrl={`/shop/category/${slug}`} /> + <ProductSearch query={query} categories ={categoryId} prefixUrl={`/shop/category/${slug}`} /> )} </BasicLayout> ); diff --git a/src/pages/shop/lob/[slug].jsx b/src/pages/shop/lob/[slug].jsx new file mode 100644 index 00000000..d939c25c --- /dev/null +++ b/src/pages/shop/lob/[slug].jsx @@ -0,0 +1,48 @@ +import _ from 'lodash'; +import dynamic from 'next/dynamic'; +import { useRouter } from 'next/router'; +import Seo from '@/core/components/Seo'; +import { getIdFromSlug, getNameFromSlug } from '@/core/utils/slug'; +import Breadcrumb from '../../../lib/lob/components/Breadcrumb'; +import { useEffect, useState } from 'react'; +import odooApi from '@/core/api/odooApi'; +import { div } from 'lodash-contrib'; + +const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout')); +const ProductSearch = dynamic(() => import('@/lib/product/components/ProductSearch')); +const CategorySection = dynamic(() => import('@/lib/product/components/CategorySection')); + +export default function CategoryDetail() { + const router = useRouter(); + const { slug = '', page = 1 } = router.query; + const [dataLob, setDataLob] = useState([]); + const [finalQuery, setFinalQuery] = useState({}); + const [dataCategoriesProduct, setDataCategoriesProduct] = useState([]) + const [data, setData] = useState([]) + const dataIdCategories = [] + + const categoryName = getNameFromSlug(slug); + const lobId = getIdFromSlug(slug); + const q = router?.query.q || null; + + return ( + <BasicLayout> + <Seo + title={`Beli ${categoryName} di Indoteknik`} + description={`Jual ${categoryName} Kirim Jakarta Surabaya Semarang Makassar Manado Denpasar Balikpapan Medan Palembang Lampung Bali Bandung Makassar Manado.`} + additionalMetaTags={[ + { + property: 'keywords', + content: `Jual ${categoryName}, harga ${categoryName}, ${categoryName} murah, toko ${categoryName}, ${categoryName} jakarta, ${categoryName} surabaya`, + }, + ]} + /> + + <Breadcrumb categoryId={getIdFromSlug(slug)} /> + + {!_.isEmpty(router.query) && ( + <ProductSearch query={router.query} categories={getIdFromSlug(slug)} prefixUrl={`/shop/lob/${slug}`} /> + )} + </BasicLayout> + ); +} diff --git a/src/pages/shop/promo/[slug].tsx b/src/pages/shop/promo/[slug].tsx index bd69c071..aaee1249 100644 --- a/src/pages/shop/promo/[slug].tsx +++ b/src/pages/shop/promo/[slug].tsx @@ -91,7 +91,7 @@ export default function PromoDetail() { setCurrentPage(pageNumber) try { - const items = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug}`); + const items = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug}`,0,100); setPromoItems(items); if (items.length === 0) { @@ -147,16 +147,16 @@ export default function PromoDetail() { const response = await fetchVariantSolr(`id:${item.product_id} AND ${combinedQuery}`); const product = response.response.docs[0]; const product_id = product.id; - const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} AND ${combinedQueryPrice}`); + const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} AND ${combinedQueryPrice}`,0,100); return response2; }else if(combinedQuery){ const response = await fetchVariantSolr(`id:${item.product_id} AND ${combinedQuery}`); const product = response.response.docs[0]; const product_id = product.id; - const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} `); + const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} `,0,100); return response2; } else { - const response = await fetchPromoItemsSolr(`id:${item.id}`); + const response = await fetchPromoItemsSolr(`id:${item.id}`,0,100); return response; } } catch (fetchError) { diff --git a/src/pages/shop/promo/index.jsx b/src/pages/shop/promo/index.jsx new file mode 100644 index 00000000..01a11aad --- /dev/null +++ b/src/pages/shop/promo/index.jsx @@ -0,0 +1,40 @@ +import Seo from '@/core/components/Seo' +import BasicLayout from '@/core/components/layouts/BasicLayout'; +import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react'; +import Link from 'next/link'; +import Promo from '~/pages/shop/promo'; + +import React from 'react'; + +const PromoPage = () => { + return ( + <BasicLayout> + <Seo title='Promo Indoteknik.com' /> + <div className='container mx-auto py-4 md:py-6 pb-0'> + <Breadcrumb> + <BreadcrumbItem> + <BreadcrumbLink + as={Link} + href='/' + className='!text-danger-500 whitespace-nowrap' + > + Home + </BreadcrumbLink> + </BreadcrumbItem> + + <BreadcrumbItem isCurrentPage> + <BreadcrumbLink className='whitespace-nowrap'> + Promo + </BreadcrumbLink> + </BreadcrumbItem> + </Breadcrumb> + + <div className='h-10' /> + + <Promo /> + </div> + </BasicLayout> + ); +}; + +export default PromoPage; diff --git a/src/pages/shop/promo/index.tsx b/src/pages/shop/promo/index.tsx deleted file mode 100644 index 7ec4f6b0..00000000 --- a/src/pages/shop/promo/index.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import dynamic from 'next/dynamic' -import { useEffect, useState } from 'react' -import { useRouter } from 'next/router' -import Seo from '../../../core/components/Seo.jsx' -import Promocrumb from '../../../lib/promo/components/Promocrumb.jsx' -import { fetchPromoItemsSolr } from '../../../api/promoApi.js' -import LogoSpinner from '../../../core/components/elements/Spinner/LogoSpinner.jsx' -import ProductPromoCard from '../../../../src-migrate/modules/product-promo/components/Card.tsx' -import { IPromotion } from '../../../../src-migrate/types/promotion.ts' -import React from 'react' -import { SolrResponse } from "../../../../src-migrate/types/solr.ts"; - -const BasicLayout = dynamic(() => import('../../../core/components/layouts/BasicLayout.jsx')) - -export default function Promo() { - const router = useRouter() - const { slug = '' } = router.query - const [promoItems, setPromoItems] = useState<any[]>([]) - const [promoData, setPromoData] = useState<IPromotion[] | null>(null) - const [loading, setLoading] = useState(true) - const [currentPage, setCurrentPage] = useState(1) - const [fetchingData, setFetchingData] = useState(false) - - useEffect(() => { - const loadPromo = async () => { - try { - const items = await fetchPromoItemsSolr(`*:*`) - - - setPromoItems(items) - - - if (items.length === 0) { - setPromoData([]) - setLoading(false); - return; - } - - const promoDataPromises = items.map(async (item) => { - const queryParams = new URLSearchParams({ q: `id:${item.id}` }) - - - try { - const response = await fetch(`/solr/promotion_program_lines/select?${queryParams.toString()}`) - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`) - } - - const data: SolrResponse<any[]> = await response.json() - - - const promotions = await map(data.response.docs) - return promotions; - } catch (fetchError) { - console.error("Error fetching promotion data:", fetchError) - return []; - } - }); - - const promoDataArray = await Promise.all(promoDataPromises); - const mergedPromoData = promoDataArray.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []); - setPromoData(mergedPromoData); - setTimeout(() => setLoading(false), 120); // Menambahkan delay 200ms sebelum mengubah status loading - } catch (loadError) { - console.error("Error loading promo items:", loadError) - setLoading(false); - } - } - - if (slug) { - loadPromo() - } - }, [slug]) - - const map = async (promotions: any[]): Promise<IPromotion[]> => { - const result: IPromotion[] = [] - - for (const promotion of promotions) { - const data: IPromotion = { - id: promotion.id, - program_id: promotion.program_id_i, - name: promotion.name_s, - type: { - value: promotion.type_value_s, - label: promotion.type_label_s, - }, - limit: promotion.package_limit_i, - limit_user: promotion.package_limit_user_i, - limit_trx: promotion.package_limit_trx_i, - price: promotion.price_f, - total_qty: promotion.total_qty_i, - products: JSON.parse(promotion.products_s), - free_products: JSON.parse(promotion.free_products_s), - } - - result.push(data) - } - - return result - } - - - - - useEffect(() => { - const handleScroll = () => { - if ( - !fetchingData && - window.innerHeight + document.documentElement.scrollTop >= 0.95 * document.documentElement.offsetHeight - ) { - // User has scrolled to 95% of page height - - setTimeout(() => setFetchingData(true), 120); - setCurrentPage((prevPage) => prevPage + 1) - } - } - - window.addEventListener('scroll', handleScroll) - return () => window.removeEventListener('scroll', handleScroll) - }, [fetchingData]) - - useEffect(() => { - if (fetchingData) { - // Fetch more data - // You may need to adjust this logic according to your API - fetchMoreData() - } - }, [fetchingData]) - - const fetchMoreData = async () => { - try { - // Add a delay of approximately 150ms - setTimeout(async () => { - // Fetch more data - // Update promoData state with the new data - }, 150) - } catch (error) { - console.error('Error fetching more data:', error) - } finally { - setTimeout(() => setFetchingData(false), 120); - - } - } - - const visiblePromotions = promoData?.slice(0, currentPage * 12) - - return ( - <BasicLayout> - <Seo - title={`Promo ${Array.isArray(slug) ? slug[0] : slug} Terkini`} - description='B2B Marketplace MRO & Industri dengan Layanan Pembayaran Tempo, Faktur Pajak, Online Quotation, Garansi Resmi & Harga Kompetitif' - /> - {/* <Promocrumb brandName={capitalizeFirstLetter(Array.isArray(slug) ? slug[0] : slug)} /> */} - <div className='container mx-auto mt-1 flex mb-1'> - <div className=''> - <h1 className='font-semibold'>Semua Promo di Indoteknik</h1> - </div> - </div> - {loading ? ( - <div className='container flex justify-center my-4'> - <LogoSpinner width={48} height={48} /> - </div> - ) : promoData && promoItems.length >= 1 ? ( - <> - <div className='flex flex-wrap justify-center'> - {visiblePromotions?.map((promotion) => ( - <div key={promotion.id} className="min-w-[40px] max-w-[400px] mr-[20px] mb-[20px] sm:w-full md:w-1/2 lg:w-1/3 xl:w-1/4"> - <ProductPromoCard promotion={promotion} /> - </div> - ))} - </div> - {fetchingData && ( - <div className='container flex justify-center my-4'> - <LogoSpinner width={48} height={48} /> - </div> - )} - </> - ) : ( - <div className="text-center my-8"> - <p>Belum ada promo pada kategori ini</p> - </div> - )} - </BasicLayout> - ) -} diff --git a/src/styles/globals.css b/src/styles/globals.css index f6561b00..505dcab4 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -583,12 +583,11 @@ button { @apply absolute left-[100%] top-12 - w-[40vw] bg-gray_r-1/90 backdrop-blur-md border border-gray_r-6 - p-6 + p-6 opacity-0 h-full transition-all @@ -604,6 +603,7 @@ button { transition-colors ease-linear duration-100 + w-fit font-semibold; } @@ -613,6 +613,7 @@ button { transition-colors ease-linear duration-100 + w-full font-normal; } diff --git a/src/utils/solrMapping.js b/src/utils/solrMapping.js index dd90ac7d..d4694eb2 100644 --- a/src/utils/solrMapping.js +++ b/src/utils/solrMapping.js @@ -38,6 +38,7 @@ export const productMappingSolr = (products, pricelist) => { qtySold: product?.qty_sold_f || 0, isTkdn:product?.tkdn_b || false, isSni:product?.sni_b || false, + voucherPastiHemat:product?.voucher_pastihemat || [] }; if (product.manufacture_id_i && product.manufacture_name_s) { |
