summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--public/images/produk_tidak_tersedia.svg91
-rw-r--r--public/robots.txt4
-rw-r--r--src-migrate/modules/product-detail/components/AddToCart.tsx9
-rw-r--r--src-migrate/modules/product-detail/components/AddToQuotation.tsx20
-rw-r--r--src-migrate/modules/product-detail/components/PriceAction.tsx34
-rw-r--r--src-migrate/modules/product-detail/components/ProductDetail.tsx126
-rw-r--r--src-migrate/pages/shop/product/[slug].tsx2
-rw-r--r--src/core/components/layouts/BasicLayout.jsx101
-rw-r--r--src/lib/product/components/Product/ProductDesktopVariant.jsx151
-rw-r--r--src/lib/product/components/Product/ProductMobileVariant.jsx281
-rw-r--r--src/pages/api/shop/product-detail.js30
-rw-r--r--src/pages/google_merchant/products/[page].js4
-rw-r--r--src/pages/google_merchant/products/index.js4
-rw-r--r--src/pages/shop/product/variant/[slug].jsx46
-rw-r--r--src/pages/sitemap/blogs.xml.js6
-rw-r--r--src/pages/sitemap/brands.xml.js6
-rw-r--r--src/pages/sitemap/categories-brand.xml.js6
-rw-r--r--src/pages/sitemap/categories-brand/[page].js6
-rw-r--r--src/pages/sitemap/categories.xml.js6
-rw-r--r--src/pages/sitemap/products.xml.js6
-rw-r--r--src/pages/sitemap/products/[page].js12
21 files changed, 601 insertions, 350 deletions
diff --git a/public/images/produk_tidak_tersedia.svg b/public/images/produk_tidak_tersedia.svg
new file mode 100644
index 00000000..69fe214b
--- /dev/null
+++ b/public/images/produk_tidak_tersedia.svg
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 24.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 3365 2426.5" style="enable-background:new 0 0 3365 2426.5;" xml:space="preserve">
+<style type="text/css">
+ .st0{opacity:0.3;}
+ .st1{fill:#4D4D4D;}
+</style>
+<g>
+ <g class="st0">
+ <g>
+ <path class="st1" d="M885.5,1497.4c12.5-5.9,25-6.7,37.3-2.5c12.3,4.2,21.5,12.6,27.4,25.2c5.9,12.7,6.5,25.1,1.8,37.2
+ c-4.7,12.1-13.3,21.1-25.8,27l-16.5,7.7l17.4,37.2l-31.8,14.9L837.3,1520L885.5,1497.4z M912.1,1554.2c3.5-1.7,6-4.3,7.2-7.9
+ c1.3-3.6,1-7.4-0.8-11.3c-1.8-3.9-4.5-6.5-8.1-7.8c-3.6-1.3-7.1-1.1-10.7,0.6l-16.5,7.7l12.4,26.4L912.1,1554.2z"/>
+ <path class="st1" d="M1065.2,1564.7l-40.4-30l-11.4,5.3l18.8,40.1l-31.8,14.9l-58.1-124.2l49.7-23.2c12.7-5.9,25.2-6.8,37.5-2.7
+ c12.4,4.1,21.5,12.3,27.3,24.8c3.5,7.4,4.8,15.2,3.9,23.1c-0.9,8-3.7,15.3-8.5,22l47,34L1065.2,1564.7z M987.8,1485.3l13,27.8
+ l17.6-8.2c4-1.7,6.6-4.4,7.8-8.2c1.3-3.8,0.9-7.7-1-11.9c-1.9-4.1-4.8-6.9-8.5-8.4c-3.7-1.4-7.5-1.2-11.4,0.6L987.8,1485.3z"/>
+ <path class="st1" d="M1207.6,1478.5c-6.8,18.4-19.2,31.8-37.4,40.3c-18.2,8.5-36.5,9.5-55,2.9c-18.4-6.6-31.9-19-40.4-37.2
+ c-8.5-18.2-9.4-36.5-2.7-54.9c6.8-18.4,19.2-31.8,37.4-40.3c18.2-8.5,36.5-9.5,55-2.9c18.4,6.6,31.9,19,40.4,37.2
+ C1213.5,1441.8,1214.4,1460.1,1207.6,1478.5z M1127.2,1489.5c9.3,3.3,18.8,2.8,28.4-1.7s16.1-11.4,19.5-20.7
+ c3.4-9.3,2.8-18.9-1.8-28.7c-4.6-9.8-11.6-16.4-20.9-19.7c-9.3-3.3-18.8-2.8-28.4,1.7s-16.1,11.4-19.5,20.7
+ c-3.4,9.3-2.8,18.9,1.8,28.7C1110.9,1479.6,1117.9,1486.2,1127.2,1489.5z"/>
+ <path class="st1" d="M1241.2,1331c17.1-8,34.3-8.8,51.4-2.3c17.1,6.5,29.8,18.5,38,36.2c8.2,17.6,9.4,35.1,3.4,52.3
+ s-17.5,29.9-34.7,38l-50.6,23.7l-58.1-124.2L1241.2,1331z M1284.9,1424.5c8.6-4,14.3-10.2,17.1-18.5c2.8-8.3,1.9-17.1-2.5-26.6
+ c-4.4-9.5-10.7-15.8-18.8-19c-8.1-3.2-16.5-2.8-25.1,1.3l-17.9,8.4l29.4,62.8L1284.9,1424.5z"/>
+ <path class="st1" d="M1452.7,1372.1c-5.5,12.2-15.4,21.7-29.8,28.4c-14.4,6.8-28.1,8.3-40.9,4.7c-12.9-3.6-22.4-12-28.5-25.1
+ l-39-83.4l31.8-14.9l37.6,80.4c4.9,10.4,13.1,12.9,24.7,7.5c11.6-5.4,14.9-13.3,10.1-23.7l-37.6-80.4l31.7-14.9l39,83.4
+ C1457.9,1347.2,1458.2,1359.9,1452.7,1372.1z"/>
+ <path class="st1" d="M1592,1318.2l-35.5,16.6l-61.7-39l26,55.7l-31.8,14.9l-58.1-124.2l31.8-14.9l24.4,52.2l9.4-68.1l35.5-16.6
+ l-11.2,78.7L1592,1318.2z"/>
+ <path class="st1" d="M1668.3,1131.1l14.4,30.7l-30.7,14.4l43.7,93.5l-31.8,14.9l-43.7-93.5l-30.7,14.4l-14.4-30.7L1668.3,1131.1z
+ "/>
+ <path class="st1" d="M1679.8,1125.7l31.8-14.9l58.1,124.2l-31.8,14.9L1679.8,1125.7z"/>
+ <path class="st1" d="M1781.6,1078.1c17.1-8,34.3-8.8,51.4-2.3c17.1,6.5,29.8,18.5,38,36.2c8.2,17.6,9.4,35.1,3.4,52.3
+ c-6,17.3-17.5,29.9-34.7,38l-50.6,23.7l-58.1-124.2L1781.6,1078.1z M1825.4,1171.5c8.6-4,14.3-10.2,17.1-18.5
+ c2.8-8.3,1.9-17.1-2.5-26.6c-4.4-9.5-10.7-15.8-18.8-19c-8.1-3.2-16.5-2.8-25.1,1.3l-17.9,8.4l29.4,62.8L1825.4,1171.5z"/>
+ <path class="st1" d="M1987.6,1133l-14.3-15.1l-42.3,19.8l3,20.4l-34.4,16.1l-18.5-142.7l40-18.7l99.2,104.9L1987.6,1133z
+ M1926.4,1106.5l25-11.7l-32.3-36.3L1926.4,1106.5z"/>
+ <path class="st1" d="M2132.2,1065.3l-35.5,16.6l-61.7-39l26,55.7l-31.8,14.9l-58.1-124.2l31.8-14.9l24.4,52.2l9.4-68.1l35.5-16.6
+ l-11.2,78.7L2132.2,1065.3z"/>
+ <path class="st1" d="M2208.6,878.3l14.4,30.7l-30.7,14.4l43.7,93.5l-31.8,14.9l-43.7-93.5l-30.7,14.4l-14.4-30.7L2208.6,878.3z"
+ />
+ <path class="st1" d="M2296,952.4l48.1-22.5l13.9,29.8l-79.8,37.4l-58.1-124.2l78.9-36.9l13.9,29.6l-47.2,22.1l8.1,17.2l42.7-20
+ l13.7,29.3l-42.7,20L2296,952.4z"/>
+ <path class="st1" d="M2437.8,922.3l-40.4-30l-11.4,5.3l18.8,40.1l-31.8,14.9L2315,828.5l49.7-23.2c12.7-5.9,25.2-6.8,37.5-2.7
+ c12.4,4.1,21.5,12.3,27.3,24.8c3.5,7.4,4.8,15.2,3.9,23.1c-0.9,8-3.7,15.3-8.5,22l47,34L2437.8,922.3z M2360.4,842.9l13,27.8
+ l17.6-8.2c4-1.7,6.6-4.4,7.8-8.2c1.3-3.8,0.9-7.7-1-11.9c-1.9-4.1-4.8-6.9-8.5-8.4c-3.7-1.4-7.5-1.2-11.4,0.6L2360.4,842.9z"/>
+ <path class="st1" d="M2526.7,884c-13.1,6.1-25.3,8.5-36.5,6.9c-11.2-1.5-20.8-6.2-28.6-14.1l19.8-28.5c9.8,8.8,20.5,10.5,32.2,5
+ c9.7-4.5,13.3-9.6,10.7-15.1c-1.7-3.6-5.9-5.3-12.5-4.9c-2.8,0.1-8.8,0.9-17.9,2.3c-12.9,2-24.1,1.6-33.5-1.3
+ c-9.4-2.9-16.6-9.6-21.6-20.3c-5.4-11.5-5.6-22.7-0.7-33.6c4.9-10.9,13.5-19.3,25.8-25c10.4-4.9,20.8-6.8,31.1-5.7
+ c10.3,1.1,19.8,5.2,28.3,12.5l-19.3,28.1c-8.2-7.4-16.8-9-25.8-4.8c-3.8,1.8-6.3,3.9-7.5,6.3c-1.2,2.5-1.3,4.9-0.2,7.3
+ c1.3,2.7,3.8,4.4,7.6,4.9c3.8,0.6,10.4,0.1,20-1.3c6.9-0.6,12.5-1.2,16.9-1.6c4.4-0.4,9.4-0.2,15.1,0.7c5.7,0.8,10.7,3,15,6.6
+ c4.3,3.5,7.8,8.4,10.7,14.5c5.7,12.2,5.8,23.7,0.4,34.6C2550.7,868.6,2540.9,877.3,2526.7,884z"/>
+ <path class="st1" d="M2602,809.2l48.1-22.5l13.9,29.8l-79.8,37.4l-58.1-124.2l78.9-36.9l13.9,29.6l-47.2,22.1l8.1,17.2l42.7-20
+ l13.7,29.3l-42.7,20L2602,809.2z"/>
+ <path class="st1" d="M2671.5,661.6c17.1-8,34.3-8.8,51.4-2.3c17.1,6.5,29.8,18.5,38,36.2c8.2,17.6,9.4,35.1,3.4,52.3
+ c-6,17.3-17.5,29.9-34.7,38l-50.6,23.7L2621,685.3L2671.5,661.6z M2715.3,755.1c8.6-4,14.3-10.2,17.1-18.5
+ c2.8-8.3,1.9-17.1-2.5-26.6c-4.4-9.5-10.7-15.8-18.8-19c-8.1-3.2-16.5-2.8-25.1,1.3l-17.9,8.4l29.4,62.8L2715.3,755.1z"/>
+ <path class="st1" d="M2746.5,626.5l31.8-14.9l58.1,124.2l-31.8,14.9L2746.5,626.5z"/>
+ <path class="st1" d="M2933.2,690.5l-14.3-15.1l-42.3,19.8l3,20.4l-34.4,16.1L2826.7,589l40-18.7l99.2,104.9L2933.2,690.5z
+ M2872,664l25-11.7l-32.3-36.3L2872,664z"/>
+ </g>
+ <g>
+ <path class="st1" d="M827,1712.7c-57.7-38.8-115.6-77.6-173.4-116.3c-11.1-7.4-22.1-15-33.4-22.2c-8.3-5.2-17.3-5.7-26.3-1.6
+ c-10.5,4.8-15.6,13.4-16.8,24.7c-7.9,75.8-15.9,151.5-23.9,227.3c-0.8,7.7-1.8,15.3-2.2,23c-0.4,9,3.1,16.6,10.1,22.2
+ c9.9,8,20.6,8.5,32.1,3.2c77-36.1,154.1-72.2,231.2-108.2c0.8-0.4,1.7-0.8,2.5-1.2c7.6-3.9,12.9-9.8,14.5-18.1
+ c1-5,0.3-10.3,0.3-15.4l-3.7-7.8C834.4,1719,831,1715.4,827,1712.7z M819,1744.4c-1.1,0.9-2.5,1.5-3.8,2.1
+ c-38.4,18-76.8,36-115.2,53.9c-38.3,17.9-76.6,35.9-114.9,53.8c-1.3,0.6-2.6,1.2-4,1.5c-4.8,1.1-9.2-2.2-9.4-7.2
+ c-0.1-1.3,0.1-2.7,0.2-4c8.6-81.5,17.2-163,25.7-244.4c0.1-0.9,0.2-1.8,0.3-2.7c1.2-5.8,6.6-8.4,11.8-5.5
+ c2.7,1.5,5.2,3.4,7.7,5.1c66.3,44.4,132.5,88.8,198.8,133.2c1.1,0.7,2.3,1.5,3.2,2.4C823.1,1735.9,822.9,1741.4,819,1744.4z"/>
+ <path class="st1" d="M647.5,1655.6c5.1,11.2,9.9,22.6,14.8,33.9c-0.1,0-0.2,0.1-0.3,0.1c5,11.7,10.1,23.4,15.1,35.2
+ c1.9,4.4,1.5,6.8-1.8,10.3c-2.7,2.9-6,4.2-9.8,4.7c-3.6,0.4-6.4-1.1-8.1-4.4c-11.6-22.7-23.2-45.4-34.8-68.1
+ c-3-6-2.2-10.7,2.2-14.8c5.1-4.7,12.9-6.4,17.6-3.1C644.6,1650.8,646.4,1653.2,647.5,1655.6z"/>
+ <path class="st1" d="M696.3,1757.2c3.3,7,0.2,16.1-6.7,19.5c-7.2,3.5-16.1,0.3-19.5-7c-3.5-7.4-0.4-16.1,6.9-19.5
+ C683.9,1746.8,692.9,1750.1,696.3,1757.2z"/>
+ </g>
+ </g>
+ <g class="st0">
+ <path class="st1" d="M3115.2,1052.3L641,2210.1c-89.1,41.7-195.5,3.1-237.2-85.9l-240-512.8c-41.7-89.1-3.1-195.5,85.9-237.2
+ L2724,216.4c89.1-41.7,195.5-3.1,237.2,85.9l240,512.8C3242.8,904.2,3204.3,1010.6,3115.2,1052.3z M265.3,1407.4
+ c-70.8,33.1-101.5,117.7-68.3,188.6l240,512.8c33.1,70.8,117.7,101.5,188.6,68.3l2474.2-1157.9c70.8-33.1,101.5-117.7,68.3-188.6
+ l-240-512.8c-33.1-70.8-117.7-101.5-188.6-68.3L265.3,1407.4z"/>
+ </g>
+ <g class="st0">
+ <path class="st1" d="M3220.3,1092.1L604.2,2316.4c-93.4,43.7-204.9,3.3-248.6-90.1L54.6,1583c-43.7-93.4-3.3-204.9,90.1-248.6
+ L2760.8,110.1c93.4-43.7,204.9-3.3,248.6,90.1l301.1,643.3C3354.1,936.9,3313.7,1048.4,3220.3,1092.1z M148.9,1343.5
+ c-88.4,41.4-126.6,146.9-85.3,235.3l301.1,643.3c41.4,88.4,146.9,126.6,235.3,85.3L3216.1,1083c88.4-41.4,126.6-146.9,85.3-235.3
+ l-301.1-643.3c-41.4-88.4-146.9-126.6-235.3-85.3L148.9,1343.5z"/>
+ </g>
+</g>
+</svg>
diff --git a/public/robots.txt b/public/robots.txt
index 1520dbb4..f9caa9bf 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -17,15 +17,11 @@ Disallow: /shop/search/*
Disallow: /promo/*
Disallow: /shop/brands/*?*
Disallow: /shop/category/*?*
-Disallow: /shop/product/*?*
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/AddToCart.tsx b/src-migrate/modules/product-detail/components/AddToCart.tsx
index 147fd6d2..0dc39c1c 100644
--- a/src-migrate/modules/product-detail/components/AddToCart.tsx
+++ b/src-migrate/modules/product-detail/components/AddToCart.tsx
@@ -66,6 +66,8 @@ const AddToCart = ({
weight: '',
isFlashSale: false,
});
+ const hasPrice =
+ !!product?.lowest_price && Number(product.lowest_price.price) > 0;
useEffect(() => {
const fetchData = async () => {
@@ -183,6 +185,7 @@ const AddToCart = ({
colorScheme={btnConfig[source].colorScheme}
variant={btnConfig[source].variant}
className='w-full'
+ isDisabled={!hasPrice || status === 'loading'}
>
{btnConfig[source].text}
</Button>
@@ -194,6 +197,7 @@ const AddToCart = ({
colorScheme={btnConfig[source].colorScheme}
variant={btnConfig[source].variant}
className='w-full'
+ isDisabled={!hasPrice || status === 'loading'}
>
{btnConfig[source].text}
</Button>
@@ -208,7 +212,10 @@ const AddToCart = ({
{/* ===== MOBILE LAYOUT: konten scroll + footer fixed di dalam popup ===== */}
<div className='md:hidden flex flex-col max-h-[75vh]'>
{/* area scroll */}
- <div className='flex-1 overflow-y-auto' style={{ scrollbarWidth: 'none' }}>
+ <div
+ className='flex-1 overflow-y-auto'
+ style={{ scrollbarWidth: 'none' }}
+ >
{/* HEADER ITEM */}
<div className='flex mt-4'>
<div className='w-[25%]'>
diff --git a/src-migrate/modules/product-detail/components/AddToQuotation.tsx b/src-migrate/modules/product-detail/components/AddToQuotation.tsx
index ebfcef32..3e811330 100644
--- a/src-migrate/modules/product-detail/components/AddToQuotation.tsx
+++ b/src-migrate/modules/product-detail/components/AddToQuotation.tsx
@@ -60,6 +60,8 @@ const AddToQuotation = ({
`fq=-manufacture_id_i:${product.manufacture?.id || 0}`,
].join('&');
const [addCartAlert, setAddCartAlert] = useState(false);
+ const hasPrice =
+ !!product?.lowest_price && Number(product.lowest_price.price) > 0;
const handleButton = async () => {
if (typeof auth !== 'object') {
@@ -124,9 +126,10 @@ const AddToQuotation = ({
color={'red'}
colorScheme='white'
className='w-full border-2 p-2 gap-1 hover:bg-slate-100 flex items-center'
+ isDisabled={!hasPrice}
>
<ImageNext
- src= {isDesktop ? '/images/doc_red.svg' : '/images/doc.svg'}
+ src={isDesktop ? '/images/doc_red.svg' : '/images/doc.svg'}
alt='penawaran instan'
className=''
width={25}
@@ -191,7 +194,20 @@ const AddToQuotation = ({
</>
)}
- {!!product.lowest_price && product.lowest_price.price === 0 && (
+ {(!!product.lowest_price && product.lowest_price.price === 0) ||
+ product.lowest_price.price < 0 ? (
+ <span>
+ Hubungi kami untuk dapatkan harga terbaik,{' '}
+ <Link
+ href={askAdminUrl}
+ target='_blank'
+ className='font-medium underline'
+ color={'red'}
+ >
+ klik disini
+ </Link>
+ </span>
+ ) : (
<span>
Hubungi kami untuk dapatkan harga terbaik,{' '}
<Link
diff --git a/src-migrate/modules/product-detail/components/PriceAction.tsx b/src-migrate/modules/product-detail/components/PriceAction.tsx
index ffc9ba40..d73ab5f6 100644
--- a/src-migrate/modules/product-detail/components/PriceAction.tsx
+++ b/src-migrate/modules/product-detail/components/PriceAction.tsx
@@ -37,6 +37,7 @@ const PriceAction = ({ product }: Props) => {
} = useProductDetail();
const [qtyPickUp, setQtyPickUp] = useState(0);
const { isDesktop, isMobile } = useDevice();
+
useEffect(() => {
setActive(selectedVariant);
if (product.variants.length > 2 && product.variants[0].price.price === 0) {
@@ -77,6 +78,7 @@ const PriceAction = ({ product }: Props) => {
const validJsonString = stringVoucher.replace(/'/g, '"');
voucherPastiHemat = JSON.parse(validJsonString);
}
+ const hasPrice = Number(product?.lowest_price?.price) > 0;
return (
<div
@@ -144,7 +146,7 @@ const PriceAction = ({ product }: Props) => {
</>
)}
- {!!activePrice && activePrice.price === 0 && (
+ {/* {!!activePrice && activePrice.price === 0 && (
<span>
Hubungi kami untuk dapatkan harga terbaik,{' '}
<Link
@@ -155,7 +157,7 @@ const PriceAction = ({ product }: Props) => {
klik disini
</Link>
</span>
- )}
+ )} */}
<DesktopView>
<div className='h-4' />
@@ -164,27 +166,32 @@ const PriceAction = ({ product }: Props) => {
<div className='relative flex items-center'>
<button
type='button'
- className='absolute left-0 px-2 py-1 h-full text-gray-500'
+ className='absolute left-0 px-2 py-1 h-full text-gray-500 disabled:opacity-40'
onClick={() =>
setQuantityInput(String(Math.max(1, Number(quantityInput) - 1)))
}
+ disabled={!hasPrice}
>
-
</button>
+
<input
type='number'
id='quantity'
min={1}
value={quantityInput}
onChange={(e) => setQuantityInput(e.target.value)}
- className={style['quantity-input']}
+ className={`${style['quantity-input']} disabled:bg-gray-100 disabled:text-gray-400`}
+ disabled={!hasPrice}
/>
+
<button
type='button'
- className='absolute right-0 px-2 py-1 h-full text-gray-500'
+ className='absolute right-0 px-2 py-1 h-full text-gray-500 disabled:opacity-40'
onClick={() =>
setQuantityInput(String(Number(quantityInput) + 1))
}
+ disabled={!hasPrice}
>
+
</button>
@@ -195,9 +202,11 @@ const PriceAction = ({ product }: Props) => {
<Skeleton
isLoaded={sla}
h='21px'
- className={sla?.qty < 10 ? 'text-red-600 font-medium' : ''}
+ className={
+ !hasPrice || sla?.qty < 10 ? 'text-red-600 font-medium' : ''
+ }
>
- Stock : {sla?.qty}{' '}
+ Stock : {hasPrice ? sla?.qty : 'Habis'}
</Skeleton>
</div>
@@ -218,9 +227,9 @@ const PriceAction = ({ product }: Props) => {
)}
</div>
</div>
- <span className='text-[12px] text-red-500 italic'>
+ {/* <span className='text-[12px] text-red-500 italic'>
* {qtyPickUp} barang bisa di pickup
- </span>
+ </span> */}
</DesktopView>
{/* ===== MOBILE: grid kiri-kanan, kanan hanya qty ===== */}
@@ -255,11 +264,11 @@ const PriceAction = ({ product }: Props) => {
)}
</div>
- {qtyPickUp > 0 && (
+ {/* {qtyPickUp > 0 && (
<div className='text-[12px] mt-1 text-red-500 italic'>
* {qtyPickUp} barang bisa di pickup
</div>
- )}
+ )} */}
</div>
{/* Kanan: hanya qty, rata kanan */}
@@ -274,6 +283,7 @@ const PriceAction = ({ product }: Props) => {
)
}
aria-label='Kurangi'
+ disabled={!hasPrice}
>
<span className='text-2xl leading-none'>–</span>
</button>
@@ -288,6 +298,7 @@ const PriceAction = ({ product }: Props) => {
[appearance:textfield]
[&::-webkit-outer-spin-button]:appearance-none
[&::-webkit-inner-spin-button]:appearance-none'
+ disabled={!hasPrice}
/>
<button
@@ -297,6 +308,7 @@ const PriceAction = ({ product }: Props) => {
setQuantityInput(String(Number(quantityInput) + 1))
}
aria-label='Tambah'
+ disabled={!hasPrice}
>
<span className='text-2xl leading-none'>+</span>
</button>
diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx
index f32bb38e..e4ba2b2f 100644
--- a/src-migrate/modules/product-detail/components/ProductDetail.tsx
+++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx
@@ -5,7 +5,12 @@ import { useRouter } from 'next/router';
import { useEffect, useRef, useState, UIEvent } from 'react';
import { Button } from '@chakra-ui/react';
-import { MessageCircleIcon, Share2Icon } from 'lucide-react';
+import {
+ AlertCircle,
+ AlertTriangle,
+ MessageCircleIcon,
+ Share2Icon,
+} from 'lucide-react';
import { LazyLoadComponent } from 'react-lazy-load-image-component';
import useDevice from '@/core/hooks/useDevice';
@@ -23,7 +28,6 @@ import SimilarBottom from './SimilarBottom';
import SimilarSide from './SimilarSide';
import dynamic from 'next/dynamic';
-
import { gtagProductDetail } from '@/core/utils/googleTag';
type Props = {
@@ -31,7 +35,7 @@ type Props = {
};
const RWebShare = dynamic(
- () => import('react-web-share').then(m => m.RWebShare),
+ () => import('react-web-share').then((m) => m.RWebShare),
{ ssr: false }
);
@@ -42,7 +46,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 +93,6 @@ const ProductDetail = ({ product }: Props) => {
setSelectedVariant(selectedVariant);
}, []);
-
const allImages = (() => {
const arr: string[] = [];
if (product?.image) arr.push(product.image);
@@ -95,7 +100,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 +112,14 @@ const ProductDetail = ({ product }: Props) => {
})();
const [mainImage, setMainImage] = useState(allImages[0] || '');
+ const hasPrice = Number(product?.lowest_price?.price) > 0;
useEffect(() => {
-
if (!allImages.includes(mainImage)) {
setMainImage(allImages[0] || '');
}
}, [allImages]);
-
const sliderRef = useRef<HTMLDivElement | null>(null);
const [currentIdx, setCurrentIdx] = useState(0);
@@ -138,9 +141,32 @@ const ProductDetail = ({ product }: Props) => {
setMainImage(allImages[i] || '');
};
-
return (
<>
+ <div className='relative'>
+ {isDesktop && !hasPrice && (
+ <div className='absolute inset-0 z-[20] flex items-center justify-center pointer-events-none select-none'>
+ <img
+ src='/images/produk_tidak_tersedia.svg'
+ alt='Produk tidak tersedia'
+ className='w-[47%] opacity-50 -translate-x-[3%] -translate-y-[-70%]'
+ />
+ </div>
+ )}
+ </div>
+
+ <div className='relative'>
+ {isMobile && !hasPrice && (
+ <div className='absolute inset-0 z-[50] flex items-center justify-center pointer-events-none select-none'>
+ <img
+ src='/images/produk_tidak_tersedia.svg'
+ alt='Produk tidak tersedia'
+ className='w-[100%] opacity-[1000%] -translate-x-[0%] -translate-y-[-197%]'
+ />
+ </div>
+ )}
+ </div>
+
<div className='md:flex md:flex-wrap'>
<div className='w-full mb-4 md:mb-0 px-4 md:px-0'>
<Breadcrumb id={product.id} name={product.name} />
@@ -165,7 +191,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 +225,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 +246,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
@@ -247,13 +274,45 @@ const ProductDetail = ({ product }: Props) => {
{/* <<=== TUTUP kolom kiri */}
{/* ===== Kolom kanan: info ===== */}
- <div className='md:w-8/12 px-4 md:pl-6'>
- <div className='h-6 md:h-0' />
- <h1 className={style['title']}>{product.name}</h1>
- <div className='h-3 md:h-0' />
- <Information product={product} />
- <div className='h-6' />
- </div>
+ {isDesktop && (
+ <div className='md:w-8/12 px-4 md:pl-6'>
+ {!hasPrice && (
+ <div className='bg-red-50 p-2 py-1.5 rounded-lg border border-red-500 flex gap-1 items-center '>
+ <AlertTriangle
+ size={18}
+ className='text-red-600 shrink-0 mx-2'
+ />
+ <h1 className='text-red-600 font-normal text-h-sm'>
+ Maaf untuk saat ini Produk yang anda cari tidak tersedia
+ </h1>
+ </div>
+ )}
+ <div className='h-6 md:h-0' />
+ <h1 className={style['title']}>{product.name}</h1>
+ <div className='h-3 md:h-0' />
+ <Information product={product} />
+ <div className='h-6' />
+ </div>
+ )}
+ {isMobile && (
+ <div className='md:w-8/12 px-4 md:pl-6 relative'>
+ {!hasPrice && (
+ <div className='bg-red-50 p-2 py-1.5 border-b border-red-500 flex gap-1 items-center w-screen relative left-1/2 right-1/2 -translate-x-1/2'>
+ <AlertTriangle
+ size={18}
+ className='text-red-600 shrink-0 mx-2'
+ />
+ <h1 className='text-red-600 font-normal text-h-sm'>
+ Maaf untuk saat ini Produk yang anda cari tidak tersedia
+ </h1>
+ </div>
+ )}
+ <h1 className={style['title']}>{product.name}</h1>
+ <div className='h-3 md:h-0' />
+ <Information product={product} />
+ <div className='h-6' />
+ </div>
+ )}
</div>
<div className='h-full'>
@@ -281,7 +340,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,
}}
@@ -302,13 +362,16 @@ const ProductDetail = ({ product }: Props) => {
target='_blank'
colorScheme='gray'
leftIcon={<MessageCircleIcon size={18} />}
+ isDisabled={!hasPrice}
>
Ask Admin
</Button>
<span>|</span>
- <AddToWishlist productId={product.id} />
+ <div className={hasPrice ? '' : 'opacity-40 pointer-events-none'}>
+ <AddToWishlist productId={product.id} />
+ </div>
<span>|</span>
@@ -317,10 +380,17 @@ 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} />}
+ isDisabled={!hasPrice}
+ >
Share
</Button>
</RWebShare>
@@ -350,8 +420,6 @@ const ProductDetail = ({ product }: Props) => {
</div>
</>
);
-
-
};
export default ProductDetail;
diff --git a/src-migrate/pages/shop/product/[slug].tsx b/src-migrate/pages/shop/product/[slug].tsx
index 058e4832..8d6558b1 100644
--- a/src-migrate/pages/shop/product/[slug].tsx
+++ b/src-migrate/pages/shop/product/[slug].tsx
@@ -49,7 +49,7 @@ export const getServerSideProps: GetServerSideProps<PageProps> = async (
Array.isArray(product.variants) &&
product.variants.some((v) => (v?.price?.price ?? 0) > 0);
- if (!hasValidVariant) return { notFound: true };
+ // if (!hasValidVariant) return { notFound: true };
// bikin canonical path yang BERSIH dan KONSISTEN dari data produk,
// bukan dari URL request user (jadi gak ikut ?utm_source, ?ref=, dsb)
diff --git a/src/core/components/layouts/BasicLayout.jsx b/src/core/components/layouts/BasicLayout.jsx
index 81f8b41f..b13e807d 100644
--- a/src/core/components/layouts/BasicLayout.jsx
+++ b/src/core/components/layouts/BasicLayout.jsx
@@ -33,6 +33,10 @@ const BasicLayout = ({ children }) => {
const { product } = useProductContext();
+ const hasPrice =
+ product &&
+ Number(product?.lowest_price?.price || product?.price?.price) > 0;
+
useEffect(() => {
if (
router.pathname === '/shop/product/[slug]' ||
@@ -54,8 +58,7 @@ const BasicLayout = ({ children }) => {
useEffect(() => {
const handleMouseOut = (event) => {
-
- if (!buttonRef.current) return;
+ if (!buttonRef.current) return;
const rect = buttonRef.current.getBoundingClientRect();
if (event.clientY <= 0) {
setButtonPosition(rect);
@@ -114,56 +117,58 @@ const BasicLayout = ({ children }) => {
onAnimationEnd={() => setHighlight(false)}
/>
)}
- <Navbar isMobile = {isMobile} />
+ <Navbar isMobile={isMobile} />
<AnimationLayout>
{children}
- <div
- className={`fixed ${
- isMobile && isProductPage ? 'bottom-40' : 'bottom-16'
- } right-4 sm:bottom-14 sm:right-10 z-50`}
- >
- <div className='flex flex-row items-center'>
- <a
- href={whatsappUrl(templateWA, payloadWA, urlPath)}
- className='flex flex-row items-center'
- rel='noopener noreferrer'
- target='_blank'
- >
- <span
- className={`text-green-300 text-lg font-bold mr-4 ${
- wobble ? 'animate-wobble' : ''
- }`}
- onAnimationEnd={() => setWobble(false)}
+ {(!isProductPage || hasPrice) && (
+ <div
+ className={`fixed ${
+ isMobile && isProductPage ? 'bottom-40' : 'bottom-16'
+ } right-4 sm:bottom-14 sm:right-10 z-50`}
+ >
+ <div className='flex flex-row items-center'>
+ <a
+ href={whatsappUrl(templateWA, payloadWA, urlPath)}
+ className='flex flex-row items-center'
+ rel='noopener noreferrer'
+ target='_blank'
+ >
+ <span
+ className={`text-green-300 text-lg font-bold mr-4 ${
+ wobble ? 'animate-wobble' : ''
+ }`}
+ onAnimationEnd={() => setWobble(false)}
+ >
+ {isDesktop && 'Whatsapp'}
+ </span>
+ </a>
+ <a
+ href={whatsappUrl(templateWA, payloadWA, urlPath)}
+ className='elemen-whatsapp p-4 rounded-full bg-[#4FB84A] border border-green-300 flex items-center'
+ rel='noopener noreferrer'
+ target='_blank'
+ ref={buttonRef}
>
- {isDesktop && 'Whatsapp'}
- </span>
- </a>
- <a
- href={whatsappUrl(templateWA, payloadWA, urlPath)}
- className='elemen-whatsapp p-4 rounded-full bg-[#4FB84A] border border-green-300 flex items-center'
- rel='noopener noreferrer'
- target='_blank'
- ref={buttonRef}
- >
- <Image
- src='/images/socials/WHATSAPP.svg'
- alt='Whatsapp'
- className='block sm:hidden'
- width={36}
- height={36}
- loading='eager'
- />
- <Image
- src='/images/socials/WHATSAPP.svg'
- alt='Whatsapp'
- className='hidden sm:block'
- width={44}
- height={44}
- loading='eager'
- />
- </a>
+ <Image
+ src='/images/socials/WHATSAPP.svg'
+ alt='Whatsapp'
+ className='block sm:hidden'
+ width={36}
+ height={36}
+ loading='eager'
+ />
+ <Image
+ src='/images/socials/WHATSAPP.svg'
+ alt='Whatsapp'
+ className='hidden sm:block'
+ width={44}
+ height={44}
+ loading='eager'
+ />
+ </a>
+ </div>
</div>
- </div>
+ )}
</AnimationLayout>
<BasicFooter />
</>
diff --git a/src/lib/product/components/Product/ProductDesktopVariant.jsx b/src/lib/product/components/Product/ProductDesktopVariant.jsx
index 44ae04bd..6b4ab1e1 100644
--- a/src/lib/product/components/Product/ProductDesktopVariant.jsx
+++ b/src/lib/product/components/Product/ProductDesktopVariant.jsx
@@ -1,3 +1,4 @@
+import { AlertTriangle } from 'lucide-react';
import { Box, Button, Skeleton, Tooltip } from '@chakra-ui/react';
import { HeartIcon } from '@heroicons/react/24/outline';
import { Info, MessageCircleIcon, Share2Icon } from 'lucide-react';
@@ -249,6 +250,7 @@ const ProductDesktopVariant = ({
});
router.push('/shop/quotation?source=buy');
};
+ const hasPrice = Number(product?.price?.price) > 0;
const variantSectionRef = useRef(null);
const goToVariantSection = () => {
@@ -314,6 +316,17 @@ const ProductDesktopVariant = ({
return (
<DesktopView>
+ <div className='relative'>
+ {!hasPrice && (
+ <div className='absolute inset-0 z-[20] flex items-center justify-center pointer-events-none select-none'>
+ <img
+ src='/images/produk_tidak_tersedia.svg'
+ alt='Produk tidak tersedia'
+ className='w-[35%] opacity-50 -translate-x-[3%] -translate-y-[-70%]'
+ />
+ </div>
+ )}
+ </div>
<div className='container mx-auto pt-10'>
<div className='flex'>
<div className='w-full flex flex-wrap'>
@@ -326,9 +339,21 @@ const ProductDesktopVariant = ({
</div>
<div className='w-7/12 px-6'>
+ {!hasPrice && (
+ <div className='bg-red-50 p-2 py-1.5 rounded-lg border border-red-500 flex gap-1 items-center '>
+ <AlertTriangle
+ size={18}
+ className='text-red-600 shrink-0 mx-2'
+ />
+ <h1 className='text-red-600 font-normal text-h-sm'>
+ Maaf untuk saat ini Produk yang anda cari tidak tersedia
+ </h1>
+ </div>
+ )}
<h1 className='text-title-md leading-10 font-medium'>
{product?.name}
</h1>
+
<div className='mt-10'>
<div className='flex p-3 bg-gray_r-4'>
<div className='w-4/12 text-gray_r-12/70'>Item Code</div>
@@ -430,73 +455,55 @@ const ProductDesktopVariant = ({
</div>
</div>
<div className='w-[33%]'>
- {product?.isFlashsale > 0 &&
- product?.price?.discountPercentage > 0 ? (
+ {product?.price?.price > 0 && (
<>
- <div className='flex gap-x-1 items-center mt-2'>
- <div className='badge-solid-red text-caption-1'>
- {product?.price?.discountPercentage}%
- </div>
- <div className='text-gray_r-9 line-through text-caption-1'>
- {currencyFormat(product?.price?.price)}
- </div>
- <div className='text-danger-500 font-semibold text-xl'>
- {currencyFormat(product?.price?.priceDiscount)}
- </div>
- </div>
- <div className='text-gray_r-9 text-base font-normal mt-1'>
- Termasuk PPN:{' '}
- {currencyFormat(
- product?.price?.priceDiscount * process.env.NEXT_PUBLIC_PPN
- )}
- </div>
- </>
- ) : (
- <h3 className='text-danger-500 font-semibold mt-1 text-title-md'>
- {product?.price?.price > 0 ? (
+ {product?.isFlashsale > 0 &&
+ product?.price?.discountPercentage > 0 ? (
<>
- {currencyFormat(product?.price?.price)}
+ <div className='flex gap-x-1 items-center mt-2'>
+ <div className='badge-solid-red text-caption-1'>
+ {product?.price?.discountPercentage}%
+ </div>
+ <div className='text-gray_r-9 line-through text-caption-1'>
+ {currencyFormat(product?.price?.price)}
+ </div>
+ <div className='text-danger-500 font-semibold text-xl'>
+ {currencyFormat(product?.price?.priceDiscount)}
+ </div>
+ </div>
<div className='text-gray_r-9 text-base font-normal mt-1'>
Termasuk PPN:{' '}
{currencyFormat(
- product?.price?.price * process.env.NEXT_PUBLIC_PPN
+ product?.price?.priceDiscount *
+ process.env.NEXT_PUBLIC_PPN
)}
</div>
</>
) : (
- <span className='text-gray_r-12/90 font-normal text-h-sm'>
- Hubungi kami untuk dapatkan harga terbaik,&nbsp;
- <a
- href={whatsappUrl('product', {
- name: product.name,
- manufacture: product.manufacture?.name,
- url: createSlug(
- '/shop/product/',
- product.name,
- product.id,
- true
- ),
- })}
- className='text-danger-500 underline'
- rel='noopener noreferrer'
- target='_blank'
- >
- klik disini
- </a>
- </span>
+ <h3 className='text-danger-500 font-semibold mt-1 text-title-md'>
+ {currencyFormat(product?.price?.price)}
+ <div className='text-gray_r-9 text-base font-normal mt-1'>
+ Termasuk PPN:{' '}
+ {currencyFormat(
+ product?.price?.price * process.env.NEXT_PUBLIC_PPN
+ )}
+ </div>
+ </h3>
)}
- </h3>
+ </>
)}
+
<div className='flex gap-x-5 items-center py-5'>
<div className='relative flex items-center'>
<button
type='button'
- className='absolute left-0 px-2 py-1 h-full text-gray-500'
+ className='absolute left-0 px-2 py-1 h-full text-gray-500 disabled:opacity-40'
onClick={() => {
const n = parseInt(String(quantityInput), 10);
const next = Number.isFinite(n) ? Math.max(1, n - 1) : 1;
setQuantityInput(next);
}}
+ disabled={!hasPrice}
>
-
</button>
@@ -518,43 +525,40 @@ const ProductDesktopVariant = ({
if (['e', 'E', '+', '-', '.'].includes(e.key))
e.preventDefault();
}}
- className='w-24 h-10 text-center border border-gray-300 rounded focus:outline-none'
+ className='w-24 h-10 text-center border border-gray-300 rounded focus:outline-none disabled:bg-gray-100 disabled:text-gray-400'
+ disabled={!hasPrice}
/>
<button
type='button'
- className='absolute right-0 px-2 py-1 h-full text-gray-500'
+ className='absolute right-0 px-2 py-1 h-full text-gray-500 disabled:opacity-40'
onClick={() => {
const n = parseInt(String(quantityInput), 10);
const next = (Number.isFinite(n) ? n : 0) + 1;
setQuantityInput(next);
}}
+ disabled={!hasPrice}
>
+
</button>
</div>
<div>
- {/* <Skeleton
+ <Skeleton
isLoaded={!isLoadingSLA}
h='21px'
- // w={16}
className={
- product?.sla?.qty < 10 ? 'text-red-600 font-medium' : ''
+ !hasPrice || fakeStock < 10
+ ? 'text-red-600 font-medium'
+ : ''
}
>
- Stock : {product?.sla?.qty}{' '}
- </Skeleton> */}
- <Skeleton
- isLoaded={!isLoadingSLA}
- h='21px'
- className={fakeStock < 10 ? 'text-red-600 font-medium' : ''}
- >
- Stock : {fakeStock}{' '}
+ Stock : {hasPrice ? fakeStock : 'Habis'}
</Skeleton>
</div>
+
<div>
- {qtyPickUp > 0 && (
+ {qtyPickUp > 0 && hasPrice && (
<Link href='/panduan-pick-up-service' className='group'>
<Image
src='/images/PICKUP-NOW.png'
@@ -565,20 +569,23 @@ const ProductDesktopVariant = ({
)}
</div>
</div>
- {qtyPickUp > 0 && (
+ {/* {qtyPickUp > 0 && (
<>
<div className='text-[12px] mt-1 text-red-500 italic'>
* {qtyPickUp} barang bisa di pickup
</div>
<div className='h-4' />
</>
- )}
+ )} */}
<div className='flex gap-x-3'>
<Button
onClick={() => handleAddToCart(product.id)}
className='w-full'
colorScheme='red'
variant={'outline'}
+ isDisabled={
+ !product?.price?.price || product?.price?.price <= 0
+ }
>
Keranjang
</Button>
@@ -586,6 +593,9 @@ const ProductDesktopVariant = ({
onClick={() => handleBuy(product.id)}
className='w-full'
colorScheme='red'
+ isDisabled={
+ !product?.price?.price || product?.price?.price <= 0
+ }
>
Beli
</Button>
@@ -595,6 +605,7 @@ const ProductDesktopVariant = ({
color={'red'}
colorScheme='white'
className='w-full border-2 p-2 gap-1 mt-2 hover:bg-slate-100 flex items-center'
+ isDisabled={!product?.price?.price || product?.price?.price <= 0}
>
<ImageNext
src='/images/doc_red.svg'
@@ -692,6 +703,20 @@ const ProductDesktopVariant = ({
alt={product.name}
className='h-32 object-contain object-center w-full border border-gray_r-4'
/>
+ {/* Watermark jika produk nonaktif */}
+ {/* {(!product?.price?.price || product?.price?.price <= 0) && (
+ <div className='absolute inset-0 flex items-center justify-center'>
+ <span
+ className='text-[60px] font-extrabold text-green-700/20 rotate-[-25deg] select-none'
+ style={{
+ textShadow: '0 0 3px rgba(0,0,0,0.1)',
+ letterSpacing: '6px',
+ }}
+ >
+ NONAKTIF
+ </span>
+ </div>
+ )} */}
</div>
<div className='ml-3 flex flex-1 items-center font-normal'>
{product.name}
diff --git a/src/lib/product/components/Product/ProductMobileVariant.jsx b/src/lib/product/components/Product/ProductMobileVariant.jsx
index 4cfc63ca..0f4953df 100644
--- a/src/lib/product/components/Product/ProductMobileVariant.jsx
+++ b/src/lib/product/components/Product/ProductMobileVariant.jsx
@@ -183,10 +183,20 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
return (
<MobileView>
- {/* PRICE & ACTIONS: tetap punyamu, hanya hapus input number lama */}
- {/* ===== BAR BAWAH (fixed) ===== */}
+ <div className='relative'>
+ {!product.price.price > 0 && (
+ <div className='absolute inset-0 z-[50] flex items-center justify-center pointer-events-none select-none'>
+ <img
+ src='/images/produk_tidak_tersedia.svg'
+ alt='Produk tidak tersedia'
+ className='w-[100%] opacity-[1000%] -translate-x-[0%] -translate-y-[-160%]'
+ />
+ </div>
+ )}
+ </div>
+ {/* ===== BAR BAWAH ===== */}
<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]'>
- {/* HARGA & PPN (logikamu tetap) */}
+ {/* HARGA & PPN */}
{activeVariant.isFlashSale &&
activeVariant?.price?.discountPercentage > 0 ? (
<>
@@ -220,80 +230,65 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
)}
</div>
</>
- ) : (
- <span className='text-gray_r-11 leading-6 font-normal'>
- Hubungi kami untuk dapatkan harga terbaik,&nbsp;
- <a
- href={whatsappUrl('product', {
- name: product.name,
- url: createSlug(
- '/shop/product/',
- product.name,
- product.id,
- true
- ),
- })}
- className='text-danger-500 underline'
- >
- klik disini
- </a>
- </span>
- )}
+ ) : null}
</div>
)}
- {/* ⬇️ TAMBAHKAN BLOK INI DI DALAM BAR: STOK & STEPPER */}
<div className='grid grid-cols-12 items-center gap-3 mt-3'>
- <div className='col-span-7'>
- <div
- className={`text-[14px] ${
- product?.sla?.qty < 10 ? 'text-red-600 font-medium' : ''
- }`}
- >
- {/* Stock : {activeVariant?.stock ?? 0} */}
- Stock : {fakeStock}{' '}
- </div>
- {qtyPickUp > 0 && (
- <div className='text-[16px] mt-0.5 text-red-500 italic'>
- * {qtyPickUp} barang bisa di pickup
+ {product?.price?.price > 0 && (
+ <div className='col-span-7'>
+ <div
+ className={`text-[14px] ${
+ product?.sla?.qty < 10 ? 'text-red-600 font-medium' : ''
+ }`}
+ >
+ {/* Stock : {activeVariant?.stock ?? 0} */}
+ Stock : {fakeStock}{' '}
</div>
- )}
- </div>
+ {qtyPickUp > 0 && (
+ <div className='text-[16px] mt-0.5 text-red-500 italic'>
+ * {qtyPickUp} barang bisa di pickup
+ </div>
+ )}
+ </div>
+ )}
<div className='col-span-5 flex justify-end'>
- <div className='inline-flex items-stretch border rounded-xl overflow-hidden'>
- <button
- type='button'
- className='h-10 w-10 grid place-items-center text-gray-700 hover:bg-gray-100 active:scale-95'
- onClick={() =>
- setQuantityInput(
- String(Math.max(1, Number(quantityInput || 1) - 1))
- )
- }
- aria-label='Kurangi'
- >
- <span className='text-2xl leading-none'>–</span>
- </button>
- <input
- type='number'
- min={1}
- value={quantityInput}
- onChange={(e) => setQuantityInput(e.target.value)}
- className='h-10 w-16 text-center text-lg outline-none border-x
+ {product?.price?.price > 0 && (
+ <div className='inline-flex items-stretch border rounded-xl overflow-hidden'>
+ <button
+ type='button'
+ className='h-10 w-10 grid place-items-center text-gray-700 hover:bg-gray-100 active:scale-95'
+ onClick={() =>
+ setQuantityInput(
+ String(Math.max(1, Number(quantityInput || 1) - 1))
+ )
+ }
+ aria-label='Kurangi'
+ >
+ <span className='text-2xl leading-none'>–</span>
+ </button>
+ <input
+ type='number'
+ min={1}
+ value={quantityInput}
+ onChange={(e) => setQuantityInput(e.target.value)}
+ className='h-10 w-16 text-center text-lg outline-none border-x
[appearance:textfield]
[&::-webkit-outer-spin-button]:appearance-none
[&::-webkit-inner-spin-button]:appearance-none'
- />
- <button
- type='button'
- className='h-10 w-10 grid place-items-center text-gray-700 hover:bg-gray-100 active:scale-95'
- onClick={() =>
- setQuantityInput(String(Number(quantityInput || 1) + 1))
- }
- aria-label='Tambah'
- >
- <span className='text-2xl leading-none'>+</span>
- </button>
- </div>
+ />
+ <button
+ type='button'
+ className='h-10 w-10 grid place-items-center text-gray-700 hover:bg-gray-100 active:scale-95'
+ onClick={() =>
+ setQuantityInput(String(Number(quantityInput || 1) + 1))
+ }
+ aria-label='Tambah'
+ >
+ <span className='text-2xl leading-none'>+</span>
+ </button>
+ </div>
+ )}
</div>
</div>
@@ -306,6 +301,7 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
className='flex items-center justify-center p-2 border-2 hover:bg-slate-100'
variant='outline'
title='Lihat Dokumen'
+ isDisabled={product?.price?.price <= 0}
>
<ImageNext
src='/images/doc.svg'
@@ -323,7 +319,7 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
className='flex-1'
colorScheme='red'
variant='outline'
- isDisabled={product.stock === 0}
+ isDisabled={product?.price?.price <= 0}
>
Keranjang
</Button>
@@ -333,7 +329,7 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
}
className='flex-1'
colorScheme='red'
- isDisabled={product.stock === 0}
+ isDisabled={product?.price?.price <= 0}
>
Beli
</Button>
@@ -396,87 +392,92 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
active={informationTab == 'specification'}
className='rounded border border-gray_r-6 divide-y divide-gray_r-6'
>
- <SpecificationContent label='Ketersediaan'>
- <span>
- {isLoadingSLA ? (
- <Skeleton width='100px' height='full' />
- ) : product?.sla?.slaDate != '-' ? (
- <button
- type='button'
- title={`Masa Persiapan Barang ${product?.sla?.slaDate}`}
- className={`flex gap-x-1 items-center p-2 h-8 rounded-lg w-full ${
- product?.sla?.slaDate === 'indent'
- ? 'bg-indigo-900'
- : 'btn-light'
- }`}
- >
- <div
- className={`flex-1 text-sm ${
- product?.sla?.slaDate === 'indent' ? 'text-white' : ''
+ {(!product?.price?.price || product?.price?.price <= 0) && (
+ <SpecificationContent label='Ketersediaan'>
+ <span>
+ {isLoadingSLA ? (
+ <Skeleton width='100px' height='full' />
+ ) : product?.sla?.slaDate != '-' ? (
+ <button
+ type='button'
+ title={`Masa Persiapan Barang ${product?.sla?.slaDate}`}
+ className={`flex gap-x-1 items-center p-2 h-8 rounded-lg w-full ${
+ product?.sla?.slaDate === 'indent'
+ ? 'bg-indigo-900'
+ : 'btn-light'
}`}
>
- {product?.sla?.slaDate}
- </div>
- <div className='flex-end'>
- <svg
- aria-hidden='true'
- fill='none'
- stroke='currentColor'
- stroke-width='1.5'
- className={`w-7 h-7 text-sm ${
+ <div
+ className={`flex-1 text-sm ${
product?.sla?.slaDate === 'indent' ? 'text-white' : ''
}`}
>
- <path
- d='M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z'
- stroke-linecap='round'
- stroke-linejoin='round'
- ></path>
- </svg>
- </div>
- </button>
- ) : (
- '-'
- )}
- </span>
- </SpecificationContent>
+ {product?.sla?.slaDate}
+ </div>
+ <div className='flex-end'>
+ <svg
+ aria-hidden='true'
+ fill='none'
+ stroke='currentColor'
+ stroke-width='1.5'
+ className={`w-7 h-7 text-sm ${
+ product?.sla?.slaDate === 'indent' ? 'text-white' : ''
+ }`}
+ >
+ <path
+ d='M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z'
+ stroke-linecap='round'
+ stroke-linejoin='round'
+ ></path>
+ </svg>
+ </div>
+ </button>
+ ) : (
+ '-'
+ )}
+ </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>
+ {product?.price?.price > 0 && (
+ <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 && (
+ {activeVariant?.weight > -1 && (
<span>{activeVariant?.weight} KG</span>
)}
- {activeVariant?.weight == 0 && (
+ {activeVariant?.weight == -1 && (
<a
href={whatsappUrl('productWeight', {
name: product.name,
@@ -487,7 +488,11 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
true
),
})}
- className='text-danger-500 font-medium'
+ className={`text-danger-501 font-medium ${
+ !product?.price?.price || product?.price?.price <= 0
+ ? 'pointer-events-none opacity-50 cursor-default'
+ : ''
+ }`}
>
Tanya Berat
</a>
diff --git a/src/pages/api/shop/product-detail.js b/src/pages/api/shop/product-detail.js
index faa96028..504f9dd6 100644
--- a/src/pages/api/shop/product-detail.js
+++ b/src/pages/api/shop/product-detail.js
@@ -1,26 +1,32 @@
-import { productMappingSolr, variantsMappingSolr } from '@/utils/solrMapping'
-import axios from 'axios'
+import { productMappingSolr, variantsMappingSolr } from '@/utils/solrMapping';
+import axios from 'axios';
export default async function handler(req, res) {
try {
let productTemplate = await axios(
- process.env.SOLR_HOST + `/solr/product/select?q=id:${req.query.id}&q.op=OR&indent=true`
- )
+ process.env.SOLR_HOST +
+ `/solr/product/select?q=id:${req.query.id}&q.op=OR&indent=true`
+ );
let productVariants = await axios(
process.env.SOLR_HOST +
- `/solr/variants/select?q=template_id_i:${req.query.id}&q.op=OR&indent=true&rows=100&fq=-publish_b:false AND price_tier1_v2_f:[1 TO *]`
- )
- let auth = req.query.auth === 'false' ? JSON.parse(req.query.auth) : req.query.auth
- let result = productMappingSolr(productTemplate.data.response.docs, auth || false)
+ `/solr/variants/select?q=template_id_i:${req.query.id}&q.op=OR&indent=true&rows=100`
+ // `/solr/variants/select?q=template_id_i:${req.query.id}&q.op=OR&indent=true&rows=100&fq=-publish_b:false AND price_tier1_v2_f:[1 TO *]`
+ );
+ let auth =
+ req.query.auth === 'false' ? JSON.parse(req.query.auth) : req.query.auth;
+ let result = productMappingSolr(
+ productTemplate.data.response.docs,
+ auth || false
+ );
result[0].variants = variantsMappingSolr(
productTemplate.data.response.docs[0],
productVariants.data.response.docs,
auth || false
- )
- res.status(200).json(result)
+ );
+ res.status(200).json(result);
} catch (error) {
- console.error('Error fetching data from Solr:', error)
- res.status(500).json({ error: 'Internal Server Error' })
+ console.error('Error fetching data from Solr:', error);
+ res.status(500).json({ error: 'Internal Server Error' });
}
}
diff --git a/src/pages/google_merchant/products/[page].js b/src/pages/google_merchant/products/[page].js
index 161b6aec..2a53f7c0 100644
--- a/src/pages/google_merchant/products/[page].js
+++ b/src/pages/google_merchant/products/[page].js
@@ -18,7 +18,9 @@ export async function getServerSideProps({ res, query }) {
page: page.replace('.xml', ''),
priceFrom: 1,
orderBy: 'popular',
- fq: ['image_s:["" TO *] AND publish_b:true'],
+ fq: ['image_s:["" TO *] AND publish_b:true AND price_tier1_v2_f:[1 TO *]'],
+ // product_rating_f: '[8 TO *]',
+ // price_tier1_v2_f: '[1 TO *]',
};
const products = await variantSearchApi({ query: _.toQuery(queries) });
diff --git a/src/pages/google_merchant/products/index.js b/src/pages/google_merchant/products/index.js
index b6c7bfef..d6ef413a 100644
--- a/src/pages/google_merchant/products/index.js
+++ b/src/pages/google_merchant/products/index.js
@@ -8,7 +8,9 @@ export async function getServerSideProps() {
const queries = {
limit: 1,
priceFrom: 1,
- fq: 'image_s:["" TO *] AND publish_b:true',
+ fq: 'image_s:["" TO *] AND publish_b:true AND price_tier1_v2_f:[1 TO *]',
+ // product_rating_f: '[8 TO *]',
+ // price_tier1_v2_f: '[1 TO *]',
};
const products = await variantSearchApi({ query: _.toQuery(queries) });
const { numFound } = products.response;
diff --git a/src/pages/shop/product/variant/[slug].jsx b/src/pages/shop/product/variant/[slug].jsx
index 2c0dd64b..32c00a35 100644
--- a/src/pages/shop/product/variant/[slug].jsx
+++ b/src/pages/shop/product/variant/[slug].jsx
@@ -25,23 +25,39 @@ export async function getServerSideProps(context) {
const tier = auth.pricelist ? auth.pricelist : false;
const authToken = auth?.token || '';
- let response = await axios(
- `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/variant-detail?id=` +
- getIdFromSlug(slug) +
- '&auth=' +
- tier
- );
- let product = response.data;
+ try {
+ const response = await axios(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/variant-detail?id=` +
+ getIdFromSlug(slug) +
+ '&auth=' +
+ tier
+ );
- if (product?.length == 1) {
- product = product[0];
- } else {
- product = null;
- }
+ let product = response.data;
- return {
- props: { product },
- };
+ if (product?.length == 1) {
+ product = product[0];
+ } else {
+ product = null;
+ }
+
+ return {
+ props: { product },
+ };
+ } catch (error) {
+ // kalau status 500 → tampilkan halaman 404
+ if (error.response && error.response.status === 500) {
+ return { notFound: true };
+ }
+
+ // kalau 404 dari API juga langsung 404
+ if (error.response && error.response.status === 404) {
+ return { notFound: true };
+ }
+
+ // kalau error lain, lempar agar bisa dilihat di console log server
+ throw error;
+ }
}
export default function ProductDetail({ product }) {
diff --git a/src/pages/sitemap/blogs.xml.js b/src/pages/sitemap/blogs.xml.js
index 628dc710..1125ad1c 100644
--- a/src/pages/sitemap/blogs.xml.js
+++ b/src/pages/sitemap/blogs.xml.js
@@ -10,12 +10,12 @@ export async function getServerSideProps({ res }) {
'http://www.sitemaps.org/schemas/sitemap/0.9'
);
- // const date = new Date()
- const date = '2025-10-30';
+ const date = new Date();
+ // const date = '2025-10-30';
blogs.blogs.forEach((blog) => {
const url = sitemap.ele('url');
url.ele('loc', createSlug(baseUrl, blog.title, blog.id));
- url.ele('lastmod', date);
+ url.ele('lastmod', date.toISOString().slice(0, 10));
url.ele('changefreq', 'weekly');
url.ele('priority', '0.6');
});
diff --git a/src/pages/sitemap/brands.xml.js b/src/pages/sitemap/brands.xml.js
index c2199d85..dc92419c 100644
--- a/src/pages/sitemap/brands.xml.js
+++ b/src/pages/sitemap/brands.xml.js
@@ -10,12 +10,12 @@ export async function getServerSideProps({ res }) {
'http://www.sitemaps.org/schemas/sitemap/0.9'
);
- // const date = new Date()
- const date = '2025-10-30';
+ const date = new Date();
+ // const date = '2025-10-30';
brands.manufactures.forEach((brand) => {
const url = sitemap.ele('url');
url.ele('loc', createSlug(baseUrl, brand.name, brand.id));
- url.ele('lastmod', date);
+ url.ele('lastmod', date.toISOString().slice(0, 10));
url.ele('changefreq', 'daily');
url.ele('priority', '1.0');
});
diff --git a/src/pages/sitemap/categories-brand.xml.js b/src/pages/sitemap/categories-brand.xml.js
index faf67b9f..8dfd3a32 100644
--- a/src/pages/sitemap/categories-brand.xml.js
+++ b/src/pages/sitemap/categories-brand.xml.js
@@ -16,12 +16,12 @@ export async function getServerSideProps({ res }) {
'http://www.sitemaps.org/schemas/sitemap/0.9'
);
- // const date = new Date()
- const date = '2025-10-30';
+ const date = new Date();
+ // const date = '2025-10-30';
pages.forEach((page) => {
const sitemap = sitemapIndex.ele('sitemap');
sitemap.ele('loc', `${baseUrl}/${page}.xml`);
- sitemap.ele('lastmod', date);
+ sitemap.ele('lastmod', date.toISOString().slice(0, 10));
});
res.setHeader('Content-Type', 'text/xml');
diff --git a/src/pages/sitemap/categories-brand/[page].js b/src/pages/sitemap/categories-brand/[page].js
index 4d28ab04..e7f264ee 100644
--- a/src/pages/sitemap/categories-brand/[page].js
+++ b/src/pages/sitemap/categories-brand/[page].js
@@ -22,13 +22,13 @@ export async function getServerSideProps({ query, res }) {
'http://www.sitemaps.org/schemas/sitemap/0.9'
);
- // const date = new Date()
- const date = '2025-10-30';
+ const date = new Date();
+ // const date = '2025-10-30';
categories.data.response.docs.forEach((product) => {
const url = sitemap.ele('url');
const loc = product.url_s;
url.ele('loc', loc);
- url.ele('lastmod', date);
+ url.ele('lastmod', date.toISOString().slice(0, 10));
url.ele('changefreq', 'daily');
url.ele('priority', '0.8');
});
diff --git a/src/pages/sitemap/categories.xml.js b/src/pages/sitemap/categories.xml.js
index 357a2072..18c8a8f3 100644
--- a/src/pages/sitemap/categories.xml.js
+++ b/src/pages/sitemap/categories.xml.js
@@ -28,11 +28,11 @@ export async function getServerSideProps({ res }) {
function addUrlToSitemap(sitemap, name, id) {
const baseUrl = process.env.SELF_HOST + '/shop/category/';
- // const date = new Date()
- const date = '2025-10-30';
+ const date = new Date();
+ // const date = '2025-10-30';
const url = sitemap.ele('url');
url.ele('loc', createSlug(baseUrl, name, id));
- url.ele('lastmod', date);
+ url.ele('lastmod', date.toISOString().slice(0, 10));
url.ele('changefreq', 'weekly');
url.ele('priority', '0.6');
}
diff --git a/src/pages/sitemap/products.xml.js b/src/pages/sitemap/products.xml.js
index 0269ec59..5ed6b759 100644
--- a/src/pages/sitemap/products.xml.js
+++ b/src/pages/sitemap/products.xml.js
@@ -14,12 +14,12 @@ export async function getServerSideProps({ res }) {
'http://www.sitemaps.org/schemas/sitemap/0.9'
);
- // const date = new Date()
- const date = '2025-10-30';
+ const date = new Date();
+ // const date = '2025-10-30';
pages.forEach((page) => {
const sitemap = sitemapIndex.ele('sitemap');
sitemap.ele('loc', `${baseUrl}/${page}.xml`);
- sitemap.ele('lastmod', date);
+ sitemap.ele('lastmod', date.toISOString().slice(0, 10));
});
res.setHeader('Content-Type', 'text/xml');
diff --git a/src/pages/sitemap/products/[page].js b/src/pages/sitemap/products/[page].js
index 421c08e3..3603d64c 100644
--- a/src/pages/sitemap/products/[page].js
+++ b/src/pages/sitemap/products/[page].js
@@ -10,9 +10,9 @@ export async function getServerSideProps({ query, res }) {
const queries = {
limit,
page: page.replace('.xml', ''),
- '-publish_b': false,
- product_rating_f: '[8 TO *]',
- price_tier1_v2_f: '[1 TO *]',
+ // '-publish_b': false,
+ // product_rating_f: '[8 TO *]',
+ // price_tier1_v2_f: '[1 TO *]',
};
const products = await productSearchApi({ query: _.toQuery(queries) });
const sitemap = create('urlset', { encoding: 'utf-8' }).att(
@@ -20,12 +20,12 @@ export async function getServerSideProps({ query, res }) {
'http://www.sitemaps.org/schemas/sitemap/0.9'
);
- // const date = new Date()
- const date = '2025-10-30';
+ const date = new Date();
+ // const date = '2025-10-30';
products.response.products.forEach((product) => {
const url = sitemap.ele('url');
url.ele('loc', createSlug(baseUrl, product.name, product.id));
- url.ele('lastmod', date);
+ url.ele('lastmod', date.toISOString().slice(0, 10));
url.ele('changefreq', 'daily');
url.ele('priority', '0.8');
});