summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--public/images/PICKUP-NOW.pngbin0 -> 1926 bytes
-rw-r--r--public/images/penawaran-terbatas.jpgbin0 -> 6602 bytes
-rw-r--r--src-migrate/libs/auth.ts5
-rw-r--r--src-migrate/modules/cart/components/Item.tsx9
-rw-r--r--src-migrate/modules/cart/components/ItemSelect.tsx28
-rw-r--r--src-migrate/modules/cart/stores/useCartStore.ts12
-rw-r--r--src-migrate/modules/product-detail/components/Information.tsx2
-rw-r--r--src-migrate/modules/product-detail/stores/useProductDetail.ts2
-rw-r--r--src-migrate/modules/promo/components/Hero.tsx10
-rw-r--r--src-migrate/modules/promo/components/HeroDiskon.tsx22
-rw-r--r--src-migrate/modules/promo/components/PromoList.tsx4
-rw-r--r--src-migrate/modules/promo/components/Voucher.tsx16
-rw-r--r--src-migrate/pages/shop/cart/index.tsx96
-rw-r--r--src-migrate/types/cart.ts2
-rw-r--r--src-migrate/types/product.ts1
-rw-r--r--src/api/productApi.js5
-rw-r--r--src/components/ui/PopularProduct.jsx23
-rw-r--r--src/core/components/elements/Footer/BasicFooter.jsx2
-rw-r--r--src/core/components/elements/Navbar/NavbarDesktop.jsx78
-rw-r--r--src/core/components/elements/Navbar/NavbarMobile.jsx4
-rw-r--r--src/core/components/elements/Navbar/TopBanner.jsx42
-rw-r--r--src/core/components/layouts/BasicLayout.jsx96
-rw-r--r--src/core/components/layouts/BasicLayout.module.css13
-rw-r--r--src/lib/brand/components/BrandCard.jsx8
-rw-r--r--src/lib/category/api/popularProduct.js32
-rw-r--r--src/lib/category/components/Category.jsx66
-rw-r--r--src/lib/category/components/PopularBrand.jsx96
-rw-r--r--src/lib/checkout/components/Checkout.jsx44
-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.jsx109
-rw-r--r--src/lib/home/components/CategoryDynamicMobile.jsx101
-rw-r--r--src/lib/home/components/CategoryPilihan.jsx123
-rw-r--r--src/lib/home/components/PreferredBrand.jsx43
-rw-r--r--src/lib/home/components/PromotionProgram.jsx5
-rw-r--r--src/lib/home/components/Skeleton/BannerPromoSkeleton.jsx16
-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.jsx80
-rw-r--r--src/lib/product/components/ProductCard.jsx36
-rw-r--r--src/lib/product/components/ProductFilterDesktop.jsx7
-rw-r--r--src/lib/product/components/ProductSearch.jsx147
-rw-r--r--src/lib/transaction/components/Transaction.jsx400
-rw-r--r--src/pages/_document.jsx8
-rw-r--r--src/pages/api/activation-request.js2
-rw-r--r--src/pages/api/shop/product-homepage.js3
-rw-r--r--src/pages/api/shop/search.js21
-rw-r--r--src/pages/index.jsx72
-rw-r--r--src/pages/shop/brands/[slug].jsx2
-rw-r--r--src/pages/shop/category/[slug].jsx9
-rw-r--r--src/pages/shop/lob/[slug].jsx48
-rw-r--r--src/styles/globals.css8
-rw-r--r--src/utils/solrMapping.js1
-rw-r--r--tailwind.config.js14
56 files changed, 1760 insertions, 414 deletions
diff --git a/public/images/PICKUP-NOW.png b/public/images/PICKUP-NOW.png
new file mode 100644
index 00000000..d7c01f44
--- /dev/null
+++ b/public/images/PICKUP-NOW.png
Binary files differ
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/src-migrate/libs/auth.ts b/src-migrate/libs/auth.ts
index 86ce26e1..e8516747 100644
--- a/src-migrate/libs/auth.ts
+++ b/src-migrate/libs/auth.ts
@@ -1,5 +1,8 @@
import { deleteCookie, getCookie, setCookie } from 'cookies-next';
import { AuthProps } from '~/types/auth';
+// @ts-ignore
+import camelcaseObjectDeep from 'camelcase-object-deep';
+
const COOKIE_KEY = 'auth';
@@ -14,7 +17,7 @@ export const getAuth = (): AuthProps | boolean => {
};
export const setAuth = (user: AuthProps): boolean => {
- setCookie(COOKIE_KEY, JSON.stringify(user));
+ setCookie(COOKIE_KEY, JSON.stringify(camelcaseObjectDeep(user)));
return true;
};
diff --git a/src-migrate/modules/cart/components/Item.tsx b/src-migrate/modules/cart/components/Item.tsx
index 47893498..6ffbb524 100644
--- a/src-migrate/modules/cart/components/Item.tsx
+++ b/src-migrate/modules/cart/components/Item.tsx
@@ -17,11 +17,11 @@ import CartItemSelect from './ItemSelect'
type Props = {
item: CartItemProps
editable?: boolean
+ selfPicking?: boolean
pilihSemuaCart?: boolean
}
-const CartItem = ({ item, editable = true,}: Props) => {
-
+const CartItem = ({ item, editable = true, selfPicking}: Props) => {
return (
<div className={style.wrapper}>
{item.cart_type === 'promotion' && (
@@ -53,6 +53,11 @@ const CartItem = ({ item, editable = true,}: Props) => {
<CartItem.Image item={item} />
<div className={style.details}>
+ {(item.is_in_bu) && (item.on_hand_qty >= item.quantity) && (
+ <div className='text-[10px] text-red-500 italic'>
+ *Barang ini bisa di pickup maksimal pukul 16.00
+ </div>
+ )}
<CartItem.Name item={item} />
<div className='mt-2 flex justify-between w-full'>
diff --git a/src-migrate/modules/cart/components/ItemSelect.tsx b/src-migrate/modules/cart/components/ItemSelect.tsx
index b904a1de..d4a1b537 100644
--- a/src-migrate/modules/cart/components/ItemSelect.tsx
+++ b/src-migrate/modules/cart/components/ItemSelect.tsx
@@ -13,23 +13,25 @@ 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..3b50ec32 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,15 @@ 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/product-detail/components/Information.tsx b/src-migrate/modules/product-detail/components/Information.tsx
index 52eb6b88..75ae3c41 100644
--- a/src-migrate/modules/product-detail/components/Information.tsx
+++ b/src-migrate/modules/product-detail/components/Information.tsx
@@ -19,7 +19,7 @@ type Props = {
const Information = ({ product }: Props) => {
const querySLA = useQuery<IProductVariantSLA>({
- queryKey: ['variant-sla', product.variants[0].id],
+ queryKey: ['variant-sla', product.variants[0]?.id],
queryFn: () => getVariantSLA(product.variants[0].id),
enabled: product.variant_total === 1
})
diff --git a/src-migrate/modules/product-detail/stores/useProductDetail.ts b/src-migrate/modules/product-detail/stores/useProductDetail.ts
index 2da8835d..eb409930 100644
--- a/src-migrate/modules/product-detail/stores/useProductDetail.ts
+++ b/src-migrate/modules/product-detail/stores/useProductDetail.ts
@@ -23,7 +23,7 @@ export const useProductDetail = create<State & Action>((set, get) => ({
askAdminUrl: '',
isApproval : false,
setActive: (variant) => {
- set({ activeVariantId: variant.id, activePrice: variant.price });
+ set({ activeVariantId: variant?.id, activePrice: variant?.price });
},
setQuantityInput: (value: string) => {
set({ quantityInput: value });
diff --git a/src-migrate/modules/promo/components/Hero.tsx b/src-migrate/modules/promo/components/Hero.tsx
index c5f0afad..97cbe0b7 100644
--- a/src-migrate/modules/promo/components/Hero.tsx
+++ b/src-migrate/modules/promo/components/Hero.tsx
@@ -60,12 +60,12 @@ const Hero = () => {
<DesktopView>
<div className={style['wrapper']}>
<Swiper {...swiperBanner}>
- {banners.map((banner, index) => (
+ {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={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 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
@@ -89,8 +89,8 @@ const Hero = () => {
width={439}
height={150}
quality={100}
- src={banner.image}
- alt={banner.name}
+ src={banner?.image}
+ alt={banner?.name}
className='w-full h-full object-cover object-center rounded-2xl'
/>
</SwiperSlide>
diff --git a/src-migrate/modules/promo/components/HeroDiskon.tsx b/src-migrate/modules/promo/components/HeroDiskon.tsx
index 6d38c763..8b8edcc0 100644
--- a/src-migrate/modules/promo/components/HeroDiskon.tsx
+++ b/src-migrate/modules/promo/components/HeroDiskon.tsx
@@ -39,7 +39,7 @@ const Hero = () => {
queryFn: () => getBanner({ type: 'banner-promotion' })
})
- const banners = useMemo(() => bannerQuery.data || [], [bannerQuery.data]);
+ const banners = useMemo(() => bannerQuery?.data || [], [bannerQuery?.data]);
useEffect(() => {
if (banners.length > 1) {
@@ -56,8 +56,8 @@ const Hero = () => {
{banners.map((banner, index) => (
<SwiperSlide key={index}>
<Image
- src={banner.image}
- alt={banner.name}
+ src={banner?.image}
+ alt={banner?.name}
width={666}
height={480}
className='w-[446px] h-[446px] object-fill object-center rounded-2xl'
@@ -71,8 +71,8 @@ const Hero = () => {
{banners.map((banner, index) => (
<SwiperSlide key={index}>
<Image
- src={banner.image}
- alt={banner.name}
+ src={banner?.image}
+ alt={banner?.name}
width={666}
height={450}
className='w-[400px] h-[217px] object-cover object-center rounded-2xl '
@@ -86,8 +86,8 @@ const Hero = () => {
{banners.map((banner, index) => (
<SwiperSlide key={index}>
<Image
- src={banner.image}
- alt={banner.name}
+ src={banner?.image}
+ alt={banner?.name}
width={666}
height={450}
className='w-[400px] h-[217px] object-cover object-center rounded-2xl'
@@ -101,8 +101,8 @@ const Hero = () => {
{banners.map((banner, index) => (
<SwiperSlide key={index}>
<Image
- src={banner.image}
- alt={banner.name}
+ src={banner?.image}
+ alt={banner?.name}
width={666}
height={450}
className='w-[400px] h-[217px] object-cover object-center rounded-2xl'
@@ -116,8 +116,8 @@ const Hero = () => {
{banners.map((banner, index) => (
<SwiperSlide key={index}>
<Image
- src={banner.image}
- alt={banner.name}
+ src={banner?.image}
+ alt={banner?.name}
width={666}
height={450}
className='w-[400px] h-[217px] object-cover object-center rounded-2xl'
diff --git a/src-migrate/modules/promo/components/PromoList.tsx b/src-migrate/modules/promo/components/PromoList.tsx
index 42725034..69a5ef48 100644
--- a/src-migrate/modules/promo/components/PromoList.tsx
+++ b/src-migrate/modules/promo/components/PromoList.tsx
@@ -59,7 +59,7 @@ const PromoList: React.FC<PromoListProps> = ({ selectedPromo }) => {
const items = await fetchPromoItemsSolr(`type_value_s:${slug}`, 0, 10);
setPromoItems(items);
- const promoDataPromises = items.map(async (item) => {
+ const promoDataPromises = items?.map(async (item) => {
try {
const response = await fetchPromoItemsSolr(`id:${item.id}`, 0, 10);
return response;
@@ -69,7 +69,7 @@ const PromoList: React.FC<PromoListProps> = ({ selectedPromo }) => {
});
const promoDataArray = await Promise.all(promoDataPromises);
- const mergedPromoData = promoDataArray.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []);
+ const mergedPromoData = promoDataArray?.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []);
setPromoData(mergedPromoData);
} catch (error) {
diff --git a/src-migrate/modules/promo/components/Voucher.tsx b/src-migrate/modules/promo/components/Voucher.tsx
index e5877e51..510fe746 100644
--- a/src-migrate/modules/promo/components/Voucher.tsx
+++ b/src-migrate/modules/promo/components/Voucher.tsx
@@ -56,7 +56,7 @@ const VoucherComponent = () => {
spaceBetween: 2,
};
- const dataVouchers = useMemo(() => voucherQuery.data || [], [voucherQuery.data]);
+ const dataVouchers = useMemo(() => voucherQuery?.data || [], [voucherQuery?.data]);
const vouchers = auth?.id? listVouchers : dataVouchers;
@@ -121,30 +121,30 @@ const VoucherComponent = () => {
<div className='h-6' />
- {voucherQuery.isLoading && (
+ {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 && (
+ {!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']} />
+ <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-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 className={style['voucher-code']}>{voucher?.code}</div>
</div>
- <button className={style['voucher-copy']} onClick={() => copyText(voucher.code)}>Salin</button>
+ <button className={style['voucher-copy']} onClick={() => copyText(voucher?.code)}>Salin</button>
</div>
</div>
</div>
diff --git a/src-migrate/pages/shop/cart/index.tsx b/src-migrate/pages/shop/cart/index.tsx
index 73b002b6..5e3e042a 100644
--- a/src-migrate/pages/shop/cart/index.tsx
+++ b/src-migrate/pages/shop/cart/index.tsx
@@ -1,6 +1,6 @@
import style from './cart.module.css';
-import React, { useEffect, useMemo, useState } from 'react';
+import React, { useEffect, useMemo, useRef, useState } from 'react';
import Link from 'next/link';
import { Button, Checkbox, Spinner, Tooltip } from '@chakra-ui/react';
import { toast } from 'react-hot-toast';
@@ -28,10 +28,12 @@ const CartPage = () => {
const [buttonSelectNow, setButtonSelectNow] = useState(true);
const [isLoad, setIsLoad] = useState<boolean>(false)
const [isLoadDelete, setIsLoadDelete] = useState<boolean>(false)
- const { loadCart, cart, summary } = useCartStore();
+ 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);
useEffect(() => {
const handleScroll = () => {
@@ -51,6 +53,34 @@ const CartPage = () => {
}
}, [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)
+ }
+
+ prevCartRef.current = cart ? [...cart.products] : null;
+ }, [cart]);
+
const hasSelectedPromo = useMemo(() => {
if (!cart) return false;
return cart.products.some(item => item.cart_type === 'promotion' && item.selected);
@@ -71,6 +101,31 @@ const CartPage = () => {
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');
}
@@ -84,23 +139,26 @@ const CartPage = () => {
}
const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
- if (typeof auth !== 'object' || !cart) return;
- setIsLoad(true)
- const newSelected = e.target.checked;
- setIsSelectedAll(newSelected);
-
- for (const item of cart.products) {
- await upsertUserCart({
- userId: auth.id,
- type: item.cart_type,
- id: item.id,
- qty: item.quantity,
- selected: newSelected
- });
+
+
+ if (cart) {
+ const updatedCart = {
+ ...cart,
+ products: cart.products.map(item => ({
+ ...item,
+ selected: !hasSelectedAll
+ }))
+ };
+
+ updateCartItem(updatedCart);
+ if(hasSelectedAll){
+ setIsSelectedAll(false);
+ }else{
+ setIsSelectedAll(true);
+ }
}
- await loadCart(auth.id);
- setIsLoad(false)
- }
+ };
+
const handleDelete = async () => {
if (typeof auth !== 'object' || !cart) return;
@@ -136,7 +194,7 @@ const CartPage = () => {
/>
)}
<p className='p-2 text-caption-2'>
- {hasSelectedAll ? "Unchek all" : "Select all"}
+ {hasSelectedAll ? "Uncheck all" : "Select all"}
</p>
</div>
<div className='delate all flex items-center object-center'>
diff --git a/src-migrate/types/cart.ts b/src-migrate/types/cart.ts
index 4e3c8b99..a3115103 100644
--- a/src-migrate/types/cart.ts
+++ b/src-migrate/types/cart.ts
@@ -32,6 +32,8 @@ export type CartItem = {
id: number;
name: string;
stock: number;
+ is_in_bu: boolean;
+ on_hand_qty: number;
weight: number;
attributes: string[];
parent: {
diff --git a/src-migrate/types/product.ts b/src-migrate/types/product.ts
index 681cdc8e..31ea0ce1 100644
--- a/src-migrate/types/product.ts
+++ b/src-migrate/types/product.ts
@@ -12,6 +12,7 @@ export interface IProduct {
variant_total: number;
description: string;
isSni: boolean;
+ is_in_bu: boolean;
isTkdn: boolean;
categories: {
id: string;
diff --git a/src/api/productApi.js b/src/api/productApi.js
index 009d95ef..4a29b59d 100644
--- a/src/api/productApi.js
+++ b/src/api/productApi.js
@@ -2,8 +2,11 @@ import axios from 'axios'
export const popularProductApi = () => {
return async () => {
+ const today = new Date();
+ const dayOfYear = Math.floor((today - new Date(today.getFullYear(), 0, 0)) / 86400000);
+ const page = (dayOfYear % 24) + 1;
const dataPopularProducts = await axios(
- `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?q=*&page=1&orderBy=popular-weekly&priceFrom=1`
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?q=*&page=${page}&orderBy=stock&priceFrom=1`
)
return dataPopularProducts.data.response
}
diff --git a/src/components/ui/PopularProduct.jsx b/src/components/ui/PopularProduct.jsx
index bbbd18bc..92b2a1b6 100644
--- a/src/components/ui/PopularProduct.jsx
+++ b/src/components/ui/PopularProduct.jsx
@@ -5,6 +5,7 @@ import { useQuery } from 'react-query'
import { PopularProductSkeleton } from '../skeleton/PopularProductSkeleton'
import DesktopView from '@/core/components/views/DesktopView'
import ProductCard from '@/lib/product/components/ProductCard'
+import Link from '@/core/components/elements/Link/Link'
const PopularProduct = () => {
const popularProduct = useQuery('popularProduct', popularProductApi())
@@ -16,15 +17,31 @@ const PopularProduct = () => {
<>
<MobileView>
<div className='px-4'>
- <div className='font-semibold mb-4'>Produk Banyak Dilihat</div>
+ <div className='font-semibold mb-4 flex justify-between items-center'><p>
+ Produk Ready Stock
+ </p>
+ <Link
+ href='/shop/search?orderBy=stock'
+ className=''
+ >
+ <p className='text-danger-500 font-semibold'>Lihat Semua</p>
+ </Link></div>
<ProductSlider products={popularProduct.data} simpleTitle />
</div>
</MobileView>
<DesktopView>
<div className='border border-gray_r-6 h-full overflow-auto'>
- <div className='font-semibold text-center p-4 bg-gray_r-1 border-b border-gray_r-6 sticky top-0 z-10'>
- Produk Banyak Dilihat
+ <div className='font-semibold text-center p-4 bg-gray_r-1 border-b border-gray_r-6 sticky top-0 z-10 flex justify-between items-center'>
+ <p>
+ Produk Ready Stock
+ </p>
+ <Link
+ href='/shop/search?orderBy=stock'
+ className=''
+ >
+ <p className='text-danger-500 font-semibold'>Lihat Semua</p>
+ </Link>
</div>
<div className='h-full divide-y divide-gray_r-6'>
{popularProduct.data &&
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/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx
index 7d9e4264..6c308f11 100644
--- a/src/core/components/elements/Navbar/NavbarDesktop.jsx
+++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx
@@ -10,11 +10,12 @@ 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';
@@ -47,6 +48,46 @@ const NavbarDesktop = () => {
const { product } = useProductContext();
const { isOpen, onOpen, onClose } = useDisclosure();
+ 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(() => {
if (router.pathname === '/shop/product/[slug]') {
setPayloadWa({
@@ -79,7 +120,7 @@ const NavbarDesktop = () => {
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'>
@@ -204,6 +245,7 @@ const NavbarDesktop = () => {
</div>
</button>
<div className='w-6/12 flex px-1 divide-x divide-gray_r-6'>
+
<Link
href="/shop/promo"
className={`${
@@ -212,19 +254,29 @@ const NavbarDesktop = () => {
target="_blank"
rel="noreferrer"
>
- <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>
- {/* <div className='w-full h-full flex justify-end items-start'>
- <Image
- src='/images/ICON PROMO DISKON.svg'
- alt='promo'
- width={100}
- height={100}
+ {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={`inline-block z-20`}
+ // 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> */}
+ </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'
@@ -239,7 +291,7 @@ const NavbarDesktop = () => {
<Link
href='/shop/search?orderBy=stock'
className={`${
- router.asPath === '/shop/search?orderBy=stock' &&
+ router.asPath.includes('/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 group`}
target='_blank'
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..f438ae67 100644
--- a/src/core/components/elements/Navbar/TopBanner.jsx
+++ b/src/core/components/elements/Navbar/TopBanner.jsx
@@ -1,39 +1,47 @@
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 { background } from '@chakra-ui/react';
+import { useEffect } from 'react';
-const TopBanner = () => {
+const TopBanner = ({ onLoad = () => {} }) => {
+ const { isDesktop, isMobile } = useDevice()
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/layouts/BasicLayout.jsx b/src/core/components/layouts/BasicLayout.jsx
index a4f3a856..c4674344 100644
--- a/src/core/components/layouts/BasicLayout.jsx
+++ b/src/core/components/layouts/BasicLayout.jsx
@@ -1,12 +1,13 @@
import dynamic from 'next/dynamic';
import Image from 'next/image';
import { useRouter } from 'next/router';
-import { useEffect, useState } from 'react';
+import { useEffect, useState, useRef } from 'react';
import { useProductContext } from '@/contexts/ProductContext';
import odooApi from '@/core/api/odooApi';
import whatsappUrl from '@/core/utils/whatsappUrl';
import Navbar from '../elements/Navbar/Navbar';
+import styles from './BasicLayout.module.css'; // Import modul CSS
const AnimationLayout = dynamic(() => import('./AnimationLayout'), {
ssr: false,
@@ -19,10 +20,15 @@ const BasicLayout = ({ children }) => {
const [templateWA, setTemplateWA] = useState(null);
const [payloadWA, setPayloadWa] = useState(null);
const [urlPath, setUrlPath] = useState(null);
+ const [highlight, setHighlight] = useState(false);
+ const [buttonPosition, setButtonPosition] = useState(null);
+ const [wobble, setWobble] = useState(false);
const router = useRouter();
+ const buttonRef = useRef(null);
const { product } = useProductContext();
+
useEffect(() => {
if (
router.pathname === '/shop/product/[slug]' ||
@@ -39,6 +45,32 @@ const BasicLayout = ({ children }) => {
}
}, [product, router]);
+ useEffect(() => {
+ const handleMouseOut = (event) => {
+ const rect = buttonRef.current.getBoundingClientRect();
+ if (event.clientY <= 0) {
+ setButtonPosition(rect)
+ setHighlight(true);
+ } else {
+ setHighlight(false);
+ }
+ };
+
+ window.addEventListener('mouseout', handleMouseOut);
+
+ return () => {
+ window.removeEventListener('mouseout', handleMouseOut);
+ };
+ }, []);
+
+ useEffect(() => {
+ if (highlight) {
+ // Set wobble animation after overlay highlight animation completes
+ const timer = setTimeout(() => setWobble(true), 1000); // Adjust timing if needed
+ return () => clearTimeout(timer);
+ }
+ }, [highlight]);
+
const recordActivity = async (pathname) => {
const ONLY_ON_PATH = false;
const recordedPath = [];
@@ -60,32 +92,50 @@ const BasicLayout = ({ children }) => {
return (
<>
+ {highlight && buttonPosition && (
+ <div
+ className={styles['overlay-highlight']}
+ style={{
+ '--button-x': `${buttonPosition.x + buttonPosition.width / 2}px`,
+ '--button-y': `${buttonPosition.y + buttonPosition.height / 2}px`,
+ '--button-radius': `${Math.max(buttonPosition.width, buttonPosition.height) / 2}px`
+ }}
+ onAnimationEnd={() => setHighlight(false)}
+ />
+ )}
<Navbar />
<AnimationLayout>
{children}
<div className='fixed bottom-4 right-4 sm:bottom-14 sm:right-10 z-50'>
- <a
- href={whatsappUrl(templateWA, payloadWA, urlPath)}
- className='py-2 pl-3 pr-4 rounded-full bg-[#4FB84A] border border-green-300 flex items-center'
- rel='noopener noreferrer'
- target='_blank'
- >
- <Image
- src='/images/socials/WHATSAPP.svg'
- alt='Whatsapp'
- className='block sm:hidden'
- width={36}
- height={36}
- />
- <Image
- src='/images/socials/WHATSAPP.svg'
- alt='Whatsapp'
- className='hidden sm:block'
- width={44}
- height={44}
- />
- <span className='text-white font-bold ml-1.5'>Whatsapp</span>
- </a>
+ <div className='flex flex-row items-center'>
+ <a href={whatsappUrl(templateWA, payloadWA, urlPath)} className='flex flex-row items-center' rel='noopener noreferrer' target='_blank'>
+ <span className={`text-green-300 text-lg font-bold mr-4 ${wobble ? 'animate-wobble' : ''}`} onAnimationEnd={() => setWobble(false)}>
+ Whatsapp
+ </span>
+ </a>
+ <a
+ href={whatsappUrl(templateWA, payloadWA, urlPath)}
+ className='elemen-whatsapp p-4 rounded-full bg-[#4FB84A] border border-green-300 flex items-center'
+ rel='noopener noreferrer'
+ target='_blank'
+ ref={buttonRef}
+ >
+ <Image
+ src='/images/socials/WHATSAPP.svg'
+ alt='Whatsapp'
+ className='block sm:hidden'
+ width={36}
+ height={36}
+ />
+ <Image
+ src='/images/socials/WHATSAPP.svg'
+ alt='Whatsapp'
+ className='hidden sm:block'
+ width={44}
+ height={44}
+ />
+ </a>
+ </div>
</div>
</AnimationLayout>
<BasicFooter />
diff --git a/src/core/components/layouts/BasicLayout.module.css b/src/core/components/layouts/BasicLayout.module.css
new file mode 100644
index 00000000..4945c420
--- /dev/null
+++ b/src/core/components/layouts/BasicLayout.module.css
@@ -0,0 +1,13 @@
+.overlay-highlight {
+ @apply fixed top-0 left-0 w-full h-full bg-[#4FB84A]/30 z-[900];
+ animation: closeOverlay 1s forwards;
+ }
+
+ @keyframes closeOverlay {
+ from {
+ clip-path: circle(100% at 50% 50%);
+ }
+ to {
+ clip-path: circle(var(--button-radius) at var(--button-x) var(--button-y));
+ }
+ }
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/category/api/popularProduct.js b/src/lib/category/api/popularProduct.js
new file mode 100644
index 00000000..3fdfc41c
--- /dev/null
+++ b/src/lib/category/api/popularProduct.js
@@ -0,0 +1,32 @@
+
+export const fetchPopulerProductSolr = 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,qty_sold_f,qty_sold_f&${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,
+ qty_sold: promotion.qty_sold_f,
+ };
+ 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..f76e6e42 100644
--- a/src/lib/category/components/Category.jsx
+++ b/src/lib/category/components/Category.jsx
@@ -2,11 +2,20 @@ 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'
+import { bannerApi } from '@/api/bannerApi';
+const { useQuery } = require('react-query')
const Category = () => {
const [categories, setCategories] = useState([])
+ const [openCategories, setOpenCategory] = useState([]);
+ const [banner, setBanner] = useState([]);
+ const promotionProgram = useQuery('banner-promo-category-card', bannerApi({ type: 'banner-promo-category-card' }));
+
useEffect(() => {
const loadCategories = async () => {
let dataCategories = await odooApi('GET', '/api/v1/category/tree')
@@ -26,46 +35,65 @@ const Category = () => {
}
loadCategories()
}, [])
-
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='mr-2 flex justify-center items-center'>
+ <Image src={category.image} alt='' width={25} height={25} />
+ </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} />
+ {Array.isArray(promotionProgram?.data) && promotionProgram?.data.length > 0 && promotionProgram?.data[0]?.map((banner, index) => (
+ <div key={index} className='flex w-60 h-20 object-cover'>
+ <Image src={`${banner.image}`} alt={`${banner.name}`} 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..4777fded
--- /dev/null
+++ b/src/lib/category/components/PopularBrand.jsx
@@ -0,0 +1,96 @@
+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 { fetchPopulerProductSolr } from '../api/popularProduct'
+
+const SOLR_HOST = process.env.SOLR_HOST
+
+const PopularBrand = ({ category }) => {
+ const [topBrands, setTopBrands] = useState([]);
+
+ const fetchTopBrands = async () => {
+ try {
+ const items = await fetchPopulerProductSolr(`category_id_ids:(${category?.categoryDataIds?.join(' OR ')})`);
+ const getTop12UniqueBrands = (prod) => {
+ const brandMap = new Map();
+
+ for (const product of prod) {
+ const { manufacture_name, manufacture_id, qty_sold } = product;
+
+ if (brandMap.has(manufacture_name)) {
+ // Update the existing brand's qty_sold
+ brandMap.set(manufacture_name, {
+ name: manufacture_name,
+ id: manufacture_id,
+ qty_sold: brandMap.get(manufacture_name).qty_sold + qty_sold
+ });
+ } else {
+ // Add a new brand to the map
+ brandMap.set(manufacture_name, {
+ name: manufacture_name,
+ id: manufacture_id,
+ qty_sold
+ });
+ }
+ }
+
+ // Convert the map to an array and sort by qty_sold in descending order
+ const sortedBrands = Array.from(brandMap.values()).sort((a, b) => b.qty_sold - a.qty_sold);
+
+ // Return the top 12 brands
+ return sortedBrands.slice(0, 18);
+ };
+
+ // Using the fetched products
+ const products = items;
+ const top12UniqueBrands = getTop12UniqueBrands(products);
+
+ // Set the top 12 brands to the state
+ 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/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx
index 84f68dab..384e7105 100644
--- a/src/lib/checkout/components/Checkout.jsx
+++ b/src/lib/checkout/components/Checkout.jsx
@@ -443,12 +443,38 @@ const Checkout = () => {
if (typeof file !== 'undefined') data.po_file = await getFileBase64(file);
const isCheckouted = await checkoutApi({ data });
+
if (!isCheckouted?.id) {
toast.error('Gagal melakukan transaksi, terjadi kesalahan internal');
return;
- }
+ } else {
+ gtagPurchase(products, biayaKirim, isCheckouted.name);
+
+ gtag('event', 'conversion', {
+ send_to: 'AW-954540379/nDymCL3BhaQYENvClMcD',
+ value:
+ cartCheckout?.grandTotal +
+ Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000,
+ currency: 'IDR',
+ transaction_id: isCheckouted.id,
+ });
- gtagPurchase(products, biayaKirim, isCheckouted.name);
+ for (const product of products) deleteItemCart({ productId: product.id });
+ if (grandTotal > 0) {
+ const payment = await axios.post(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/midtrans-payment?transactionId=${isCheckouted.id}`
+ );
+ setIsLoading(false);
+ window.location.href = payment.data.redirectUrl;
+ } else {
+ window.location.href = `${
+ process.env.NEXT_PUBLIC_SELF_HOST
+ }/shop/checkout/success?order_id=${isCheckouted.name.replace(
+ /\//g,
+ '-'
+ )}`;
+ }
+ }
const midtrans = async () => {
for (const product of products) deleteItemCart({ productId: product.id });
@@ -467,16 +493,6 @@ const Checkout = () => {
)}`;
}
};
-
- gtag('event', 'conversion', {
- send_to: 'AW-954540379/nDymCL3BhaQYENvClMcD',
- value:
- cartCheckout?.grandTotal +
- Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000,
- currency: 'IDR',
- transaction_id: isCheckouted.id,
- event_callback: midtrans,
- });
};
const handlingActivateCode = async () => {
@@ -1058,7 +1074,7 @@ const Checkout = () => {
<div className='p-4 flex flex-col gap-y-4'>
{!!products &&
snakecaseKeys(products).map((item, index) => (
- <CartItem key={index} item={item} editable={false} />
+ <CartItem key={index} item={item} editable={false} selfPicking={selectedExpedisi === '1,32' ? true : false}/>
))}
</div>
@@ -1349,7 +1365,7 @@ const Checkout = () => {
<div className='flex flex-col gap-y-8 border-t border-gray-300 pt-8'>
{!!products &&
snakecaseKeys(products).map((item, index) => (
- <CartItem key={index} item={item} editable={false} />
+ <CartItem key={index} item={item} editable={false} selfPicking={selectedExpedisi === '1,32' ? true : false} />
))}
</div>
</div>
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..b7798a24
--- /dev/null
+++ b/src/lib/home/components/CategoryDynamic.jsx
@@ -0,0 +1,109 @@
+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}
+ className='p-2 ml-1'
+ 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..6dbf771e
--- /dev/null
+++ b/src/lib/home/components/CategoryPilihan.jsx
@@ -0,0 +1,123 @@
+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 (
+ categoryPilihan.length > 0 && (
+ <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 6b64a444..56268db7 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'>
@@ -54,18 +71,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=''>
+ {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 99258d94..c2f76069 100644
--- a/src/lib/home/components/PromotionProgram.jsx
+++ b/src/lib/home/components/PromotionProgram.jsx
@@ -3,11 +3,16 @@ import Image from 'next/image'
import { bannerApi } from '@/api/bannerApi';
import useDevice from '@/core/hooks/useDevice'
import { Swiper, SwiperSlide } from 'swiper/react';
+import BannerPromoSkeleton from '../components/Skeleton/BannerPromoSkeleton';
const { useQuery } = require('react-query')
const BannerSection = () => {
const promotionProgram = useQuery('promotionProgram', bannerApi({ type: 'banner-promotion' }));
const { isMobile, isDesktop } = useDevice()
+ if (promotionProgram.isLoading) {
+ return <BannerPromoSkeleton />;
+ }
+
return (
<div className='px-4 sm:px-0'>
<div className='flex justify-between items-center mb-4 '>
diff --git a/src/lib/home/components/Skeleton/BannerPromoSkeleton.jsx b/src/lib/home/components/Skeleton/BannerPromoSkeleton.jsx
new file mode 100644
index 00000000..c5f39f19
--- /dev/null
+++ b/src/lib/home/components/Skeleton/BannerPromoSkeleton.jsx
@@ -0,0 +1,16 @@
+import useDevice from '@/core/hooks/useDevice'
+import Skeleton from 'react-loading-skeleton'
+
+const BannerPromoSkeleton = () => {
+ const { isDesktop } = useDevice()
+
+ return (
+ <div className='grid grid-cols-1 md:grid-cols-3 gap-x-3'>
+ {Array.from({ length: isDesktop ? 3 : 1.2 }, (_, index) => (
+ <Skeleton count={1} height={isDesktop ? 60 : 36} key={index} />
+ ))}
+ </div>
+ )
+}
+
+export default BannerPromoSkeleton
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..e8ebb095
--- /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 p-1" 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..5cd467e9
--- /dev/null
+++ b/src/lib/product/components/LobSectionCategory.jsx
@@ -0,0 +1,80 @@
+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 e10241a7..22ce0533 100644
--- a/src/lib/product/components/ProductCard.jsx
+++ b/src/lib/product/components/ProductCard.jsx
@@ -17,8 +17,8 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
const [discount, setDiscount] = useState(0);
let voucherPastiHemat = 0;
-
- if (product?.voucherPastiHemat.length > 0) {
+
+ if (product?.voucherPastiHemat ? product?.voucherPastiHemat.length : voucherPastiHemat > 0) {
const stringVoucher = product?.voucherPastiHemat[0];
const validJsonString = stringVoucher.replace(/'/g, '"');
voucherPastiHemat = JSON.parse(validJsonString);
@@ -146,13 +146,22 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
)}
</Link>
<div className='p-2 sm:p-3 pb-3 text-caption-2 sm:text-body-2 leading-5'>
- {product?.manufacture?.name ? (
- <Link href={URL.manufacture} className='mb-1'>
- {product.manufacture.name}
+ <div className='flex justify-between '>
+ {product?.manufacture?.name ? (
+ <Link href={URL.manufacture} className='mb-1 mt-1'>
+ {product.manufacture.name}
+ </Link>
+ ) : (
+ <div>-</div>
+ )}
+ {product?.isInBu && (
+ <Link href='/panduan-pick-up-service' className='group'>
+ <Image src='/images/PICKUP-NOW.png' className='group-hover:scale-105 transition-transform duration-200' alt='pickup now' width={90} height={12} />
</Link>
- ) : (
- <div>-</div>
- )}
+
+
+ )}
+ </div>
<Link
href={URL.product}
className={`mb-2 !text-gray_r-12 leading-6 block line-clamp-3 h-[64px]`}
@@ -292,9 +301,18 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
</div>
)}
{product?.manufacture?.name ? (
- <Link href={URL.manufacture} className='mb-1'>
+ <div className='flex justify-between'>
+ <Link href={URL.manufacture} className='mb-1'>
{product.manufacture.name}
</Link>
+ {/* {product?.is_in_bu && (
+ <div className='bg-red-500 rounded'>
+ <span className='p-[6px] text-xs text-white'>
+ Click & Pickup
+ </span>
+ </div>
+ )} */}
+ </div>
) : (
<div>-</div>
)}
diff --git a/src/lib/product/components/ProductFilterDesktop.jsx b/src/lib/product/components/ProductFilterDesktop.jsx
index a8073036..73fecab5 100644
--- a/src/lib/product/components/ProductFilterDesktop.jsx
+++ b/src/lib/product/components/ProductFilterDesktop.jsx
@@ -22,6 +22,7 @@ import { formatCurrency } from '@/core/utils/formatValue'
const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = null }) => {
+
const router = useRouter()
const { query } = router
const [order, setOrder] = useState(query?.orderBy)
@@ -107,7 +108,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 fb9017f4..eaeac71a 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 [limit, setLimit] = useState(query?.limit || 30);
+ const [limit, setLimit] = useState(router.query?.limit || 30);
const [orderBy, setOrderBy] = useState(router.query?.orderBy);
+ 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([]);
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 (
@@ -224,7 +343,7 @@ const ProductSearch = ({
q: router.query.q,
orderBy: orderBy,
brand: brandValues.join(','),
- category: categoryValues.join(','),
+ category: categoryValues?.join(','),
priceFrom,
priceTo,
};
@@ -263,7 +382,6 @@ const ProductSearch = ({
};
const isNotReadyStockPage = router.asPath !== '/shop/search?orderBy=stock';
-
return (
<>
<MobileView>
@@ -324,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'>
@@ -364,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'
/>
@@ -412,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
@@ -550,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/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx
index 9bef895a..4d401037 100644
--- a/src/lib/transaction/components/Transaction.jsx
+++ b/src/lib/transaction/components/Transaction.jsx
@@ -1,6 +1,6 @@
import Spinner from '@/core/components/elements/Spinner/Spinner';
import NextImage from 'next/image';
-import rejectImage from "../../../../public/images/reject.png"
+import rejectImage from '../../../../public/images/reject.png';
import useTransaction from '../hooks/useTransaction';
import TransactionStatusBadge from './TransactionStatusBadge';
import Divider from '@/core/components/elements/Divider/Divider';
@@ -40,7 +40,7 @@ import rejectProductApi from '../api/rejectProductApi';
import { useRouter } from 'next/router';
const Transaction = ({ id }) => {
- const router = useRouter()
+ const router = useRouter();
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedProduct, setSelectedProduct] = useState(null);
const [reason, setReason] = useState('');
@@ -152,7 +152,10 @@ const Transaction = ({ id }) => {
const memoizeVariantGroupCardReject = useMemo(
() => (
<div className='p-4 pt-0 flex flex-col gap-y-3'>
- <VariantGroupCard variants={transaction.data?.productsRejectLine} buyMore />
+ <VariantGroupCard
+ variants={transaction.data?.productsRejectLine}
+ buyMore
+ />
</div>
),
[transaction.data]
@@ -182,26 +185,25 @@ const Transaction = ({ id }) => {
};
const handleRejectProduct = async () => {
- try{
+ 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});
+ } else {
+ let idSo = transaction?.data.id;
+ let idProduct = selectedProduct?.id;
+ await rejectProductApi({ idSo, idProduct, reason });
closeModal();
- toast.success("Produk berhasil di reject")
+ toast.success('Produk berhasil di reject');
setTimeout(() => {
window.location.reload();
- }, 1500);
+ }, 1500);
}
- }catch(error){
+ } catch (error) {
toast.error('Gagal reject produk. Silakan coba lagi.');
}
};
-
return (
transaction.data?.name && (
<>
@@ -390,14 +392,20 @@ const Transaction = ({ id }) => {
<p className='text-gray_r-11 leading-none'>Dokumen PO</p>
<button
type='button'
- className='btn-light py-1.5 px-3 ml-auto'
+ className='inline-block text-danger-500'
onClick={
transaction.data?.purchaseOrderFile
? () => downloadPurchaseOrder(transaction.data)
- : openUploadPo
+ : transaction?.data.invoices.length < 1
+ ? openUploadPo
+ : ''
}
>
- {transaction.data?.purchaseOrderFile ? 'Download' : 'Upload'}
+ {transaction?.data?.purchaseOrderFile
+ ? 'Download'
+ : transaction?.data.invoices.length < 1
+ ? 'Upload'
+ : '-'}
</button>
</div>
</div>
@@ -406,13 +414,13 @@ const Transaction = ({ id }) => {
<Divider />
<div className='font-medium p-4'>Detail Produk</div>
- {transaction?.data?.products.length > 0? (
- <div>
- {memoizeVariantGroupCard}
- </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>
- )}
+ <div className='badge-red text-sm px-2 ml-4'>
+ Semua produk telah di reject
+ </div>
+ )}
{transaction?.data?.productsRejectLine.length > 0 && (
<div>
@@ -594,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>
</>
@@ -628,9 +640,11 @@ const Transaction = ({ id }) => {
<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>
- )}
+ {transaction?.data?.pickings.length == 0 && (
+ <div className='badge-red text-sm'>
+ Belum ada pengiriman
+ </div>
+ )}
<div className='grid grid-cols-1 gap-1 w-2/3'>
{transaction?.data?.pickings?.map((airway) => (
<button
@@ -646,7 +660,9 @@ const Transaction = ({ id }) => {
</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'}
+ {airway?.delivered
+ ? 'Pesanan Tiba'
+ : 'Sedang Dikirim'}
</div>
<ChevronRightIcon className='w-5 stroke-2' />
</div>
@@ -655,51 +671,53 @@ const Transaction = ({ id }) => {
</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 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>
- <ChevronRightIcon className='w-5 stroke-2' />
</div>
- </Link>
- ))}
- </div>
+ <ChevronRightIcon className='w-5 stroke-2' />
+ </div>
+ </Link>
+ ))}
+ </div>
</div>
</div>
<div className='text-h-sm font-semibold mt-4 mb-4'>
Rincian Pembelian
</div>
- {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?.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'>
@@ -711,37 +729,37 @@ const Transaction = ({ id }) => {
)}
className='w-[20%] flex-shrink-0'
>
- <div className='relative'>
+ <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 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>
- </div>
</Link>
<div className='px-2 text-left'>
<Link
@@ -774,33 +792,42 @@ const Transaction = ({ id }) => {
{currencyFormat(product.price.price)}
</div>
)} */}
- <div>{currencyFormat(product.price.priceDiscount)}</div>
+ <div>
+ {currencyFormat(product.price.priceDiscount)}
+ </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>
- )}
+ {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='badge-red text-sm'>
+ Semua produk telah di reject
+ </div>
)}
-
+
{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
+ <div
+ className='bg-white p-4 rounded w-96
ease-in-out opacity-100
- transform transition-transform duration-300 scale-100'>
+ transform transition-transform duration-300 scale-100'
+ >
<h2 className='text-lg mb-2'>Berikan Alasan</h2>
<textarea
value={reason}
@@ -826,117 +853,116 @@ const Transaction = ({ id }) => {
</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>
+ {transaction?.data?.products?.length > 0 && (
+ <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>
- <div className='text-right'>PPN 11%</div>
- <div className='text-right font-medium'>
- {currencyFormat(transaction.data?.amountTax)}
- </div>
+ <div className='text-right'>PPN 11%</div>
+ <div className='text-right font-medium'>
+ {currencyFormat(transaction.data?.amountTax)}
+ </div>
- <div className='text-right whitespace-nowrap'>
- Biaya Pengiriman
- </div>
- <div className='text-right font-medium'>
- {currencyFormat(transaction.data?.deliveryAmount)}
- </div>
+ <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 className='text-right'>Grand Total</div>
+ <div className='text-right font-medium text-gray_r-12'>
+ {currencyFormat(transaction.data?.amountTotal)}
+ </div>
</div>
</div>
- </div>
- ))}
-
-
+ )}
{transaction?.data?.productsRejectLine.length > 0 && (
- <div className='text-h-sm font-semibold mt-10 mb-4'>
- Rincian Produk Reject
- </div>
+ <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'>
+ <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='w-[20%] flex-shrink-0'
+ 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-32 w-full rounded-md'
- />
+ {product?.parent?.name}
</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 className='text-gray_r-11 mt-2'>
+ {product?.code}{' '}
+ {product?.attributes.length > 0
+ ? `| ${product?.attributes.join(', ')}`
+ : ''}
</div>
- </td>
- {/* <td>
+ </div>
+ </td>
+ {/* <td>
{product.price.discountPercentage > 0
? `${product.price.discountPercentage}%`
: ''}
</td> */}
- <td>{product.quantity}</td>
- <td>
- {/* {product.price.discountPercentage > 0 && (
+ <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>
+ {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>
</DesktopView>
diff --git a/src/pages/_document.jsx b/src/pages/_document.jsx
index cd60bd89..6af6294f 100644
--- a/src/pages/_document.jsx
+++ b/src/pages/_document.jsx
@@ -41,13 +41,13 @@ export default function MyDocument() {
content='328wmjs7hcnz74rwsqzxvq50rmbtm2'
/>
- <Script
+ {/* <Script
async
strategy='beforeInteractive'
src='https://www.googletagmanager.com/gtag/js?id=UA-10501937-1'
- />
+ /> */}
- <Script
+ {/* <Script
async
id='google-analytics-ua'
strategy='beforeInteractive'
@@ -59,7 +59,7 @@ export default function MyDocument() {
gtag('config', 'UA-10501937-1');
`,
}}
- />
+ /> */}
<Script
async
diff --git a/src/pages/api/activation-request.js b/src/pages/api/activation-request.js
index 61dbb597..98d27f78 100644
--- a/src/pages/api/activation-request.js
+++ b/src/pages/api/activation-request.js
@@ -7,7 +7,7 @@ export default async function handler(req, res) {
let result = await odooApi('POST', '/api/v1/user/activation-request', { email })
if (result.activationRequest) {
mailer.sendMail({
- from: 'sales@indoteknik.com',
+ from: 'Indoteknik.com <noreply@indoteknik.com>',
to: result.user.email,
subject: 'Permintaan Aktivasi Akun Indoteknik',
html: `
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 29471f7b..6f98efcb 100644
--- a/src/pages/api/shop/search.js
+++ b/src/pages/api/shop/search.js
@@ -42,7 +42,10 @@ export default async function handler(req, res) {
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,12 +54,12 @@ 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, product_rating_f:[13 TO *], discount_tier1_v2_f:[1 TO *]`,
+ `fq=-publish_b:false, product_rating_f:[8 TO *], price_tier1_v2_f:[1 TO *]`,
];
if (priceFrom > 0 || priceTo > 0) {
@@ -77,7 +80,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)
@@ -120,12 +126,14 @@ export default async function handler(req, res) {
const escapeSolrQuery = (query) => {
if (query == '*') return query;
+
+ query = query.replace(/-/g, ' ');
- const specialChars = /([\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g;
+ const specialChars = /([\+\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g;
const words = query.split(/\s+/);
const escapedWords = words.map((word) => {
if (specialChars.test(word)) {
- return `"${word.replace(specialChars, '\\$1')}"`;
+ return word.replace(specialChars, '\\$1');
}
return word;
});
@@ -133,6 +141,7 @@ const escapeSolrQuery = (query) => {
return escapedWords.join(' ');
};
+
/*const productResponseMap = (products, pricelist) => {
return products.map((product) => {
let price = product.price_tier1_v2_f || 0
diff --git a/src/pages/index.jsx b/src/pages/index.jsx
index 8af963fb..4d6e59e0 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';
@@ -9,10 +8,14 @@ import DesktopView from '@/core/components/views/DesktopView';
import MobileView from '@/core/components/views/MobileView';
import { FlashSaleSkeleton } from '@/lib/flashSale/skeleton/FlashSaleSkeleton';
import PreferredBrandSkeleton from '@/lib/home/components/Skeleton/PreferredBrandSkeleton';
+import BannerPromoSkeleton from '@/lib/home/components/Skeleton/BannerPromoSkeleton';
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';
+import useProductDetail from '~/modules/product-detail/stores/useProductDetail';
const BasicLayout = dynamic(() =>
import('@/core/components/layouts/BasicLayout')
@@ -45,7 +48,10 @@ const FlashSale = dynamic(
);
const ProgramPromotion = dynamic(() =>
- import('@/lib/home/components/PromotionProgram')
+ import('@/lib/home/components/PromotionProgram'),
+{
+ loading: () => <BannerPromoSkeleton />,
+}
);
const BannerSection = dynamic(() =>
@@ -54,12 +60,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,7 +87,19 @@ 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
title='Indoteknik.com: B2B Industrial Supply & Solution'
@@ -78,11 +107,9 @@ export default function Home() {
additionalMetaTags={[
{
name: 'keywords',
- content:
- 'indoteknik, indoteknik.com, toko teknik, toko perkakas, jual genset, jual fogging, jual krisbow, harga krisbow, harga alat safety, harga pompa air',
+ content: 'indoteknik, indoteknik.com, toko teknik, toko perkakas, jual genset, jual fogging, jual krisbow, harga krisbow, harga alat safety, harga pompa air',
},
- ]}
- />
+ ]} />
<PagePopupIformation />
@@ -113,22 +140,30 @@ export default function Home() {
</div>
{!auth?.feature?.soApproval && (
<>
- <ProgramPromotion /> <FlashSale />
+ <DelayRender renderAfter={200}>
+ <ProgramPromotion />
+ </DelayRender>
+ <DelayRender renderAfter={200}>
+ <FlashSale />
+ </DelayRender>
</>
)}
<PromotinProgram />
+ {dataCategories &&(
+ <CategoryPilihan categories={dataCategories} />
+ )}
+ <CategoryDynamic />
<CategoryHomeId />
<BannerSection />
<CustomerReviews />
</div>
</div>
- </DesktopView>
-
- <MobileView>
+ </DesktopView>
+ <MobileView>
<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>
@@ -150,6 +185,12 @@ export default function Home() {
<DelayRender renderAfter={600}>
<PromotinProgram />
</DelayRender>
+ <DelayRender renderAfter={600}>
+ {dataCategories &&(
+ <CategoryPilihan categories={dataCategories} />
+ )}
+ <CategoryDynamicMobile />
+ </DelayRender>
<DelayRender renderAfter={800}>
<PopularProduct />
</DelayRender>
@@ -163,5 +204,6 @@ export default function Home() {
</div>
</MobileView>
</BasicLayout>
+ </>
);
-}
+} \ No newline at end of file
diff --git a/src/pages/shop/brands/[slug].jsx b/src/pages/shop/brands/[slug].jsx
index e786ef78..88e9b302 100644
--- a/src/pages/shop/brands/[slug].jsx
+++ b/src/pages/shop/brands/[slug].jsx
@@ -18,7 +18,7 @@ export default function BrandDetail() {
const brandName = getNameFromSlug(slug)
const id = getIdFromSlug(slug)
const {brand} = useBrand({id})
- if (!brand || !brand.data || _.isEmpty(brand.data)) {
+ if ( !brand.isLoading && _.isEmpty(brand.data)) {
return <PageNotFound />;
}
return (
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/styles/globals.css b/src/styles/globals.css
index f6561b00..b3ca85f4 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -563,7 +563,8 @@ button {
}
.category-mega-box > div:hover .category-mega-box__parent {
- @apply bg-gray_r-5;
+ @apply bg-gray_r-5
+ w-full;
}
.category-mega-box > div:hover .category-mega-box__child-wrapper {
@@ -583,12 +584,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 +604,7 @@ button {
transition-colors
ease-linear
duration-100
+ w-fit
font-semibold;
}
@@ -613,6 +614,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 d4694eb2..fee474be 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,
+ is_in_bu:product?.is_in_bu_b || false,
voucherPastiHemat:product?.voucher_pastihemat || []
};
diff --git a/tailwind.config.js b/tailwind.config.js
index f1c740d5..799fdb02 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -116,7 +116,19 @@ module.exports = {
800: '#760A26',
900: '#610625'
}
- }
+ },
+ keyframes: {
+ wobble: {
+ '0%': { transform: 'translateY(0)' },
+ '25%': { transform: 'translateY(-10px)' },
+ '50%': { transform: 'translateY(0)' },
+ '75%': { transform: 'translateY(10px)' },
+ '100%': { transform: 'translateY(0)' },
+ },
+ },
+ animation: {
+ wobble: 'wobble 0.5s ease forwards',
+ },
}
},
plugins: [require('@tailwindcss/line-clamp'), require('@tailwindcss/typography')]