summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/brand/components/MediaCard.jsx8
-rw-r--r--src/lib/product/components/Product/ProductDesktopVariant.jsx86
-rw-r--r--src/lib/product/components/Product/ProductMobileVariant.jsx147
-rw-r--r--src/lib/product/components/ProductCard.jsx687
-rw-r--r--src/lib/product/components/ProductSearch.jsx44
-rw-r--r--src/lib/product/styles/desc_mobile_variant.module.css18
6 files changed, 702 insertions, 288 deletions
diff --git a/src/lib/brand/components/MediaCard.jsx b/src/lib/brand/components/MediaCard.jsx
index 4a298e15..e37aa76c 100644
--- a/src/lib/brand/components/MediaCard.jsx
+++ b/src/lib/brand/components/MediaCard.jsx
@@ -4,9 +4,7 @@ import useDevice from '@/core/hooks/useDevice';
import { createSlug } from '@/core/utils/slug';
const MediaCard = ({ media }) => {
- const { isMobile } = useDevice();
-
- console.log("Media logo:", media);
+ const { isMobile } = useDevice();
return (
<Link
@@ -25,11 +23,11 @@ const MediaCard = ({ media }) => {
width={500}
height={500}
quality={85}
- className="h-full w-[122px] object-contain object-center"
+ className='h-full w-[122px] object-contain object-center'
/>
) : (
<span
- className="text-center"
+ className='text-center'
style={{ fontSize: `${16 - media.name.length * 0.5}px` }}
>
{media.name}
diff --git a/src/lib/product/components/Product/ProductDesktopVariant.jsx b/src/lib/product/components/Product/ProductDesktopVariant.jsx
index 44ae04bd..bc07507b 100644
--- a/src/lib/product/components/Product/ProductDesktopVariant.jsx
+++ b/src/lib/product/components/Product/ProductDesktopVariant.jsx
@@ -1,3 +1,4 @@
+import { TicketIcon } from '@heroicons/react/24/solid';
import { Box, Button, Skeleton, Tooltip } from '@chakra-ui/react';
import { HeartIcon } from '@heroicons/react/24/outline';
import { Info, MessageCircleIcon, Share2Icon } from 'lucide-react';
@@ -46,6 +47,7 @@ const ProductDesktopVariant = ({
const [isLoadingSLA, setIsLoadingSLA] = useState(true);
const [selectedVariant, setSelectedVariant] = useState(product.id);
const { setRefreshCart } = useProductCartContext();
+ const [discount, setDiscount] = useState(0);
const [quantityInput, setQuantityInput] = useState(1);
@@ -312,6 +314,75 @@ const ProductDesktopVariant = ({
fetchData();
}, [product]);
+ // voucher dari Solr: utamakan newVoucherPastiHemat, fallback voucherPastiHemat
+ let voucherPastiHemat = Array.isArray(product?.newVoucherPastiHemat)
+ ? product.newVoucherPastiHemat[0]
+ : product?.newVoucherPastiHemat;
+
+ if (!voucherPastiHemat && Array.isArray(product?.voucherPastiHemat)) {
+ voucherPastiHemat = product.voucherPastiHemat[0];
+ }
+
+ const hitungDiscountVoucher = () => {
+ const basePrice =
+ Number(product?.price?.priceDiscount ?? 0) ||
+ Number(product?.price?.price ?? 0);
+
+ if (!voucherPastiHemat || !basePrice) {
+ setDiscount(0);
+ return;
+ }
+
+ const type = String(
+ voucherPastiHemat.discountType ?? voucherPastiHemat.discount_type ?? ''
+ ).toLowerCase();
+ const amount = Number(
+ voucherPastiHemat.discountAmount ?? voucherPastiHemat.discount_amount ?? 0
+ );
+ const max = Number(
+ voucherPastiHemat.maxDiscount ?? voucherPastiHemat.max_discount ?? 0
+ );
+ const min = Number(
+ voucherPastiHemat.minPurchase ?? voucherPastiHemat.min_purchase ?? 0
+ );
+
+ if (min > 0 && basePrice < min) {
+ setDiscount(0);
+ return;
+ }
+
+ let countDiscount = 0;
+
+ if (type.startsWith('percent')) {
+ const pct = amount <= 1 ? amount * 100 : amount;
+ countDiscount = Math.floor(basePrice * (pct / 100));
+ } else {
+ countDiscount = Math.floor(amount || 0);
+ }
+
+ if (max > 0 && countDiscount > max) countDiscount = max;
+
+ setDiscount(Math.max(0, countDiscount));
+ // console.log('count disc', countDiscount, {
+ // basePrice,
+ // type,
+ // amount,
+ // max,
+ // min,
+ // });
+ };
+
+ useEffect(() => {
+ hitungDiscountVoucher();
+ }, [
+ product?.price?.priceDiscount,
+ product?.price?.price,
+ product?.newVoucherPastiHemat,
+ product?.voucherPastiHemat,
+ ]);
+
+ // console.log('product', product);
+
return (
<DesktopView>
<div className='container mx-auto pt-10'>
@@ -383,7 +454,10 @@ const ProductDesktopVariant = ({
</div>
<div className='flex p-3 items-center '>
<div className='w-4/12 text-gray_r-12/70'>Terjual</div>
- <div className='w-8/12'>-</div>
+ <div className='w-8/12'>
+ {product.qtySold > 0 && <span>{product.qtySold}</span>}
+ {product.qtySold == 0 && <span>-</span>}
+ </div>
</div>
<div className='flex p-3 items-center bg-gray_r-4 '>
@@ -605,6 +679,16 @@ const ProductDesktopVariant = ({
/>
Penawaran Harga Instan
</Button>
+ {discount > 0 && (product?.isFlashSale ?? 0) < 1 && (
+ <div className='mt-3'>
+ <div className='inline-flex items-center border border-green-500 p-3 bg-green-50 rounded-md'>
+ <span className='text-sm font-semibold text-green-700'>
+ Pakai Voucher Belanja <b>{currencyFormat(discount)} </b> &
+ Potongan Ongkir hingga <b>Rp 20.000 </b> Saat Checkout
+ </span>
+ </div>
+ </div>
+ )}
<div className='flex py-5'>
<div className='flex gap-x-5 items-center justify-center'>
<Button
diff --git a/src/lib/product/components/Product/ProductMobileVariant.jsx b/src/lib/product/components/Product/ProductMobileVariant.jsx
index 4cfc63ca..c44de561 100644
--- a/src/lib/product/components/Product/ProductMobileVariant.jsx
+++ b/src/lib/product/components/Product/ProductMobileVariant.jsx
@@ -19,6 +19,7 @@ import whatsappUrl from '@/core/utils/whatsappUrl';
import { getAuth } from '~/libs/auth';
import SimilarBottom from '~/modules/product-detail/components/SimilarBottom';
import ProductSimilar from '../ProductSimilar';
+import styles from '../../styles/desc_mobile_variant.module.css';
const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
const router = useRouter();
@@ -175,6 +176,8 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
};
fetchData();
}, [product]);
+ console.log(product);
+ // console.log(product.parent.description);
const [fakeStock] = useState(() => {
// inisialisasi sekali doang pas pertama kali komponen dibuat
@@ -185,7 +188,7 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
<MobileView>
{/* PRICE & ACTIONS: tetap punyamu, hanya hapus input number lama */}
{/* ===== BAR BAWAH (fixed) ===== */}
- <div className='px-4 fixed bottom-0 left-0 right-0 bg-white z-10 pb-6 pt-4 rounded-t-2xl shadow-[rgba(0,0,4,0.1)_0px_-4px_4px_0px]'>
+ <div className='p-3 fixed gap-x-2 bottom-0 left-0 right-0 bg-white z-10 pb-6 pt-4 rounded-t-2xl shadow-[rgba(0,0,4,0.1)_0px_-4px_4px_0px]'>
{/* HARGA & PPN (logikamu tetap) */}
{activeVariant.isFlashSale &&
activeVariant?.price?.discountPercentage > 0 ? (
@@ -201,7 +204,7 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
{currencyFormat(activeVariant?.price?.priceDiscount)}
</div>
</div>
- <div className='text-gray_r-9 text-base font-normal mt-1'>
+ <div className='text-sm text-gray-400 mt-1'>
Termasuk PPN:{' '}
{currencyFormat(
activeVariant?.price.priceDiscount * process.env.NEXT_PUBLIC_PPN
@@ -209,11 +212,11 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
</div>
</>
) : (
- <div className='text-danger-500 font-semibold mt-1 text-3xl'>
+ <div className='text-danger-500 font-semibold mt-1 text-title-sm'>
{activeVariant?.price?.price > 0 ? (
<>
{currencyFormat(activeVariant?.price?.price)}
- <div className='text-gray_r-9 text-base font-normal mt-1'>
+ <div className='text-sm text-gray-500 mt-1'>
Termasuk PPN:{' '}
{currencyFormat(
activeVariant?.price.price * process.env.NEXT_PUBLIC_PPN
@@ -243,7 +246,7 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
)}
{/* ⬇️ TAMBAHKAN BLOK INI DI DALAM BAR: STOK & STEPPER */}
- <div className='grid grid-cols-12 items-center gap-3 mt-3'>
+ <div className='grid grid-cols-12 items-center gap-3'>
<div className='col-span-7'>
<div
className={`text-[14px] ${
@@ -335,7 +338,7 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
colorScheme='red'
isDisabled={product.stock === 0}
>
- Beli
+ Beli Sekarang
</Button>
</div>
</div>
@@ -348,7 +351,7 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
/>
<div className='p-4'>
- <div className='flex items-end mb-2'>
+ {/* <div className='flex items-end mb-2'>
{product.manufacture?.name ? (
<Link
href={createSlug(
@@ -369,7 +372,7 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
<HeartIcon className='w-6' />
)}
</button>
- </div>
+ </div> */}
<h1 className='font-medium text-h-lg leading-8 md:text-title-md md:leading-10 mb-3'>
{activeVariant?.name}
</h1>
@@ -396,7 +399,59 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
active={informationTab == 'specification'}
className='rounded border border-gray_r-6 divide-y divide-gray_r-6'
>
- <SpecificationContent label='Ketersediaan'>
+ {/* <SpecificationContent label='Nomor SKU'>
+ <span>SKU-{product?.id}</span>
+ </SpecificationContent> */}
+ <SpecificationContent label='Item Code'>
+ <span>{activeVariant?.code || '-'}</span>
+ </SpecificationContent>
+ <SpecificationContent label='Manufacture'>
+ <Link
+ href={createSlug(
+ '/shop/brands/',
+ product.manufacture.name,
+ product.manufacture.id.toString()
+ )}
+ >
+ {product?.manufacture.logo ? (
+ <Image
+ width={55}
+ objectFit='contain'
+ src={product.manufacture.logo}
+ alt={product.manufacture.name}
+ />
+ ) : (
+ <p className='font-bold text-red-500'>
+ {product.manufacture.name}
+ </p>
+ )}
+ </Link>
+ </SpecificationContent>{' '}
+ <SpecificationContent label='Terjual'>
+ <span className='text-sm'>{product.qtySold || '-'}</span>
+ </SpecificationContent>{' '}
+ <SpecificationContent label='Berat Barang'>
+ {activeVariant?.weight > 0 && (
+ <span>{activeVariant?.weight} KG</span>
+ )}
+ {activeVariant?.weight == 0 && (
+ <a
+ href={whatsappUrl('productWeight', {
+ name: product.name,
+ url: createSlug(
+ '/shop/product/',
+ product.name,
+ product.id,
+ true
+ ),
+ })}
+ className='text-danger-500 font-medium'
+ >
+ Tanya Berat
+ </a>
+ )}
+ </SpecificationContent>
+ <SpecificationContent label='Persiapan Barang'>
<span>
{isLoadingSLA ? (
<Skeleton width='100px' height='full' />
@@ -440,68 +495,15 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
)}
</span>
</SpecificationContent>
- <SpecificationContent label='Nomor SKU'>
- <span>SKU-{product?.id}</span>
- </SpecificationContent>
- <SpecificationContent label='Part Number'>
- <span>{activeVariant?.code || '-'}</span>
- </SpecificationContent>
- <SpecificationContent label='Stok'>
- {activeVariant?.stock > 0 && (
- <span className='flex gap-x-1.5'>
- <div className='badge-solid-red'>Ready Stock</div>
- <div className='badge-gray'>
- {activeVariant?.stock > 5 ? '> 5' : '< 5'}
- </div>
- </span>
- )}
- {activeVariant?.stock == 0 && (
- <a
- href={whatsappUrl('product', {
- name: product.name,
- url: createSlug(
- '/shop/product/',
- product.name,
- product.id,
- true
- ),
- })}
- className='text-danger-500 font-medium'
- >
- Tanya Stok
- </a>
- )}
- </SpecificationContent>
- <SpecificationContent label='Berat Barang'>
- {activeVariant?.weight > 0 && (
- <span>{activeVariant?.weight} KG</span>
- )}
- {activeVariant?.weight == 0 && (
- <a
- href={whatsappUrl('productWeight', {
- name: product.name,
- url: createSlug(
- '/shop/product/',
- product.name,
- product.id,
- true
- ),
- })}
- className='text-danger-500 font-medium'
- >
- Tanya Berat
- </a>
- )}
- </SpecificationContent>
</TabContent>
<TabContent
active={informationTab == 'description'}
- className='leading-6 text-gray_r-11'
+ className={`${styles.richtextHtml} leading-6 text-gray_r-11`}
dangerouslySetInnerHTML={{
__html:
- product.description != ''
- ? product.description
+ product.parent.description != ''
+ ? product.parent.description
: 'Belum ada deskripsi produk.',
}}
/>
@@ -562,7 +564,7 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
const informationTabOptions = [
{ value: 'specification', label: 'Spesifikasi' },
- // { value: 'description', label: 'Deskripsi' },
+ { value: 'description', label: 'Deskripsi' },
// { value: 'information', label: 'Info Penting' }
];
@@ -581,11 +583,14 @@ const TabButton = ({ children, active, ...props }) => {
);
};
-const TabContent = ({ children, active, className, ...props }) => (
- <div {...props} className={`${active ? 'block' : 'hidden'} ${className}`}>
- {children}
- </div>
-);
+const TabContent = ({ children, active, className = '', ...props }) => {
+ if (!active) return null; // <— jangan render kalau tidak aktif
+ return (
+ <div {...props} className={className}>
+ {children}
+ </div>
+ );
+};
const SpecificationContent = ({ children, label }) => (
<div className='flex justify-between p-3'>
diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx
index f4f5882e..b6a3cfad 100644
--- a/src/lib/product/components/ProductCard.jsx
+++ b/src/lib/product/components/ProductCard.jsx
@@ -11,6 +11,10 @@ import { createSlug } from '@/core/utils/slug';
import whatsappUrl from '@/core/utils/whatsappUrl';
import useUtmSource from '~/hooks/useUtmSource';
import useDevice from '@/core/hooks/useDevice';
+import { BadgePercent, Tag } from 'lucide-react';
+import { TicketIcon } from '@heroicons/react/24/solid';
+import DesktopView from '@/core/components/views/DesktopView';
+import MobileView from '@/core/components/views/MobileView';
const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
const router = useRouter();
@@ -73,186 +77,461 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
if (variant == 'vertical') {
return (
- <div className='rounded shadow-sm border border-gray_r-4 bg-white'>
- <Link href={URL.product} className='border-b border-gray_r-4 relative' aria-label='Produk'>
- <div className='relative'>
- <Image
- src={image}
- alt={product?.name}
- className='gambarA w-full object-contain object-center h-36 sm:h-48'
- />
- <div className='absolute top-0 right-0 flex mt-3'>
- <div className='gambarB '>
- {product?.isSni && (
- <ImageNext
- src='/images/sni-logo.png'
- alt='SNI Logo'
- className='w-4 h-5 object-contain object-top sm:h-6'
- width={50}
- height={50}
- loading='eager'
- />
- )}
- </div>
- <div className='gambarC '>
- {product?.isTkdn && (
- <ImageNext
- src='/images/TKDN.png'
- alt='TKDN'
- className='w-11 h-6 object-contain object-top ml-1 mr-1 sm:h-6'
- width={50}
- height={50}
- loading='eager'
- />
- )}
- </div>
- </div>
- </div>
-
- {router.pathname != '/' && product?.flashSale?.id > 0 && (
- <div className='absolute bottom-0 w-full grid'>
- <div className='absolute bottom-0 w-full h-full'>
- <ImageNext
- src='/images/BG-FLASH-SALE.jpg'
- className='h-full'
- width={1000}
- height={100}
- loading='eager'
+ <>
+ <DesktopView>
+ <div className='rounded shadow-sm'>
+ <Link href={URL.product} className='relative' aria-label='Produk'>
+ <div className='relative'>
+ <Image
+ src={image}
+ alt={product?.name}
+ className='gambarA w-full object-contain object-center h-36 sm:h-48'
/>
+
+ <div className='absolute top-0 right-0 flex mt-3 z-20'>
+ <div className='gambarB '>
+ {product?.isSni && (
+ <ImageNext
+ src='/images/sni-logo.png'
+ alt='SNI Logo'
+ className='w-4 h-5 object-contain object-top sm:h-6'
+ width={50}
+ height={50}
+ loading='eager'
+ />
+ )}
+ </div>
+ <div className='gambarC '>
+ {product?.isTkdn && (
+ <ImageNext
+ src='/images/TKDN.png'
+ alt='TKDN'
+ className='w-11 h-6 object-contain object-top ml-1 mr-1 sm:h-6'
+ width={50}
+ height={50}
+ loading='eager'
+ />
+ )}
+ </div>
+ </div>
</div>
- <div className='relative'>
- <div className='flex gap-x-1 items-center p-2 justify-center'>
- <div className='bg-yellow-400 rounded-lg p-1 h-6 w-19 flex items-center justify-center '>
- <span className='text-sm font-bold text-black'>
- {Math.floor(product?.lowestPrice.discountPercentage)}%
- </span>
+
+ {(product?.lowestPrice?.discountPercentage ?? 0) > 0 && (
+ <div className='absolute right-0 top-1.5 '>
+ <div className='bg-red-600 text-white px-2 py-1 rounded-l-lg shadow-sm text-xs font-bold leading-none'>
+ {Math.floor(product.lowestPrice.discountPercentage)}%
</div>
- <div className='bg-red-600 border border-solid border-yellow-400 p-2 rounded-full h-6 flex w-fit items-center justify-center gap-x-2'>
+ </div>
+ )}
+
+ {(product?.variantTotal > 1 || product?.isInBu) && (
+ <div className='absolute bottom-1.5 left-1.5 z-30 flex items-center gap-2'>
+ {' '}
+ {product?.variantTotal > 1 && (
+ <div className='badge-gray'>
+ {' '}
+ {product.variantTotal} Varian{' '}
+ </div>
+ )}{' '}
+ {product?.isInBu && (
+ <Image
+ src='/images/PICKUP-NOW.png'
+ alt='Pick Up Now'
+ width={83}
+ height={24}
+ className='drop-shadow-sm'
+ loading='eager'
+ />
+ )}{' '}
+ </div>
+ )}
+ {router.pathname != '/' && product?.flashSale?.id > 0 && (
+ <div className='absolute bottom-0 w-full grid'>
+ <div className='absolute bottom-0 w-full h-full'>
<ImageNext
- src='/images/ICON_FLASH_SALE_WEBSITE_INDOTEKNIK.svg'
- alt='flash sale'
- width={13}
- height={5}
+ src='/images/BG-FLASH-SALE.jpg'
+ className='h-full'
+ width={1000}
+ height={100}
loading='eager'
/>
- <span className='text-white text-[9px] md:text-[10px] font-semibold'>
- {product?.flashSale?.tag != 'false' ||
- product?.flashSale?.tag
- ? product?.flashSale?.tag
- : 'FLASH SALE'}
+ </div>
+ <div className='relative'>
+ {/* <div className='flex gap-x-1 items-center p-2 justify-center'>
+ <div className='bg-yellow-400 rounded-lg p-1 h-6 w-19 flex items-center justify-center '>
+ <span className='text-sm font-bold text-black'>
+ {Math.floor(product?.lowestPrice.discountPercentage)}%
+ </span>
+ </div>
+ <div className='bg-red-600 border border-solid border-yellow-400 p-2 rounded-full h-6 flex w-fit items-center justify-center gap-x-2'>
+ <ImageNext
+ src='/images/ICON_FLASH_SALE_WEBSITE_INDOTEKNIK.svg'
+ alt='flash sale'
+ width={13}
+ height={5}
+ loading='eager'
+ />
+ <span className='text-white text-[9px] md:text-[10px] font-semibold'>
+ {product?.flashSale?.tag != 'false' ||
+ product?.flashSale?.tag
+ ? product?.flashSale?.tag
+ : 'FLASH SALE'}
+ </span>
+ </div>
+ </div> */}
+ </div>
+ </div>
+ )}
+ {/* {product.variantTotal > 1 && (
+ <div className='absolute badge-gray bottom-1.5 left-1.5'>
+ {product.variantTotal} Varian
+ </div>
+ )} */}
+ </Link>
+
+ <div className='p-2 sm:p-3 pb-3 text-caption-2 sm:text-body-2 leading-5'>
+ <div className='flex justify-between '>
+ {product?.manufacture?.name ? (
+ <Link
+ href={URL.manufacture}
+ className='mb-1 mt-1 truncate'
+ aria-label={product.manufacture.name}
+ >
+ {product.manufacture.name}
+ </Link>
+ ) : (
+ <div>-</div>
+ )}
+ </div>
+
+ {/* ⬇️ line clamp desktop dibiarkan seperti aslinya */}
+ <Link
+ href={URL.product}
+ aria-label={product?.name}
+ className={`mb-2 !text-gray_r-12 leading-6 block`}
+ style={{
+ WebkitLineClamp: 2,
+ display: '-webkit-box',
+ WebkitBoxOrient: 'vertical',
+ overflow: 'hidden',
+ }}
+ title={product?.name}
+ >
+ {product?.name}
+ </Link>
+
+ {product?.flashSale?.id > 0 &&
+ product?.lowestPrice.discountPercentage > 0 ? (
+ <div className='mb-2'>
+ <div className='flex items-baseline gap-1 min-w-0'>
+ <span className='text-danger-500 text-sm font-semibold whitespace-nowrap'>
+ {product?.lowestPrice.priceDiscount > 0 ? (
+ currencyFormat(product?.lowestPrice.priceDiscount)
+ ) : (
+ <a
+ rel='noopener noreferrer'
+ target='_blank'
+ href={callForPriceWhatsapp}
+ aria-label='Call for Inquiry'
+ >
+ Call for Inquiry
+ </a>
+ )}
+ </span>
+ <span
+ className='text-gray_r-11 line-through text-xs sm:text-caption-2
+ whitespace-nowrap overflow-hidden text-ellipsis max-w-[40%]'
+ >
+ {currencyFormat(product.lowestPrice.price)}
+ </span>
+ </div>
+ </div>
+ ) : (
+ <div className='text-danger-500 font-semibold mb-2'>
+ {product?.lowestPrice.price > 0 ? (
+ <>
+ {currencyFormat(product?.lowestPrice.priceDiscount)}
+ <div className='text-gray_r-9 text-[10px] font-normal'>
+ Include PPN:{' '}
+ {currencyFormat(
+ product.lowestPrice.price *
+ process.env.NEXT_PUBLIC_PPN
+ )}
+ </div>
+ </>
+ ) : (
+ <a
+ rel='noopener noreferrer'
+ target='_blank'
+ href={callForPriceWhatsapp}
+ aria-label='Call for Inquiry'
+ >
+ Call for Inquiry
+ </a>
+ )}
+ </div>
+ )}
+
+ {discount > 0 && (product?.flashSale?.id ?? 0) < 1 && (
+ <div className='mt-1 mb-1'>
+ <div className='flex items-center gap-2 text-green-600 min-w-0 mb-2 flex-nowrap'>
+ {/* label jangan pecah */}
+ <span className='text-xs font-medium shrink-0 whitespace-nowrap'>
+ Voucher
+ </span>
+
+ {/* chip bisa mengecil & memotong teks di dalam */}
+ <span
+ className='flex items-center gap-1.5 rounded bg-green-50 px-2.5 py-0.5 ring-0
+ min-w-0 max-w-full overflow-hidden'
+ >
+ <TicketIcon className='h-3.5 w-3.5 shrink-0' />
+ {/* nominal: truncate */}
+ <span className='text-xs font-medium truncate whitespace-nowrap min-w-0'>
+ {currencyFormat(discount)}
+ </span>
</span>
</div>
</div>
+ )}
+
+ <div className='flex w-full items-center gap-x-1 '>
+ {(product?.stockTotal > 0 || product?.qtySold > 0) && (
+ <div className='flex w-full items-center gap-x-2 flex-nowrap min-w-0'>
+ {product?.stockTotal > 0 && (
+ <div className='badge-solid-red text-center shrink-0 whitespace-nowrap'>
+ Ready Stock
+ </div>
+ )}
+
+ {product?.qtySold > 0 && (
+ <div className='text-gray_r-9 text-xs flex-1 min-w-0 truncate'>
+ {sellingProductFormat(product?.qtySold)} Terjual
+ </div>
+ )}
+ </div>
+ )}
</div>
</div>
- )}
- {product.variantTotal > 1 && (
- <div className='absolute badge-gray bottom-1.5 left-1.5'>
- {product.variantTotal} Varian
- </div>
- )}
- </Link>
- <div className='p-2 sm:p-3 pb-3 text-caption-2 sm:text-body-2 leading-5'>
- <div className='flex justify-between '>
- {product?.manufacture?.name ? (
- <Link href={URL.manufacture} className='mb-1 mt-1 truncate' aria-label={product.manufacture.name}>
- {product.manufacture.name}
- </Link>
- ) : (
- <div>-</div>
- )}
- {product?.isInBu && (
- <Link href='/panduan-pick-up-service' className='group' aria-label='pickup now'>
- <Image
- src='/images/PICKUP-NOW.png'
- className='group-hover:scale-105 transition-transform duration-200'
- alt='pickup now'
- width={90}
- height={12}
- loading='eager'
- />
- </Link>
- )}
</div>
- <Link
- href={URL.product}
- aria-label={product?.name}
- className={`mb-2 !text-gray_r-12 leading-6 block line-clamp-3 h-[64px]`}
- title={product?.name}
- >
- {product?.name}
- </Link>
- {product?.flashSale?.id > 0 &&
- product?.lowestPrice.discountPercentage > 0 ? (
- <>
- <div className='flex gap-x-1 mb-1 items-center'>
- <div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'>
- {currencyFormat(product.lowestPrice.price)}
+ </DesktopView>
+
+ <MobileView>
+ <div className='rounded shadow-sm'>
+ <Link href={URL.product} className='relative' aria-label='Produk'>
+ <div className='relative'>
+ <div
+ className='relative w-full overflow-hidden'
+ style={{ aspectRatio: '1 / 1' }}
+ >
+ <Image
+ src={image}
+ alt={product?.name}
+ fill
+ sizes='(max-width:640px) 100vw, 50vw'
+ className='object-contain object-center bg-white'
+ />
</div>
- <div className='badge-solid-red'>
- {Math.floor(product?.lowestPrice.discountPercentage)}%
+
+ {/* SNI / TKDN (kanan-atas, tetap) */}
+ <div className='absolute top-0 right-0 flex mt-3 z-20'>
+ <div className='gambarB'>
+ {product?.isSni && (
+ <ImageNext
+ src='/images/sni-logo.png'
+ alt='SNI Logo'
+ className='w-4 h-5 object-contain object-top sm:h-6'
+ width={50}
+ height={50}
+ loading='eager'
+ />
+ )}
+ </div>
+ <div className='gambarC'>
+ {product?.isTkdn && (
+ <ImageNext
+ src='/images/TKDN.png'
+ alt='TKDN'
+ className='w-11 h-6 object-contain object-top ml-1 mr-1 sm:h-6'
+ width={50}
+ height={50}
+ loading='eager'
+ />
+ )}
+ </div>
</div>
+
+ {/* BADGE DISKON Kanan-ATAS */}
+ {(product?.lowestPrice?.discountPercentage ?? 0) > 0 && (
+ <div className='absolute right-0 top-1.5 '>
+ <div className='bg-red-600 text-white px-2 py-1 rounded-l-lg shadow-sm text-xs font-bold leading-none'>
+ {Math.floor(product.lowestPrice.discountPercentage)}%
+ </div>
+ </div>
+ )}
+
+ {/* BOTTOM-LEFT: Varian + PICK UP NOW */}
+ {(product?.variantTotal > 1 || product?.isInBu) && (
+ <div className='absolute bottom-1.5 left-1.5 z-30 flex items-center gap-2'>
+ {product?.variantTotal > 1 && (
+ <div className='badge-gray'>
+ {product.variantTotal} Varian
+ </div>
+ )}
+ {product?.isInBu && (
+ <Image
+ src='/images/PICKUP-NOW.png'
+ alt='Pick Up Now'
+ width={83}
+ height={24}
+ className='drop-shadow-sm'
+ loading='eager'
+ />
+ )}
+ </div>
+ )}
</div>
- <div className='text-danger-500 font-semibold mb-2'>
- {product?.lowestPrice.priceDiscount > 0 ? (
- currencyFormat(product?.lowestPrice.priceDiscount)
- ) : (
- <a
- rel='noopener noreferrer'
- target='_blank'
- href={callForPriceWhatsapp}
- aria-label='Call for Inquiry'
+
+ {router.pathname != '/' && product?.flashSale?.id > 0 && (
+ <div className='absolute bottom-0 w-full grid z-10'>
+ <div className='absolute bottom-0 w-full h-full'>
+ <ImageNext
+ src='/images/BG-FLASH-SALE.jpg'
+ className='h-full'
+ width={1000}
+ height={100}
+ loading='eager'
+ />
+ </div>
+ </div>
+ )}
+ </Link>
+
+ {/* ⬇️ konten bawah (tidak diubah) */}
+ <div className='p-2 sm:p-3 pb-3 text-caption-2 sm:text-body-2 leading-5 min-w-0'>
+ <div className='flex justify-between '>
+ {product?.manufacture?.name ? (
+ <Link
+ href={URL.manufacture}
+ className='mt-1 truncate'
+ aria-label={product.manufacture.name}
>
- Call for Inquiry
- </a>
+ {product.manufacture.name}
+ </Link>
+ ) : (
+ <div>-</div>
)}
</div>
- </>
- ) : (
- <div className='text-danger-500 font-semibold mb-2 min-h-[40px]'>
- {product?.lowestPrice.price > 0 ? (
- <>
- {currencyFormat(product?.lowestPrice.price)}
- <div className='text-gray_r-9 text-[10px] font-normal mt-2'>
- Inc. PPN:{' '}
- {currencyFormat(
- product.lowestPrice.price * process.env.NEXT_PUBLIC_PPN
- )}
+
+ <Link
+ href={URL.product}
+ aria-label={product?.name}
+ className='block mb-1 leading-6 !text-gray_r-12 line-clamp-2'
+ title={product?.name}
+ style={{
+ display: '-webkit-box',
+ WebkitLineClamp: 2,
+ WebkitBoxOrient: 'vertical',
+ overflow: 'hidden',
+ }}
+ >
+ {product?.name}
+ </Link>
+
+ {product?.flashSale?.id > 0 &&
+ product?.lowestPrice.discountPercentage > 0 ? (
+ <div className='mb-2'>
+ <div className='flex items-baseline gap-1 min-w-0'>
+ <span className='text-danger-500 text-sm font-semibold whitespace-nowrap'>
+ {product?.lowestPrice.priceDiscount > 0 ? (
+ currencyFormat(product?.lowestPrice.priceDiscount)
+ ) : (
+ <a
+ rel='noopener noreferrer'
+ target='_blank'
+ href={callForPriceWhatsapp}
+ aria-label='Call for Inquiry'
+ >
+ Call for Inquiry
+ </a>
+ )}
+ </span>
+ <span
+ className='text-gray_r-11 line-through text-xs sm:text-caption-2
+ whitespace-nowrap overflow-hidden text-ellipsis max-w-[40%]'
+ >
+ {currencyFormat(product.lowestPrice.price)}
+ </span>
</div>
- </>
+ </div>
) : (
- <a
- rel='noopener noreferrer'
- target='_blank'
- href={callForPriceWhatsapp}
- aria-label='Call for Inquiry'
- >
- Call for Inquiry
- </a>
+ <div className='text-danger-500 font-semibold mb-2'>
+ {product?.lowestPrice.price > 0 ? (
+ <>
+ {currencyFormat(product?.lowestPrice.priceDiscount)}
+ <div className='text-gray_r-9 text-[10px] font-normal'>
+ Include PPN:{' '}
+ {currencyFormat(
+ product.lowestPrice.price *
+ process.env.NEXT_PUBLIC_PPN
+ )}
+ </div>
+ </>
+ ) : (
+ <a
+ rel='noopener noreferrer'
+ target='_blank'
+ href={callForPriceWhatsapp}
+ aria-label='Call for Inquiry'
+ >
+ Call for Inquiry
+ </a>
+ )}
+ </div>
)}
- </div>
- )}
- {discount > 0 && product?.flashSale?.id < 1 && (
- <div className='flex gap-x-1 mb-1 text-sm'>
- <div className='inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20'>
- Voucher : {currencyFormat(discount)}
- </div>
- </div>
- )}
- <div className='flex w-full items-center gap-x-1 '>
- {product?.stockTotal > 0 && (
- <div className='badge-solid-red'>Ready Stock</div>
- )}
- {/* <div className='badge-gray'>{product?.stockTotal > 5 ? '> 5' : '< 5'}</div> */}
- {product?.qtySold > 0 && (
- <div className='text-gray_r-9 text-[11px]'>
- {sellingProductFormat(product?.qtySold) + ' Terjual'}
- </div>
- )}
+ {discount > 0 && (product?.flashSale?.id ?? 0) < 1 && (
+ <div className='mt-1 mb-1'>
+ <div className='flex items-center gap-2 text-green-600 min-w-0 mb-2 flex-nowrap'>
+ {/* label jangan pecah */}
+ <span className='text-xs font-medium shrink-0 whitespace-nowrap'>
+ Voucher
+ </span>
+
+ {/* chip bisa mengecil & memotong teks di dalam */}
+ <span
+ className='flex items-center gap-1.5 rounded bg-green-50 px-2.5 py-0.5 ring-0
+ min-w-0 max-w-full overflow-hidden'
+ >
+ <TicketIcon className='h-3.5 w-3.5 shrink-0' />
+ {/* nominal: truncate */}
+ <span className='text-xs font-medium truncate whitespace-nowrap min-w-0'>
+ {currencyFormat(discount)}
+ </span>
+ </span>
+ </div>
+ </div>
+ )}
+
+ {(product?.stockTotal > 0 || product?.qtySold > 0) && (
+ <div className='flex w-full items-center gap-x-2 flex-nowrap min-w-0'>
+ {product?.stockTotal > 0 && (
+ <div className='badge-solid-red text-center shrink-0 whitespace-nowrap'>
+ Ready Stock
+ </div>
+ )}
+
+ {product?.qtySold > 0 && (
+ <div className='text-gray_r-9 text-xs flex-1 min-w-0 truncate'>
+ {sellingProductFormat(product?.qtySold)} Terjual
+ </div>
+ )}
+ </div>
+ )}
+ </div>
</div>
- </div>
- </div>
+ </MobileView>
+ </>
);
}
@@ -260,7 +539,11 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
return (
<div className='flex bg-white'>
<div className='w-4/12'>
- <Link href={URL.product} className='relative' aria-label={product?.name}>
+ <Link
+ href={URL.product}
+ className='relative'
+ aria-label={product?.name}
+ >
<div className='relative'>
<Image
src={image}
@@ -321,7 +604,11 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
)}
{product?.manufacture?.name ? (
<div className='flex justify-between'>
- <Link href={URL.manufacture} className='mb-1' aria-label={product?.manufacture.name}>
+ <Link
+ href={URL.manufacture}
+ className='mb-1'
+ aria-label={product?.manufacture.name}
+ >
{product.manufacture.name}
</Link>
{/* {product?.is_in_bu && (
@@ -338,46 +625,51 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
<Link
href={URL.product}
aria-label={product?.name}
- className={`mb-3 !text-gray_r-12 leading-6 line-clamp-3`}
+ className={`mb-3 !text-gray_r-12 leading-6 `}
+ style={{
+ display: '-webkit-box',
+ WebkitLineClamp: 2,
+ WebkitBoxOrient: 'vertical',
+ overflow: 'hidden',
+ }}
>
{product?.name}
</Link>
+
{product?.flashSale?.id > 0 &&
- product?.lowestPrice?.discountPercentage > 0 ? (
- <>
- {product?.lowestPrice.discountPercentage > 0 && (
- <div className='flex gap-x-1 mb-1 items-center'>
- <div className='badge-solid-red'>
- {Math.floor(product?.lowestPrice?.discountPercentage)}%
- </div>
- <div className='text-gray_r-11 line-through text-caption-2'>
- {currencyFormat(product?.lowestPrice?.price)}
- </div>
- </div>
- )}
+ product?.lowestPrice.discountPercentage > 0 ? (
+ <div className='mb-2'>
+ <div className='flex items-baseline gap-1'>
+ {/* harga sekarang (merah) */}
+ <span className='text-danger-500 font-semibold text-sm'>
+ {product?.lowestPrice.priceDiscount > 0 ? (
+ currencyFormat(product?.lowestPrice.priceDiscount) // ← perbaikan di sini
+ ) : (
+ <a
+ rel='noopener noreferrer'
+ target='_blank'
+ href={callForPriceWhatsapp}
+ aria-label='Call for Inquiry'
+ >
+ Call for Inquiry
+ </a>
+ )}
+ </span>
- <div className='text-danger-500 font-semibold mb-2'>
- {product?.lowestPrice?.priceDiscount > 0 ? (
- currencyFormat(product?.lowestPrice?.priceDiscount)
- ) : (
- <a
- rel='noopener noreferrer'
- target='_blank'
- href={callForPriceWhatsapp}
- aria-label='Call for Inquiry'
- >
- Call for Inquiry
- </a>
- )}
+ {/* harga lama (abu, dicoret) */}
+ <span className='text-gray_r-11 line-through text-xs sm:text-caption-2 '>
+ {currencyFormat(product.lowestPrice.price)}
+ </span>
</div>
- </>
+ </div>
) : (
- <div className='text-danger-500 font-semibold mb-2 min-h-[40px]'>
+ // === BLOK ELSE PUNYA KAMU, TIDAK DIUBAH ===
+ <div className='text-danger-500 font-semibold mb-2'>
{product?.lowestPrice.price > 0 ? (
<>
- {currencyFormat(product?.lowestPrice.price)}
- <div className='text-gray_r-9 text-[11px] sm:text-caption-2 font-normal mt-2'>
- Inc. PPN:{' '}
+ {currencyFormat(product?.lowestPrice.priceDiscount)}
+ <div className='text-gray_r-9 text-[10px] font-normal'>
+ Include PPN:{' '}
{currencyFormat(
product.lowestPrice.price * process.env.NEXT_PUBLIC_PPN
)}
@@ -396,10 +688,17 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
</div>
)}
- {discount > 0 && product?.flashSale?.id < 1 && (
- <div className='flex gap-x-1 mb-1 text-sm'>
- <div className='inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20'>
- Voucher : {currencyFormat(discount)}
+ {discount > 0 && (product?.flashSale?.id ?? 0) < 1 && (
+ <div className='mt-1 mb-1'>
+ {/* ⬇️ gunakan flex-wrap & min-w-0 */}
+ <div className='flex flex-wrap items-center gap-2 text-green-600 min-w-0 mb-2'>
+ <span className='text-xs font-medium'>Voucher</span>
+ <span className='inline-flex items-center gap-1.5 rounded bg-green-50 px-2.5 py-0.5 ring-0 max-w-full'>
+ <TicketIcon className='h-3.5 w-3.5 shrink-0' />
+ <span className='text-xs font-medium break-all'>
+ {currencyFormat(discount)}
+ </span>
+ </span>
</div>
</div>
)}
diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx
index 850d00cc..0c106df7 100644
--- a/src/lib/product/components/ProductSearch.jsx
+++ b/src/lib/product/components/ProductSearch.jsx
@@ -6,7 +6,10 @@ import { HStack, Image, Tag, TagCloseButton, TagLabel } from '@chakra-ui/react';
import axios from 'axios';
import _ from 'lodash';
import { toQuery } from 'lodash-contrib';
-import { FunnelIcon, AdjustmentsHorizontalIcon } from '@heroicons/react/24/outline';
+import {
+ FunnelIcon,
+ AdjustmentsHorizontalIcon,
+} from '@heroicons/react/24/outline';
import odooApi from '@/core/api/odooApi';
import searchSpellApi from '@/core/api/searchSpellApi';
import Link from '@/core/components/elements/Link/Link';
@@ -57,7 +60,8 @@ const ProductSearch = ({
if (!router.isReady) return;
const onBrandsPage = router.pathname.includes('brands');
- const hasOrder = typeof router.query?.orderBy === 'string' && router.query.orderBy !== '';
+ const hasOrder =
+ typeof router.query?.orderBy === 'string' && router.query.orderBy !== '';
if (onBrandsPage && !hasOrder && !appliedDefaultBrandOrder.current) {
let params = {
@@ -67,10 +71,8 @@ const ProductSearch = ({
params = _.pickBy(params, _.identity);
const qs = toQuery(params);
- // ganti URL tanpa nambah history & tanpa full reload
router.replace(`${prefixUrl}?${qs}`, undefined, { shallow: true });
- // sinkronkan state lokal
setOrderBy('popular');
appliedDefaultBrandOrder.current = true;
@@ -175,7 +177,11 @@ const ProductSearch = ({
}, [dataCategoriesProduct, dataLob]);
useEffect(() => {
- if (prefixUrl.includes('category') || prefixUrl.includes('lob') || router.asPath.includes('penawaran')) {
+ if (
+ prefixUrl.includes('category') ||
+ prefixUrl.includes('lob') ||
+ router.asPath.includes('penawaran')
+ ) {
setQueryFinal({ ...finalQuery, q, limit, orderBy });
} else {
setQueryFinal({ ...query, q, limit, orderBy });
@@ -430,7 +436,9 @@ const ProductSearch = ({
<div className='p-4 pt-0'>
{isNotReadyStockPage && isBrand && isBrand.logo && (
<div className='mb-3'>
- <h1 className='mb-2 font-semibold text-h-sm'>Brand Pencarian {q}</h1>
+ <h1 className='mb-2 font-semibold text-h-sm'>
+ Brand Pencarian {q}
+ </h1>
<Link
href={createSlug('/shop/brands/', isBrand.name, isBrand.id)}
className='inline'
@@ -462,7 +470,8 @@ const ProductSearch = ({
{pageCount > 1 ? (
<>
{productStart + 1}-
- {parseInt(productStart) + parseInt(productRows) > productFound
+ {parseInt(productStart) + parseInt(productRows) >
+ productFound
? productFound
: parseInt(productStart) + parseInt(productRows)}
&nbsp;dari&nbsp;
@@ -474,7 +483,8 @@ const ProductSearch = ({
&nbsp;produk{' '}
{query.q && (
<>
- untuk pencarian <span className='font-semibold'>{query.q}</span>
+ untuk pencarian{' '}
+ <span className='font-semibold'>{query.q}</span>
</>
)}
</>
@@ -512,7 +522,9 @@ const ProductSearch = ({
</div>
)}
{!!dataLob?.length && <LobSectionCategory categories={dataLob} />}
- {!!dataCategories?.length && <CategorySection categories={dataCategories} />}
+ {!!dataCategories?.length && (
+ <CategorySection categories={dataCategories} />
+ )}
<div className='grid grid-cols-2 gap-3'>
{products &&
products.map((product) => (
@@ -567,9 +579,7 @@ const ProductSearch = ({
prefixUrl={prefixUrl}
defaultBrand={defaultBrand}
/>
-
<div className='h-6' />
-
<SideBanner query={search} />
</div>
@@ -621,7 +631,7 @@ const ProductSearch = ({
<>
{productStart + 1}-
{parseInt(productStart) + parseInt(productRows) >
- productFound
+ productFound
? productFound
: parseInt(productStart) + parseInt(productRows)}
&nbsp;dari&nbsp;
@@ -697,8 +707,8 @@ const ProductSearch = ({
href={
query?.q
? whatsappUrl('productSearch', {
- name: query.q,
- })
+ name: query.q,
+ })
: whatsappUrl()
}
className='text-danger-500'
@@ -783,9 +793,9 @@ const FilterChoicesComponent = ({
</Tag>
)}
{brandValues?.length > 0 ||
- categoryValues?.length > 0 ||
- priceFrom ||
- priceTo ? (
+ categoryValues?.length > 0 ||
+ priceFrom ||
+ priceTo ? (
<span>
<button
className='btn-transparent py-2 px-5 h-[40px] text-red-700'
diff --git a/src/lib/product/styles/desc_mobile_variant.module.css b/src/lib/product/styles/desc_mobile_variant.module.css
new file mode 100644
index 00000000..d2c86d77
--- /dev/null
+++ b/src/lib/product/styles/desc_mobile_variant.module.css
@@ -0,0 +1,18 @@
+.richtextHtml {
+ line-height: 1.7;
+ /* word-break: break-word; */
+ overflow-x: auto;
+}
+.richtextHtml h1 {
+ font-weight: 600;
+ margin: 0.5rem 0;
+ font-size: clamp(1.5rem, 2.5vw, 2rem);
+ line-height: 1.25;
+}
+.richtextHtml table {
+ width: 100%;
+ max-width: 100%;
+ border-collapse: collapse;
+ table-layout: auto;
+ margin: 12px 0;
+}