summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--public/robots.txt3
-rw-r--r--src-migrate/modules/product-detail/components/Image.tsx3
-rw-r--r--src-migrate/modules/product-detail/components/PriceAction.tsx73
-rw-r--r--src-migrate/modules/product-detail/components/ProductDetail.tsx96
-rw-r--r--src-migrate/modules/product-detail/styles/image.module.css14
-rw-r--r--src-migrate/modules/product-detail/styles/price-action.module.css10
-rw-r--r--src-migrate/modules/promo/components/Voucher.tsx2
-rw-r--r--src-migrate/modules/promo/styles/voucher.module.css33
-rw-r--r--src-migrate/services/product.ts5
-rw-r--r--src-migrate/types/product.ts8
-rw-r--r--src/core/components/elements/Image/Image.jsx12
-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
-rw-r--r--src/pages/shop/search.jsx37
-rw-r--r--src/utils/solrMapping.js24
19 files changed, 932 insertions, 378 deletions
diff --git a/public/robots.txt b/public/robots.txt
index 1520dbb4..08a35d98 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -23,9 +23,6 @@ User-agent: Adsbot-Google
Allow: /my/*
Allow: /shop/search/*
-User-agent: *
-Disallow: sentral.indoteknik.com/*
-Disallow: erp.indoteknik.com/*
Sitemap: https://indoteknik.com/sitemap/products.xml
Sitemap: https://indoteknik.com/sitemap/brands.xml
diff --git a/src-migrate/modules/product-detail/components/Image.tsx b/src-migrate/modules/product-detail/components/Image.tsx
index 96ae2027..d99b683c 100644
--- a/src-migrate/modules/product-detail/components/Image.tsx
+++ b/src-migrate/modules/product-detail/components/Image.tsx
@@ -50,8 +50,7 @@ const Image = ({ product }: Props) => {
<ImageUI
src={image}
alt={product.name}
- width={256}
- height={256}
+ fill
className={style['image']}
loading='eager'
priority
diff --git a/src-migrate/modules/product-detail/components/PriceAction.tsx b/src-migrate/modules/product-detail/components/PriceAction.tsx
index ffc9ba40..6cc2f0bf 100644
--- a/src-migrate/modules/product-detail/components/PriceAction.tsx
+++ b/src-migrate/modules/product-detail/components/PriceAction.tsx
@@ -66,17 +66,21 @@ const PriceAction = ({ product }: Props) => {
setQuantityInput('1');
}, [selectedVariant]);
- let voucherPastiHemat = 0;
+ const price = activePrice?.price_discount || activePrice?.price || 0;
+ const pricedigit = String(Math.floor(price)).length;
+ const fontSize = pricedigit >= 9 ? '20px' : undefined;
- if (
- product?.voucher_pasti_hemat
- ? product?.voucher_pasti_hemat.length
- : voucherPastiHemat > 0
- ) {
- const stringVoucher = product?.voucher_pasti_hemat[0];
- const validJsonString = stringVoucher.replace(/'/g, '"');
- voucherPastiHemat = JSON.parse(validJsonString);
- }
+ // let voucherPastiHemat = 0;
+
+ // if (
+ // product?.voucher_pasti_hemat
+ // ? product?.voucher_pasti_hemat.length
+ // : voucherPastiHemat > 0
+ // ) {
+ // const stringVoucher = product?.voucher_pasti_hemat[0];
+ // const validJsonString = stringVoucher.replace(/'/g, '"');
+ // voucherPastiHemat = JSON.parse(validJsonString);
+ // }
return (
<div
@@ -91,25 +95,40 @@ const PriceAction = ({ product }: Props) => {
<DesktopView>
<div className='flex items-end gap-x-2'>
{activePrice.discount_percentage > 0 && (
- <>
- <div className={style['disc-badge']}>
- {Math.floor(activePrice.discount_percentage)}%
- </div>
- <div className={style['disc-price']}>
- Rp {formatCurrency(activePrice.price || 0)}
- </div>
- </>
+ <div className={style['disc-badge']}>
+ {Math.floor(activePrice.discount_percentage)}%
+ </div>
)}
- <div className={style['main-price']}>
- Rp {formatCurrency(activePrice.price_discount || 0)}
+ <div
+ className={style['main-price']}
+ style={fontSize ? { fontSize } : undefined}
+ >
+ Rp{' '}
+ {formatCurrency(
+ activePrice.discount_percentage > 0
+ ? activePrice.price_discount || 0
+ : activePrice.price || 0
+ )}
</div>
+ {activePrice.discount_percentage > 0 && (
+ <div className={style['disc-price']}>
+ Rp {formatCurrency(activePrice.price || 0)}
+ </div>
+ )}
</div>
<div className='h-1' />
<div className={style['secondary-text']}>
Termasuk PPN: Rp{' '}
- {formatCurrency(Math.round(activePrice.price_discount * PPN))}
+ {formatCurrency(
+ Math.round(
+ (activePrice.discount_percentage > 0
+ ? activePrice.price_discount
+ : activePrice.price) * PPN
+ )
+ )}
</div>
</DesktopView>
+
<MobileView>
<div className='flex items-end gap-x-2'>
{activePrice.discount_percentage > 0 ? (
@@ -159,7 +178,7 @@ const PriceAction = ({ product }: Props) => {
<DesktopView>
<div className='h-4' />
- <div className='flex gap-x-5 items-center'>
+ <div className='flex gap-x-4 items-center'>
{/* Qty */}
<div className='relative flex items-center'>
<button
@@ -191,20 +210,20 @@ const PriceAction = ({ product }: Props) => {
</div>
{/* Stok */}
- <div>
+ <div className='min-w-[89px]'>
<Skeleton
isLoaded={sla}
h='21px'
className={sla?.qty < 10 ? 'text-red-600 font-medium' : ''}
>
- Stock : {sla?.qty}{' '}
+ Stock : {sla?.qty}
</Skeleton>
</div>
{/* Pickup badge */}
- <div>
+ <div className='shrink-0'>
{qtyPickUp > 0 && (
- <div className='flex items-center gap-2'>
+ <div className='flex items-center'>
<Link href='/panduan-pick-up-service' className='group'>
<Image
src='/images/PICKUP-NOW.png'
@@ -218,7 +237,7 @@ const PriceAction = ({ product }: Props) => {
)}
</div>
</div>
- <span className='text-[12px] text-red-500 italic'>
+ <span className='block text-[12px] text-red-500 italic mt-1'>
* {qtyPickUp} barang bisa di pickup
</span>
</DesktopView>
diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx
index f32bb38e..51b080ef 100644
--- a/src-migrate/modules/product-detail/components/ProductDetail.tsx
+++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx
@@ -22,16 +22,17 @@ import PriceAction from './PriceAction';
import SimilarBottom from './SimilarBottom';
import SimilarSide from './SimilarSide';
import dynamic from 'next/dynamic';
-
+import { TicketIcon } from '@heroicons/react/24/solid';
import { gtagProductDetail } from '@/core/utils/googleTag';
+import currencyFormat from '@/core/utils/currencyFormat';
type Props = {
product: IProductDetail;
};
const RWebShare = dynamic(
- () => import('react-web-share').then(m => m.RWebShare),
+ () => import('react-web-share').then((m) => m.RWebShare),
{ ssr: false }
);
@@ -42,7 +43,9 @@ const ProductDetail = ({ product }: Props) => {
const router = useRouter();
const [auth, setAuth] = useState<any>(null);
useEffect(() => {
- try { setAuth(getAuth() ?? null); } catch { }
+ try {
+ setAuth(getAuth() ?? null);
+ } catch {}
}, []);
const canShare =
@@ -87,7 +90,6 @@ const ProductDetail = ({ product }: Props) => {
setSelectedVariant(selectedVariant);
}, []);
-
const allImages = (() => {
const arr: string[] = [];
if (product?.image) arr.push(product.image);
@@ -95,7 +97,6 @@ const ProductDetail = ({ product }: Props) => {
Array.isArray(product?.image_carousel) &&
product.image_carousel.length
) {
-
const set = new Set(arr);
for (const img of product.image_carousel) {
if (!set.has(img)) {
@@ -108,15 +109,14 @@ const ProductDetail = ({ product }: Props) => {
})();
const [mainImage, setMainImage] = useState(allImages[0] || '');
+ const [discount, setDiscount] = useState(0);
useEffect(() => {
-
if (!allImages.includes(mainImage)) {
setMainImage(allImages[0] || '');
}
}, [allImages]);
-
const sliderRef = useRef<HTMLDivElement | null>(null);
const [currentIdx, setCurrentIdx] = useState(0);
@@ -138,6 +138,47 @@ const ProductDetail = ({ product }: Props) => {
setMainImage(allImages[i] || '');
};
+ const voucherNew = Array.isArray(product?.new_voucher_pasti_hemat)
+ ? product.new_voucher_pasti_hemat[0]
+ : undefined;
+
+ const voucher = voucherNew ?? null;
+
+ const type = voucher?.discount_type ?? ''; // 'percentage' | 'percent' | 'fixed'
+ const amount = Number(
+ voucher?.discountAmount ?? voucher?.discount_amount ?? 0
+ );
+ const max = Number(voucher?.max_discount ?? 0);
+ const min = Number(voucher?.min_purchase ?? 0);
+
+ const basePrice =
+ Number(product?.lowest_price?.price_discount ?? 0) ||
+ Number(product?.lowest_price?.price ?? 0);
+
+ function calcVoucherDiscount() {
+ if (!voucher || !basePrice) return 0;
+ if (min > 0 && basePrice < min) return 0;
+
+ const percent = type.toLowerCase().startsWith('percent')
+ ? amount <= 1
+ ? amount * 100
+ : amount
+ : 0;
+
+ let cut = 0;
+ if (type.toLowerCase().startsWith('percent')) {
+ cut = basePrice * (percent / 100);
+ } else {
+ cut = amount || 0;
+ }
+
+ if (max > 0) cut = Math.min(cut, max);
+ return Math.max(0, cut);
+ }
+
+ useEffect(() => {
+ setDiscount(calcVoucherDiscount());
+ }, [product?.lowest_price]);
return (
<>
@@ -165,7 +206,6 @@ const ProductDetail = ({ product }: Props) => {
>
{allImages.length > 0 ? (
allImages.map((img, i) => (
-
<div
key={i}
className='w-full flex-shrink-0 snap-center flex justify-center items-center'
@@ -200,8 +240,9 @@ const ProductDetail = ({ product }: Props) => {
<button
key={i}
aria-label={`Ke slide ${i + 1}`}
- className={`w-2 h-2 rounded-full ${currentIdx === i ? 'bg-gray-800' : 'bg-gray-300'
- }`}
+ className={`w-2 h-2 rounded-full ${
+ currentIdx === i ? 'bg-gray-800' : 'bg-gray-300'
+ }`}
onClick={() => scrollToIndex(i)}
/>
))}
@@ -220,10 +261,11 @@ const ProductDetail = ({ product }: Props) => {
{allImages.map((img, index) => (
<div
key={index}
- className={`flex-shrink-0 w-16 h-16 cursor-pointer border-2 rounded-md transition-colors ${mainImage === img
- ? 'border-red-500 ring-2 ring-red-200'
- : 'border-gray-200 hover:border-gray-300'
- }`}
+ className={`flex-shrink-0 w-16 h-16 cursor-pointer border-2 rounded-md transition-colors ${
+ mainImage === img
+ ? 'border-red-500 ring-2 ring-red-200'
+ : 'border-gray-200 hover:border-gray-300'
+ }`}
onClick={() => setMainImage(img)}
>
<img
@@ -249,6 +291,17 @@ const ProductDetail = ({ product }: Props) => {
{/* ===== Kolom kanan: info ===== */}
<div className='md:w-8/12 px-4 md:pl-6'>
<div className='h-6 md:h-0' />
+ {isMobile && (
+ <div className='text text-sm font-medium'>
+ <TicketIcon className='inline text-yellow-300 w-5 h-5' />{' '}
+ Pakai{' '}
+ <span className='text-green-600 font-extrabold'>
+ {' '}
+ Voucher belanja hemat {currencyFormat(discount)}
+ </span>{' '}
+ saat checkout
+ </div>
+ )}
<h1 className={style['title']}>{product.name}</h1>
<div className='h-3 md:h-0' />
<Information product={product} />
@@ -281,7 +334,8 @@ const ProductDetail = ({ product }: Props) => {
className={style['description']}
dangerouslySetInnerHTML={{
__html:
- !product.description || product.description == '<p><br></p>'
+ !product.description ||
+ product.description == '<p><br></p>'
? 'Belum ada deskripsi'
: product.description,
}}
@@ -317,10 +371,16 @@ const ProductDetail = ({ product }: Props) => {
data={{
text: 'Check out this product',
title: `${product.name} - Indoteknik.com`,
- url: (process.env.NEXT_PUBLIC_SELF_HOST || '') + (router?.asPath || '/'),
+ url:
+ (process.env.NEXT_PUBLIC_SELF_HOST || '') +
+ (router?.asPath || '/'),
}}
>
- <Button variant='link' colorScheme='gray' leftIcon={<Share2Icon size={18} />}>
+ <Button
+ variant='link'
+ colorScheme='gray'
+ leftIcon={<Share2Icon size={18} />}
+ >
Share
</Button>
</RWebShare>
@@ -350,8 +410,6 @@ const ProductDetail = ({ product }: Props) => {
</div>
</>
);
-
-
};
export default ProductDetail;
diff --git a/src-migrate/modules/product-detail/styles/image.module.css b/src-migrate/modules/product-detail/styles/image.module.css
index e472fe8d..f33a659b 100644
--- a/src-migrate/modules/product-detail/styles/image.module.css
+++ b/src-migrate/modules/product-detail/styles/image.module.css
@@ -1,9 +1,21 @@
-.wrapper {
+/* .wrapper {
@apply h-[250px] md:h-[340px] flex items-center justify-center border border-gray-200 rounded-lg p-4 relative;
}
.image {
@apply object-contain object-center h-full w-full;
+} */
+
+.wrapper {
+ @apply relative border border-gray-200 rounded-lg overflow-hidden;
+
+ @apply w-full aspect-[1/1];
+
+ @apply mx-auto;
+}
+
+.image {
+ @apply w-full h-full object-cover object-center block;
}
.absolute-info {
diff --git a/src-migrate/modules/product-detail/styles/price-action.module.css b/src-migrate/modules/product-detail/styles/price-action.module.css
index cea50bff..b94c33f7 100644
--- a/src-migrate/modules/product-detail/styles/price-action.module.css
+++ b/src-migrate/modules/product-detail/styles/price-action.module.css
@@ -2,16 +2,16 @@
@apply font-medium text-gray-500;
}
.main-price {
- @apply font-medium text-danger-500 text-title-md;
+ @apply font-medium text-danger-500 text-title-sm;
}
.action-wrapper {
@apply flex gap-x-2.5;
}
.quantity-input {
- @apply w-24 h-10 text-center border border-gray-300 rounded focus:outline-none;
- /* Padding di kiri dan kanan untuk memberi ruang bagi tombol */
- padding-left: 2rem;
- padding-right: 2rem;
+ @apply w-24 h-10 text-center border border-gray-300 rounded focus:outline-none;
+ /* Padding di kiri dan kanan untuk memberi ruang bagi tombol */
+ padding-left: 2rem;
+ padding-right: 2rem;
}
.contact-us {
diff --git a/src-migrate/modules/promo/components/Voucher.tsx b/src-migrate/modules/promo/components/Voucher.tsx
index 0c225c74..c2c65766 100644
--- a/src-migrate/modules/promo/components/Voucher.tsx
+++ b/src-migrate/modules/promo/components/Voucher.tsx
@@ -123,7 +123,7 @@ const VoucherComponent = () => {
<>
<h1 className={style['title']}>Pakai Voucher Belanja</h1>
- <div className='h-6' />
+ {/* <div className='h-6' /> */}
{voucherQuery?.isLoading && (
<div className='grid grid-cols-3 gap-x-4 animate-pulse'>
diff --git a/src-migrate/modules/promo/styles/voucher.module.css b/src-migrate/modules/promo/styles/voucher.module.css
index 22d07f91..ab652a5f 100644
--- a/src-migrate/modules/promo/styles/voucher.module.css
+++ b/src-migrate/modules/promo/styles/voucher.module.css
@@ -2,42 +2,55 @@
@apply text-h-sm md:text-h-lg font-semibold;
}
+/* beri ruang dari footer */
.voucher-section {
- @apply w-full;
+ @apply w-full pb-6 md:pb-8;
+}
+
+/* biar card bisa full-width tanpa ada gutter ekstra */
+.voucher-section :global(.swiper-slide) {
+ @apply flex items-start px-0 md:px-0;
}
.voucher-card {
- @apply w-full md:w-11/12 h-3/4 rounded-xl border items-center border-gray-200 shadow-md p-4 flex gap-x-4 ;
+ /* FULL width (hapus md:w-11/12), tetap horizontal & kompak */
+ @apply w-full rounded-xl border border-gray-200 shadow-md
+ p-4 flex items-start gap-x-4 bg-white;
}
.voucher-image {
- @apply bg-gray-100 rounded-lg w-4/12 h-fit object-contain object-center;
+ /* gambar kecil & stabil */
+ @apply bg-gray-100 rounded-lg w-3/12 md:w-2/12 h-auto object-contain object-center;
}
.voucher-content {
- @apply flex-1 flex flex-col;
+ /* penting: min-w-0 supaya teks bisa wrap/ellipsis dan card memanjang mulus */
+ @apply flex-1 min-w-0 flex flex-col justify-between;
}
.voucher-title {
- @apply font-medium text-body-1 leading-6 mb-1;
+ @apply font-medium text-body-1 leading-6 line-clamp-2 mb-1;
}
-
.voucher-desc {
- @apply text-gray-800 line-clamp-2 text-caption-1;
+ @apply text-gray-800 text-caption-1 line-clamp-1;
}
.voucher-bottom {
- @apply flex justify-between mt-2;
+ /* biar rapih di semua ukuran layar */
+ @apply flex flex-col sm:flex-row justify-between items-start sm:items-center
+ gap-2 sm:gap-3 mt-3;
}
.voucher-code-desc {
@apply text-gray-500 text-caption-1;
}
-
.voucher-code {
@apply text-red-700 font-medium;
}
+/* tombol tetap kecil, tapi lebar penuh di mobile; tidak keluar dari card */
.voucher-copy {
- @apply bg-gray-200 hover:bg-danger-500 text-danger-500 hover:text-white transition-colors rounded-lg flex items-center justify-center px-6;
+ @apply bg-gray-200 hover:bg-danger-500 text-danger-500 hover:text-white transition-colors
+ rounded-lg flex items-center justify-center px-5 py-1.5 text-sm whitespace-nowrap
+ w-full md:w-auto;
}
diff --git a/src-migrate/services/product.ts b/src-migrate/services/product.ts
index 77b645f0..9ab7814e 100644
--- a/src-migrate/services/product.ts
+++ b/src-migrate/services/product.ts
@@ -2,6 +2,7 @@ import { IProduct, IProductDetail } from '~/types/product';
import snakeCase from 'snakecase-keys';
import odooApi from '~/libs/odooApi';
import { ICategoryBreadcrumb } from '~/types/category';
+import { json } from 'stream/consumers';
const SELF_HOST = process.env.NEXT_PUBLIC_SELF_HOST;
@@ -13,6 +14,10 @@ export const getProductById = async (
const params = new URLSearchParams({ id, auth: tier });
return await fetch(`${url}?${params.toString()}`)
.then((res) => res.json())
+ // .then((json) => {
+ // console.log('[Prod detail]: ', json);
+ // return json;
+ // })
.then((res) => {
if (res.length > 0) return snakeCase(res[0]) as IProductDetail;
return null;
diff --git a/src-migrate/types/product.ts b/src-migrate/types/product.ts
index 746cdd4a..7212d34d 100644
--- a/src-migrate/types/product.ts
+++ b/src-migrate/types/product.ts
@@ -35,7 +35,13 @@ export interface IProduct {
name: string;
logo: string;
};
- voucher_pasti_hemat: any;
+ new_voucher_pasti_hemat: {
+ min_purchase: number;
+ discount_percentage: number;
+ max_discount: number;
+ discount_type: string;
+ };
+ // voucher_pasti_hemat: any;
}
export interface IProductDetail extends IProduct {
diff --git a/src/core/components/elements/Image/Image.jsx b/src/core/components/elements/Image/Image.jsx
index d7b19821..ad565be6 100644
--- a/src/core/components/elements/Image/Image.jsx
+++ b/src/core/components/elements/Image/Image.jsx
@@ -1,5 +1,5 @@
-import { LazyLoadImage } from 'react-lazy-load-image-component'
-import 'react-lazy-load-image-component/src/effects/opacity.css'
+import { LazyLoadImage } from 'react-lazy-load-image-component';
+import 'react-lazy-load-image-component/src/effects/opacity.css';
/**
* The `Image` component is used to display lazy-loaded images.
@@ -17,10 +17,10 @@ const Image = ({ ...props }) => {
placeholderSrc='/images/indoteknik-placeholder.png'
effect='opacity'
alt={props.src ? props.alt : 'Image Not Found - Indoteknik'}
- wrapperClassName='bg-white'
+ // wrapperClassName='bg-white'
loading='eager'
/>
- )
-}
+ );
+};
-export default Image
+export default Image;
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;
+}
diff --git a/src/pages/shop/search.jsx b/src/pages/shop/search.jsx
index db3b449a..157d2091 100644
--- a/src/pages/shop/search.jsx
+++ b/src/pages/shop/search.jsx
@@ -1,25 +1,33 @@
-import dynamic from 'next/dynamic'
-import { useRouter } from 'next/router'
-import _ from 'lodash-contrib'
-import Seo from '@/core/components/Seo'
-import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react'
-import Link from 'next/link'
+import dynamic from 'next/dynamic';
+import { useRouter } from 'next/router';
+import _ from 'lodash-contrib';
+import Seo from '@/core/components/Seo';
+import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react';
+import Link from 'next/link';
-const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout'))
-const ProductSearch = dynamic(() => import('@/lib/product/components/ProductSearch'))
+const BasicLayout = dynamic(() =>
+ import('@/core/components/layouts/BasicLayout')
+);
+const ProductSearch = dynamic(() =>
+ import('@/lib/product/components/ProductSearch')
+);
export default function Search() {
- const router = useRouter()
+ const router = useRouter();
return (
<BasicLayout>
<Seo title={`Cari produk ${router.query.q || ''} di Indoteknik.com`} />
- <div className='container mx-auto py-4 md:py-6'>
+ <div className='container mx-auto py-4 md:py-6 mb-3'>
{router.query?.q && (
<Breadcrumb>
<BreadcrumbItem>
- <BreadcrumbLink as={Link} href='/' className='!text-danger-500 whitespace-nowrap'>
+ <BreadcrumbLink
+ as={Link}
+ href='/'
+ className='!text-danger-500 whitespace-nowrap'
+ >
Home
</BreadcrumbLink>
</BreadcrumbItem>
@@ -32,8 +40,9 @@ export default function Search() {
</Breadcrumb>
)}
</div>
-
- {!_.isEmpty(router.query) && <ProductSearch query={router.query} prefixUrl='/shop/search' />}
+ {!_.isEmpty(router.query) && (
+ <ProductSearch query={router.query} prefixUrl='/shop/search' />
+ )}
</BasicLayout>
- )
+ );
}
diff --git a/src/utils/solrMapping.js b/src/utils/solrMapping.js
index 33f0cbaf..b7733e92 100644
--- a/src/utils/solrMapping.js
+++ b/src/utils/solrMapping.js
@@ -121,7 +121,6 @@ export const variantsMappingSolr = (parent, products, pricelist) => {
image: product.image_s || '',
code: product.default_code_s || '',
isFlashsale: flashsaleTime(product?.flashsale_end_date_s)?.isFlashSale,
- isFlashsale: flashsaleTime(product?.flashsale_end_date_s)?.isFlashSale,
name: product.display_name_s || '',
price: { price, priceDiscount, discountPercentage },
variantTotal: product.variant_total_i || 0,
@@ -140,12 +139,35 @@ export const variantsMappingSolr = (parent, products, pricelist) => {
logo: parent[0]?.x_logo_manufacture_s,
};
}
+
productMapped.parent = {
id: parent[0]?.product_id_i || '',
image: parent[0]?.image_s || '',
name: parent[0]?.name_s || '',
description: parent[0]?.description_t || '',
};
+
+ productMapped.newVoucherPastiHemat = [
+ {
+ min_purchase:
+ parent[0]?.voucher_min_purchase_f ??
+ product?.voucher_min_purchase_f ??
+ 0,
+ discount_type:
+ parent[0]?.voucher_discount_type_s ??
+ product?.voucher_discount_type_s ??
+ '',
+ discount_amount:
+ parent[0]?.voucher_discount_amount_f ??
+ product?.voucher_discount_amount_f ??
+ 0,
+ max_discount:
+ parent[0]?.voucher_max_discount_f ??
+ product?.voucher_max_discount_f ??
+ 0,
+ },
+ ];
+
return productMapped;
});
};