summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--public/images/ICON PROMO DISKON.svg13
-rw-r--r--public/images/icon_promo/angklung.svg9
-rw-r--r--public/images/icon_promo/barong.svg9
-rw-r--r--public/images/icon_promo/diskon.svg9
-rw-r--r--public/images/icon_promo/silat.svg9
-rw-r--r--public/images/penawaran-terbatas.jpgbin0 -> 6602 bytes
-rw-r--r--public/images/reject.pngbin0 -> 1275897 bytes
-rw-r--r--src-migrate/constants/menu.ts3
-rw-r--r--src-migrate/hooks/useUtmSource.ts2
-rw-r--r--src-migrate/modules/cart/components/Item.tsx20
-rw-r--r--src-migrate/modules/cart/components/ItemSelect.tsx26
-rw-r--r--src-migrate/modules/cart/stores/useCartStore.ts11
-rw-r--r--src-migrate/modules/footer-banner/index.tsx2
-rw-r--r--src-migrate/modules/product-detail/components/AddToCart.tsx171
-rw-r--r--src-migrate/modules/product-detail/components/PriceAction.tsx2
-rw-r--r--src-migrate/modules/product-detail/components/ProductDetail.tsx2
-rw-r--r--src-migrate/modules/product-promo/components/AddToCart.tsx113
-rw-r--r--src-migrate/modules/product-promo/components/Card.tsx16
-rw-r--r--src-migrate/modules/product-promo/components/Modal.tsx8
-rw-r--r--src-migrate/modules/product-promo/components/ModalContent.tsx8
-rw-r--r--src-migrate/modules/product-promo/components/Section.tsx9
-rw-r--r--src-migrate/modules/product-promo/styles/card.module.css2
-rw-r--r--src-migrate/modules/promo/components/FlashSale.tsx20
-rw-r--r--src-migrate/modules/promo/components/FlashSaleNonDisplay.tsx20
-rw-r--r--src-migrate/modules/promo/components/Hero.tsx105
-rw-r--r--src-migrate/modules/promo/components/HeroDiskon.tsx137
-rw-r--r--src-migrate/modules/promo/components/PromoList.tsx135
-rw-r--r--src-migrate/modules/promo/components/PromotinProgram.jsx134
-rw-r--r--src-migrate/modules/promo/components/Voucher.tsx160
-rw-r--r--src-migrate/modules/promo/components/promoStore.js16
-rw-r--r--src-migrate/modules/promo/styles/hero.module.css27
-rw-r--r--src-migrate/modules/promo/styles/voucher.module.css43
-rw-r--r--src-migrate/modules/side-banner/index.tsx10
-rw-r--r--src-migrate/pages/shop/cart/index.tsx215
-rw-r--r--src-migrate/pages/shop/promo/index.tsx43
-rw-r--r--src-migrate/services/promotionProgram.ts8
-rw-r--r--src-migrate/services/voucher.ts8
-rw-r--r--src-migrate/types/banner.ts6
-rw-r--r--src-migrate/types/cart.ts1
-rw-r--r--src-migrate/types/promotion.ts3
-rw-r--r--src-migrate/types/promotionProgram.ts2
-rw-r--r--src-migrate/types/voucher.ts8
-rw-r--r--src/api/promoApi.js8
-rw-r--r--src/contexts/ProductCartContext.js3
-rw-r--r--src/core/components/elements/Footer/BasicFooter.jsx2
-rw-r--r--src/core/components/elements/Footer/PromoOffer.tsx112
-rw-r--r--src/core/components/elements/Footer/style/promoOffer.module.css39
-rw-r--r--src/core/components/elements/Navbar/NavbarDesktop.jsx161
-rw-r--r--src/core/components/elements/Navbar/NavbarMobile.jsx4
-rw-r--r--src/core/components/elements/Navbar/TopBanner.jsx40
-rw-r--r--src/core/components/elements/Navbar/style/NavbarDesktop.module.css14
-rw-r--r--src/core/components/elements/Sidebar/Sidebar.jsx7
-rw-r--r--src/core/components/layouts/BasicLayout.jsx2
-rw-r--r--src/lib/brand/components/BrandCard.jsx8
-rw-r--r--src/lib/cart/components/Cartheader.jsx262
-rw-r--r--src/lib/category/api/popularProduct.js31
-rw-r--r--src/lib/category/components/Category.jsx60
-rw-r--r--src/lib/category/components/PopularBrand.jsx83
-rw-r--r--src/lib/checkout/api/checkoutApi.js34
-rw-r--r--src/lib/checkout/api/getVoucher.js37
-rw-r--r--src/lib/checkout/components/Checkout.jsx219
-rw-r--r--src/lib/flashSale/components/FlashSaleNonDisplay.jsx66
-rw-r--r--src/lib/home/api/CategoryPilihanApi.js8
-rw-r--r--src/lib/home/api/categoryManagementApi.js8
-rw-r--r--src/lib/home/components/CategoryDynamic.jsx108
-rw-r--r--src/lib/home/components/CategoryDynamicMobile.jsx101
-rw-r--r--src/lib/home/components/CategoryPilihan.jsx120
-rw-r--r--src/lib/home/components/PreferredBrand.jsx43
-rw-r--r--src/lib/home/components/PromotionProgram.jsx12
-rw-r--r--src/lib/home/hooks/useCategoryManagement.js13
-rw-r--r--src/lib/home/hooks/useCategoryPilihan.js13
-rw-r--r--src/lib/lob/components/Breadcrumb.jsx55
-rw-r--r--src/lib/product/components/CategorySection.jsx104
-rw-r--r--src/lib/product/components/LobSectionCategory.jsx81
-rw-r--r--src/lib/product/components/ProductCard.jsx165
-rw-r--r--src/lib/product/components/ProductFilterDesktop.jsx6
-rw-r--r--src/lib/product/components/ProductSearch.jsx148
-rw-r--r--src/lib/quotation/components/Quotation.jsx69
-rw-r--r--src/lib/quotation/components/Quotationheader.jsx265
-rw-r--r--src/lib/transaction/api/rejectProductApi.js9
-rw-r--r--src/lib/transaction/components/Transaction.jsx605
-rw-r--r--src/lib/variant/components/VariantCard.jsx99
-rw-r--r--src/pages/api/shop/product-homepage.js3
-rw-r--r--src/pages/api/shop/search.js22
-rw-r--r--src/pages/index.jsx54
-rw-r--r--src/pages/my/recomendation/components/products-recomendatison.jsx10
-rw-r--r--src/pages/shop/category/[slug].jsx9
-rw-r--r--src/pages/shop/lob/[slug].jsx48
-rw-r--r--src/pages/shop/promo/[slug].tsx8
-rw-r--r--src/pages/shop/promo/index.jsx40
-rw-r--r--src/pages/shop/promo/index.tsx186
-rw-r--r--src/styles/globals.css5
-rw-r--r--src/utils/solrMapping.js1
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,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"/>
+</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
new file mode 100644
index 00000000..d07866e6
--- /dev/null
+++ b/public/images/penawaran-terbatas.jpg
Binary files differ
diff --git a/public/images/reject.png b/public/images/reject.png
new file mode 100644
index 00000000..623d02e8
--- /dev/null
+++ b/public/images/reject.png
Binary files differ
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) {