diff options
Diffstat (limited to 'src/lib')
| -rw-r--r-- | src/lib/brand/components/MediaCard.jsx | 8 | ||||
| -rw-r--r-- | src/lib/product/components/Product/ProductDesktopVariant.jsx | 86 | ||||
| -rw-r--r-- | src/lib/product/components/Product/ProductMobileVariant.jsx | 147 | ||||
| -rw-r--r-- | src/lib/product/components/ProductCard.jsx | 687 | ||||
| -rw-r--r-- | src/lib/product/components/ProductSearch.jsx | 44 | ||||
| -rw-r--r-- | src/lib/product/styles/desc_mobile_variant.module.css | 18 |
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)} dari @@ -474,7 +483,8 @@ const ProductSearch = ({ 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)} dari @@ -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; +} |
