summaryrefslogtreecommitdiff
path: root/src-migrate
diff options
context:
space:
mode:
Diffstat (limited to 'src-migrate')
-rw-r--r--src-migrate/modules/cart/components/CartSummaryMobile.tsx36
-rw-r--r--src-migrate/modules/product-detail/components/AddToCart.tsx315
-rw-r--r--src-migrate/modules/product-detail/components/AddToQuotation.tsx11
-rw-r--r--src-migrate/modules/product-detail/components/Breadcrumb.tsx140
-rw-r--r--src-migrate/modules/product-detail/components/PriceAction.tsx338
-rw-r--r--src-migrate/modules/product-detail/components/ProductDetail.tsx234
-rw-r--r--src-migrate/pages/shop/cart/cart.module.css14
-rw-r--r--src-migrate/pages/shop/cart/index.tsx15
-rw-r--r--src-migrate/pages/shop/product/[slug].tsx62
9 files changed, 856 insertions, 309 deletions
diff --git a/src-migrate/modules/cart/components/CartSummaryMobile.tsx b/src-migrate/modules/cart/components/CartSummaryMobile.tsx
index 02258204..7a334fed 100644
--- a/src-migrate/modules/cart/components/CartSummaryMobile.tsx
+++ b/src-migrate/modules/cart/components/CartSummaryMobile.tsx
@@ -29,7 +29,7 @@ const CartSummaryMobile = ({
isLoaded = false,
}: Props) => {
const [showPopup, setShowPopup] = useState(false);
- const PPN : number = process.env.NEXT_PUBLIC_PPN ? parseFloat(process.env.NEXT_PUBLIC_PPN) : 0;
+ const PPN: number = process.env.NEXT_PUBLIC_PPN ? parseFloat(process.env.NEXT_PUBLIC_PPN) : 0;
return (
<>
<BottomPopup
@@ -88,22 +88,30 @@ const CartSummaryMobile = ({
</div>
</BottomPopup>
<div className='flex flex-col gap-y-3'>
- <Skeleton isLoaded={isLoaded} className={style.line}>
- <span className={clsxm(style.label, style.grandTotal)}>
+ <Skeleton
+ isLoaded={isLoaded}
+ className={clsxm(style.line, 'flex items-center justify-between !py-2')}
+ >
+ {/* Left: label */}
+ <span className={clsxm(style.label, style.grandTotal, 'leading-tight')}>
Grand Total
</span>
- <button
- onClick={() => setShowPopup(true)}
- className='bg-gray-300 w-6 h-6 items-center justify-center cursor-pointer hover:bg-red-400 md:hidden '
- >
- <ChevronDownIcon className='h-6 w-6 text-white' />
- </button>
- </Skeleton>
- <Skeleton isLoaded={isLoaded} className={style.line}>
- <span className={style.value}>
- Rp {formatCurrency(grandTotal || 0)}
- </span>
+
+ {/* Right: amount + chevron */}
+ <div className="flex items-center gap-2">
+ <span className={clsxm(style.value, 'whitespace-nowrap tabular-nums leading-tight')}>
+ Rp {formatCurrency(grandTotal || 0)}
+ </span>
+ <button
+ onClick={() => setShowPopup(true)}
+ aria-label="Expand ringkasan"
+ className="md:hidden flex w-5 h-5 items-center justify-center rounded bg-gray-300 hover:bg-gray-400"
+ >
+ <ChevronDownIcon className="h-4 w-4 text-white" />
+ </button>
+ </div>
</Skeleton>
+
</div>
</>
);
diff --git a/src-migrate/modules/product-detail/components/AddToCart.tsx b/src-migrate/modules/product-detail/components/AddToCart.tsx
index 95bc1d88..147fd6d2 100644
--- a/src-migrate/modules/product-detail/components/AddToCart.tsx
+++ b/src-migrate/modules/product-detail/components/AddToCart.tsx
@@ -1,6 +1,6 @@
import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
import style from '../styles/price-action.module.css';
-import { Button, Link, useToast } from '@chakra-ui/react';
+import { Button, color, Link, useToast } from '@chakra-ui/react';
import product from 'next-seo/lib/jsonld/product';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
@@ -17,6 +17,10 @@ import formatCurrency from '~/libs/formatCurrency';
import { useProductDetail } from '../stores/useProductDetail';
import { gtagAddToCart } from '@/core/utils/googleTag';
import axios from 'axios';
+import useDevice from '@/core/hooks/useDevice';
+import MobileView from '@/core/components/views/MobileView';
+import DesktopView from '@/core/components/views/DesktopView';
+import ProductPromoSection from '~/modules/product-promo/components/Section';
type Props = {
variantId: number | null;
quantity?: number;
@@ -39,6 +43,8 @@ const AddToCart = ({
isClosable: true,
});
+ const { isMobile, isDesktop } = useDevice();
+
const { askAdminUrl } = useProductDetail();
const [product, setProducts] = useState(products);
@@ -158,111 +164,266 @@ const AddToCart = ({
const btnConfig = {
add_to_cart: {
- colorScheme: 'yellow',
+ colorScheme: 'red',
+ variant: 'outline',
text: 'Keranjang',
},
buy: {
colorScheme: 'red',
- text: 'Beli',
+ variant: 'solid',
+ text: isDesktop ? 'Beli' : 'Beli Sekarang',
},
};
return (
<div className='w-full'>
- <Button
- onClick={handleButton}
- colorScheme={btnConfig[source].colorScheme}
- className='w-full'
- >
- {btnConfig[source].text}
- </Button>
+ <MobileView>
+ <Button
+ onClick={handleButton}
+ colorScheme={btnConfig[source].colorScheme}
+ variant={btnConfig[source].variant}
+ className='w-full'
+ >
+ {btnConfig[source].text}
+ </Button>
+ </MobileView>
+
+ <DesktopView>
+ <Button
+ onClick={handleButton}
+ colorScheme={btnConfig[source].colorScheme}
+ variant={btnConfig[source].variant}
+ className='w-full'
+ >
+ {btnConfig[source].text}
+ </Button>
+ </DesktopView>
+
<BottomPopup
className='!container'
title='Berhasil Ditambahkan'
active={addCartAlert}
- close={() => {
- setAddCartAlert(false);
- }}
+ close={() => setAddCartAlert(false)}
>
- <div className='flex mt-4'>
- <div className='w-[10%]'>
- <ImageNext
- src={product.image}
- alt={product.name}
- className='h-32 object-contain object-center w-full border border-gray_r-4'
- width={80}
- height={80}
- />
- </div>
- <div className='ml-3 flex flex-1 items-start font-medium justify-center flex-col gap-y-1'>
- {!!product.manufacture.name ? (
- <Link
- href={createSlug(
- '/shop/brands/',
- product.manufacture.name,
- product.manufacture.id.toString()
+ {/* ===== 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' }}>
+ {/* HEADER ITEM */}
+ <div className='flex mt-4'>
+ <div className='w-[25%]'>
+ <ImageNext
+ src={product.image}
+ alt={product.name}
+ className='h-32 object-contain object-center w-full border border-gray_r-4'
+ width={80}
+ height={80}
+ />
+ </div>
+
+ <div className='ml-3 flex flex-1 items-start font-medium justify-center flex-col gap-y-1'>
+ {!!product.manufacture.name ? (
+ <Link
+ href={createSlug(
+ '/shop/brands/',
+ product.manufacture.name,
+ product.manufacture.id.toString()
+ )}
+ className=' hover:underline'
+ color={'red'}
+ >
+ {product.manufacture.name}
+ </Link>
+ ) : (
+ '-'
)}
- className=' hover:underline'
- color={'red'}
- >
- {product.manufacture.name}
- </Link>
- ) : (
- '-'
- )}
- <p className='text-ellipsis overflow-hidden'>{product.name}</p>
- <p>{product.code}</p>
- {!!product.lowest_price && product.lowest_price.price > 0 && (
- <>
- <div className='flex items-end gap-x-2'>
- {product.lowest_price.discount_percentage > 0 && (
- <>
- <div className='badge-solid-red'>
- {Math.floor(product.lowest_price.discount_percentage)}%
- </div>
- <div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'>
- Rp {formatCurrency(product.lowest_price.price || 0)}
+ <p className='text-ellipsis overflow-hidden'>{product.name}</p>
+ <p>{product.code}</p>
+
+ {!!product.lowest_price && product.lowest_price.price > 0 && (
+ <>
+ <div className='flex items-end gap-x-2'>
+ {product.lowest_price.discount_percentage > 0 && (
+ <>
+ <div className='badge-solid-red'>
+ {Math.floor(
+ product.lowest_price.discount_percentage
+ )}
+ %
+ </div>
+ <div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'>
+ Rp {formatCurrency(product.lowest_price.price || 0)}
+ </div>
+ </>
+ )}
+ <div className='text-danger-500 font-semibold'>
+ Rp{' '}
+ {formatCurrency(
+ product.lowest_price.price_discount || 0
+ )}
</div>
- </>
- )}
- <div className='text-danger-500 font-semibold'>
- Rp{' '}
- {formatCurrency(product.lowest_price.price_discount || 0)}
- </div>
- </div>
- </>
- )}
+ </div>
+ </>
+ )}
- {!!product.lowest_price && product.lowest_price.price === 0 && (
- <span>
- Hubungi kami untuk dapatkan harga terbaik,{' '}
+ {!!product.lowest_price && 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>
+ )}
+ </div>
+
+ {/* sembunyikan link header di mobile agar tidak dobel */}
+ <div className='ml-3 items-center font-normal hidden md:flex'>
<Link
- href={askAdminUrl}
- target='_blank'
- className='font-medium underline'
- color={'red'}
+ href='/shop/cart'
+ className='flex-1 py-2 text-gray_r-12 btn-yellow'
>
- klik disini
+ Lihat Keranjang
</Link>
- </span>
- )}
+ </div>
+ </div>
+
+ {/* PROMO KHUSUS MOBILE */}
+ <div className='mt-6'>
+ <ProductPromoSection
+ product={product}
+ productId={Number(activeVariant?.id) || Number(variantId) || 0}
+ />
+ </div>
+
+ {/* PRODUCT SIMILAR */}
+ <div className='mt-8 mb-4'>
+ <div className='text-h-sm font-semibold mb-6'>
+ Kamu Mungkin Juga Suka
+ </div>
+ <DesktopView>
+ <LazyLoad>
+ <ProductSimilar query={productSimilarQuery} />
+ </LazyLoad>
+ </DesktopView>
+ <MobileView>
+ <ProductSimilar query={productSimilarQuery} />
+ </MobileView>
+ </div>
</div>
- <div className='ml-3 flex items-center font-normal'>
+
+ {/* footer tombol: selalu terlihat di bawah popup mobile */}
+ <div className='border-t border-gray-200 bg-white px-4 pt-3 pb-[calc(env(safe-area-inset-bottom)+12px)]'>
<Link
href='/shop/cart'
- className='flex-1 py-2 text-gray_r-12 btn-yellow'
+ className='w-full mb-2 block text-center btn-yellow py-3 rounded-xl font-semibold'
>
Lihat Keranjang
</Link>
</div>
</div>
- <div className='mt-8 mb-4'>
- <div className='text-h-sm font-semibold mb-6'>
- Kamu Mungkin Juga Suka
+
+ {/* ===== DESKTOP LAYOUT: tetap seperti semula ===== */}
+ <div className='hidden md:block'>
+ {/* HEADER ITEM */}
+ <div className='flex mt-4'>
+ <div className='w-[10%]'>
+ <ImageNext
+ src={product.image}
+ alt={product.name}
+ className='h-32 object-contain object-center w-full border border-gray_r-4'
+ width={80}
+ height={80}
+ />
+ </div>
+
+ <div className='ml-3 flex flex-1 items-start font-medium justify-center flex-col gap-y-1'>
+ {!!product.manufacture.name ? (
+ <Link
+ href={createSlug(
+ '/shop/brands/',
+ product.manufacture.name,
+ product.manufacture.id.toString()
+ )}
+ className=' hover:underline'
+ color={'red'}
+ >
+ {product.manufacture.name}
+ </Link>
+ ) : (
+ '-'
+ )}
+ <p className='text-ellipsis overflow-hidden'>{product.name}</p>
+ <p>{product.code}</p>
+
+ {!!product.lowest_price && product.lowest_price.price > 0 && (
+ <>
+ <div className='flex items-end gap-x-2'>
+ {product.lowest_price.discount_percentage > 0 && (
+ <>
+ <div className='badge-solid-red'>
+ {Math.floor(product.lowest_price.discount_percentage)}
+ %
+ </div>
+ <div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'>
+ Rp {formatCurrency(product.lowest_price.price || 0)}
+ </div>
+ </>
+ )}
+ <div className='text-danger-500 font-semibold'>
+ Rp{' '}
+ {formatCurrency(product.lowest_price.price_discount || 0)}
+ </div>
+ </div>
+ </>
+ )}
+
+ {!!product.lowest_price && 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>
+ )}
+ </div>
+
+ <div className='ml-3 flex items-center font-normal'>
+ <Link
+ href='/shop/cart'
+ className='flex-1 py-2 text-gray_r-12 btn-yellow'
+ >
+ Lihat Keranjang
+ </Link>
+ </div>
+ </div>
+
+ {/* PROMO (desktop biarkan sama posisinya) */}
+ <div className='mt-6'>
+ <ProductPromoSection
+ product={product}
+ productId={Number(activeVariant?.id) || Number(variantId) || 0}
+ />
+ </div>
+
+ {/* PRODUCT SIMILAR */}
+ <div className='mt-8 mb-4'>
+ <div className='text-h-sm font-semibold mb-6'>
+ Kamu Mungkin Juga Suka
+ </div>
+ <LazyLoad>
+ <ProductSimilar query={productSimilarQuery} />
+ </LazyLoad>
</div>
- <LazyLoad>
- <ProductSimilar query={productSimilarQuery} />
- </LazyLoad>
</div>
</BottomPopup>
</div>
diff --git a/src-migrate/modules/product-detail/components/AddToQuotation.tsx b/src-migrate/modules/product-detail/components/AddToQuotation.tsx
index f9b6c2b3..ebfcef32 100644
--- a/src-migrate/modules/product-detail/components/AddToQuotation.tsx
+++ b/src-migrate/modules/product-detail/components/AddToQuotation.tsx
@@ -16,6 +16,7 @@ import { useProductCartContext } from '@/contexts/ProductCartContext';
import { createSlug } from '~/libs/slug';
import formatCurrency from '~/libs/formatCurrency';
import { useProductDetail } from '../stores/useProductDetail';
+import useDevice from '@/core/hooks/useDevice';
type Props = {
variantId: number | null;
@@ -40,6 +41,7 @@ const AddToQuotation = ({
});
const { askAdminUrl } = useProductDetail();
+ const { isMobile, isDesktop } = useDevice();
const [product, setProducts] = useState(products);
const [status, setStatus] = useState<Status>('idle');
@@ -104,12 +106,13 @@ const AddToQuotation = ({
const btnConfig = {
add_to_cart: {
- colorScheme: 'yellow',
-
+ colorScheme: 'red',
+ variant: 'outline',
text: 'Keranjang',
},
buy: {
colorScheme: 'red',
+ variant: 'solid',
text: 'Beli',
},
};
@@ -123,13 +126,13 @@ const AddToQuotation = ({
className='w-full border-2 p-2 gap-1 hover:bg-slate-100 flex items-center'
>
<ImageNext
- src='/images/writing.png'
+ src= {isDesktop ? '/images/doc_red.svg' : '/images/doc.svg'}
alt='penawaran instan'
className=''
width={25}
height={25}
/>
- Penawaran Harga Instan
+ {isDesktop ? 'Penawaran Harga Instan' : ''}
</Button>
<BottomPopup
className='!container'
diff --git a/src-migrate/modules/product-detail/components/Breadcrumb.tsx b/src-migrate/modules/product-detail/components/Breadcrumb.tsx
index f41859a9..67aa12ab 100644
--- a/src-migrate/modules/product-detail/components/Breadcrumb.tsx
+++ b/src-migrate/modules/product-detail/components/Breadcrumb.tsx
@@ -1,31 +1,133 @@
-import React, { Fragment } from 'react'
-import { useQuery } from 'react-query'
-import { getProductCategoryBreadcrumb } from '~/services/product'
-import Link from 'next/link'
-import { createSlug } from '~/libs/slug'
+import React, { Fragment } from 'react';
+import { useQuery } from 'react-query';
+import Link from 'next/link';
+import { getProductCategoryBreadcrumb } from '~/services/product';
+import { createSlug } from '~/libs/slug';
+import useDevice from '@/core/hooks/useDevice';
-type Props = {
- id: number,
- name: string
-}
+type Props = { id: number; name: string };
const Breadcrumb = ({ id, name }: Props) => {
- const query = useQuery({
- queryKey: ['product-category-breadcrumb'],
+ const { isDesktop, isMobile } = useDevice();
+
+ const { data } = useQuery({
+ queryKey: ['product-category-breadcrumb', id],
queryFn: () => getProductCategoryBreadcrumb(id),
- refetchOnWindowFocus: false
- })
+ refetchOnWindowFocus: false,
+ });
+
+ const breadcrumbs = data ?? [];
+ const total = breadcrumbs.length;
+ const lastCat = total ? breadcrumbs[total - 1] : null;
+ const prevCat = total > 1 ? breadcrumbs[total - 2] : null;
+ const hasHidden = total > 1;
+ const hiddenText = hasHidden
+ ? breadcrumbs
+ .slice(0, total - 1)
+ .map((c) => c.name)
+ .join(' / ')
+ : '';
+
+ if (isMobile) {
+ const crumbsMobile: React.ReactNode[] = [];
+
+ crumbsMobile.push(
+ <Link href='/' className='text-danger-500 shrink-0' key='home'>
+ Home
+ </Link>
+ );
+
+ if (hasHidden && prevCat) {
+ // Jadikan ".." sebuah tautan ke kategori sebelumnya
+ crumbsMobile.push(
+ <Link
+ key='hidden'
+ href={createSlug('/shop/category/', prevCat.name, String(prevCat.id))}
+ className='text-danger-500 shrink-0'
+ title={hiddenText}
+ aria-label={`Kembali ke ${prevCat.name}`}
+ >
+ ..
+ </Link>
+ );
+ }
+
+ // Kategori terakhir
+ if (lastCat) {
+ crumbsMobile.push(
+ <Link
+ key={`cat-${lastCat.id}`}
+ href={createSlug('/shop/category/', lastCat.name, String(lastCat.id))}
+ className='text-danger-500 shrink-0'
+ >
+ {lastCat.name}
+ </Link>
+ );
+ }
+
+ // Nama produk (dipotong kalau gk muat)
+ crumbsMobile.push(
+ <span className='truncate min-w-0 flex-1' title={name} key='product'>
+ {name}
+ </span>
+ );
+
+ return (
+ <div className='flex items-center whitespace-nowrap overflow-hidden text-caption-1 leading-7'>
+ {crumbsMobile.map((node, i) => (
+ <Fragment key={i}>
+ {node}
+ {i < crumbsMobile.length - 1 && (
+ <span className='mx-2 shrink-0'>/</span>
+ )}
+ </Fragment>
+ ))}
+ </div>
+ );
+ }
- const breadcrumbs = query.data || []
+ // ===== DESKTOP =====
+ if (isDesktop) {
+ return (
+ <div className='line-clamp-2 md:line-clamp-1 leading-7 text-caption-1'>
+ <Link href='/' className='text-danger-500'>
+ Home
+ </Link>
+ <span className='mx-2'>/</span>
+ {breadcrumbs.map((category, index) => (
+ <Fragment key={index}>
+ <Link
+ href={createSlug(
+ '/shop/category/',
+ category.name,
+ category.id.toString()
+ )}
+ className='text-danger-500'
+ >
+ {category.name}
+ </Link>
+ <span className='mx-2'>/</span>
+ </Fragment>
+ ))}
+ <span>{name}</span>
+ </div>
+ );
+ }
return (
<div className='line-clamp-2 md:line-clamp-1 leading-7 text-caption-1'>
- <Link href='/' className='text-danger-500'>Home</Link>
+ <Link href='/' className='text-danger-500'>
+ Home
+ </Link>
<span className='mx-2'>/</span>
{breadcrumbs.map((category, index) => (
<Fragment key={index}>
<Link
- href={createSlug('/shop/category/', category.name, category.id.toString())}
+ href={createSlug(
+ '/shop/category/',
+ category.name,
+ category.id.toString()
+ )}
className='text-danger-500'
>
{category.name}
@@ -35,7 +137,7 @@ const Breadcrumb = ({ id, name }: Props) => {
))}
<span>{name}</span>
</div>
- )
-}
+ );
+};
-export default Breadcrumb \ No newline at end of file
+export default Breadcrumb;
diff --git a/src-migrate/modules/product-detail/components/PriceAction.tsx b/src-migrate/modules/product-detail/components/PriceAction.tsx
index 850c2d9d..ffc9ba40 100644
--- a/src-migrate/modules/product-detail/components/PriceAction.tsx
+++ b/src-migrate/modules/product-detail/components/PriceAction.tsx
@@ -12,12 +12,16 @@ import { getAuth } from '~/libs/auth';
import useDevice from '@/core/hooks/useDevice';
import odooApi from '~/libs/odooApi';
import { Button, Skeleton } from '@chakra-ui/react';
+import DesktopView from '@/core/components/views/DesktopView';
+import MobileView from '@/core/components/views/MobileView';
type Props = {
product: IProductDetail;
};
-const PPN : number = process.env.NEXT_PUBLIC_PPN ? parseFloat(process.env.NEXT_PUBLIC_PPN) : 0;
+const PPN: number = process.env.NEXT_PUBLIC_PPN
+ ? parseFloat(process.env.NEXT_PUBLIC_PPN)
+ : 0;
const PriceAction = ({ product }: Props) => {
const {
activePrice,
@@ -84,26 +88,59 @@ const PriceAction = ({ product }: Props) => {
>
{!!activePrice && activePrice.price > 0 && (
<>
- <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']}>
+ <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['main-price']}>
+ Rp {formatCurrency(activePrice.price_discount || 0)}
+ </div>
+ </div>
+ <div className='h-1' />
+ <div className={style['secondary-text']}>
+ Termasuk PPN: Rp{' '}
+ {formatCurrency(Math.round(activePrice.price_discount * PPN))}
+ </div>
+ </DesktopView>
+ <MobileView>
+ <div className='flex items-end gap-x-2'>
+ {activePrice.discount_percentage > 0 ? (
+ <>
+ <div className={style['disc-badge']}>
+ {Math.floor(activePrice.discount_percentage)}%
+ </div>
+
+ {/* harga setelah diskon (main-price) di kiri */}
+ <div className={style['main-price']}>
+ Rp {formatCurrency(activePrice.price_discount || 0)}
+ </div>
+
+ {/* harga coret di kanan */}
+ <div className={style['disc-price']}>
+ Rp {formatCurrency(activePrice.price || 0)}
+ </div>
+ </>
+ ) : (
+ // kalau tidak ada diskon, tampilkan harga normal saja
+ <div className={style['main-price']}>
Rp {formatCurrency(activePrice.price || 0)}
</div>
- </>
- )}
- <div className={style['main-price']}>
- Rp {formatCurrency(activePrice.price_discount || 0)}
+ )}
</div>
- </div>
- <div className='h-1' />
- <div className={style['secondary-text']}>
- Termasuk PPN: Rp{' '}
- {formatCurrency(Math.round(activePrice.price_discount * PPN))}
- </div>
+
+ <div className='text-md text-gray-500 shadow-0'>
+ Termasuk PPN: Rp{' '}
+ {formatCurrency(Math.round(activePrice.price_discount * PPN))}
+ </div>
+ </MobileView>
</>
)}
@@ -120,92 +157,209 @@ const PriceAction = ({ product }: Props) => {
</span>
)}
- <div className='h-4' />
- <div className='flex gap-x-5 items-center'>
- <div className='relative flex items-center'>
- <button
- type='button'
- className='absolute left-0 px-2 py-1 h-full text-gray-500'
- onClick={() =>
- setQuantityInput(String(Math.max(1, Number(quantityInput) - 1)))
- }
- >
- -
- </button>
- <input
- type='number'
- id='quantity'
- min={1}
- value={quantityInput}
- onChange={(e) => setQuantityInput(e.target.value)}
- className={style['quantity-input']}
- />
- <button
- type='button'
- className='absolute right-0 px-2 py-1 h-full text-gray-500'
- onClick={() => setQuantityInput(String(Number(quantityInput) + 1))}
- >
- +
- </button>
- </div>
+ <DesktopView>
+ <div className='h-4' />
+ <div className='flex gap-x-5 items-center'>
+ {/* Qty */}
+ <div className='relative flex items-center'>
+ <button
+ type='button'
+ className='absolute left-0 px-2 py-1 h-full text-gray-500'
+ onClick={() =>
+ setQuantityInput(String(Math.max(1, Number(quantityInput) - 1)))
+ }
+ >
+ -
+ </button>
+ <input
+ type='number'
+ id='quantity'
+ min={1}
+ value={quantityInput}
+ onChange={(e) => setQuantityInput(e.target.value)}
+ className={style['quantity-input']}
+ />
+ <button
+ type='button'
+ className='absolute right-0 px-2 py-1 h-full text-gray-500'
+ onClick={() =>
+ setQuantityInput(String(Number(quantityInput) + 1))
+ }
+ >
+ +
+ </button>
+ </div>
- <div>
- <Skeleton
- isLoaded={sla}
- h='21px'
- // w={16}
- className={sla?.qty < 10 ? 'text-red-600 font-medium' : ''}
- >
- Stock : {sla?.qty}{' '}
- </Skeleton>
- {/* <span className={sla?.qty < 10 ? 'text-red-600 font-medium' : ''}>
- {' '}
- </span> */}
- </div>
- <div>
- {qtyPickUp > 0 && (
- <Link href='/panduan-pick-up-service' className='group'>
- <Image
- src='/images/PICKUP-NOW.png'
- className='group-hover:scale-105 transition-transform duration-200'
- alt='pickup now'
- width={100}
- height={12}
- />
- </Link>
- )}
+ {/* Stok */}
+ <div>
+ <Skeleton
+ isLoaded={sla}
+ h='21px'
+ className={sla?.qty < 10 ? 'text-red-600 font-medium' : ''}
+ >
+ Stock : {sla?.qty}{' '}
+ </Skeleton>
+ </div>
+
+ {/* Pickup badge */}
+ <div>
+ {qtyPickUp > 0 && (
+ <div className='flex items-center gap-2'>
+ <Link href='/panduan-pick-up-service' className='group'>
+ <Image
+ src='/images/PICKUP-NOW.png'
+ className='group-hover:scale-105 transition-transform duration-200'
+ alt='pickup now'
+ width={100}
+ height={12}
+ />
+ </Link>
+ </div>
+ )}
+ </div>
</div>
- </div>
- {qtyPickUp > 0 && (
- <div className='text-[12px] mt-1 text-red-500 italic'>
+ <span className='text-[12px] text-red-500 italic'>
* {qtyPickUp} barang bisa di pickup
+ </span>
+ </DesktopView>
+
+ {/* ===== MOBILE: grid kiri-kanan, kanan hanya qty ===== */}
+ <MobileView>
+ <div className='grid grid-cols-12 items-start gap-3'>
+ {/* Kiri */}
+ <div className='col-span-8 mt-2'>
+ <div className='flex items-center gap-1'>
+ <Skeleton
+ isLoaded={sla}
+ h='21px'
+ w='auto' // ⬅️ penting: biar selebar konten
+ display='inline-block' // ⬅️ penting: jangan full width
+ className={sla?.qty < 10 ? 'text-red-600 font-medium' : ''}
+ >
+ Stock : {sla?.qty}
+ </Skeleton>
+
+ {qtyPickUp > 0 && (
+ <Link
+ href='/panduan-pick-up-service'
+ className='inline-block shrink-0'
+ >
+ <Image
+ src='/images/PICKUP-NOW.png'
+ className='align-middle'
+ alt='pickup now'
+ width={90}
+ height={12}
+ />
+ </Link>
+ )}
+ </div>
+
+ {qtyPickUp > 0 && (
+ <div className='text-[12px] mt-1 text-red-500 italic'>
+ * {qtyPickUp} barang bisa di pickup
+ </div>
+ )}
+ </div>
+
+ {/* Kanan: hanya qty, rata kanan */}
+ <div className='col-span-4 flex justify-end'>
+ <div className='inline-flex items-stretch border rounded-xl overflow-hidden'>
+ <button
+ type='button'
+ className='h-11 w-11 md:h-12 md:w-12 grid place-items-center text-gray-700 hover:bg-gray-100 active:scale-95 select-none touch-manipulation focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500'
+ onClick={() =>
+ setQuantityInput(
+ String(Math.max(1, Number(quantityInput) - 1))
+ )
+ }
+ aria-label='Kurangi'
+ >
+ <span className='text-2xl leading-none'>–</span>
+ </button>
+
+ <input
+ type='number'
+ id='quantity'
+ min={1}
+ value={quantityInput}
+ onChange={(e) => setQuantityInput(e.target.value)}
+ className='h-11 md:h-12 w-16 md:w-20 text-center text-lg md:text-xl outline-none border-x
+ [appearance:textfield]
+ [&::-webkit-outer-spin-button]:appearance-none
+ [&::-webkit-inner-spin-button]:appearance-none'
+ />
+
+ <button
+ type='button'
+ className='h-11 w-11 md:h-12 md:w-12 grid place-items-center text-gray-700 hover:bg-gray-100 active:scale-95 select-none touch-manipulation focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500'
+ onClick={() =>
+ setQuantityInput(String(Number(quantityInput) + 1))
+ }
+ aria-label='Tambah'
+ >
+ <span className='text-2xl leading-none'>+</span>
+ </button>
+ </div>
+ </div>
</div>
- )}
+ </MobileView>
<div className='h-4' />
- <div className={`${style['action-wrapper']}`}>
- <AddToCart
- products={product}
- variantId={activeVariantId}
- quantity={Number(quantityInput)}
- />
- {!isApproval && (
+ <DesktopView>
+ <div className={`${style['action-wrapper']}`}>
<AddToCart
+ products={product}
+ variantId={activeVariantId}
+ quantity={Number(quantityInput)}
+ />
+ {!isApproval && (
+ <AddToCart
+ source='buy'
+ products={product}
+ variantId={activeVariantId}
+ quantity={Number(quantityInput)}
+ />
+ )}
+ </div>
+ <div className='mt-4'>
+ <AddToQuotation
source='buy'
products={product}
variantId={activeVariantId}
quantity={Number(quantityInput)}
/>
- )}
- </div>
- <div className='mt-4'>
- <AddToQuotation
- source='buy'
- products={product}
- variantId={activeVariantId}
- quantity={Number(quantityInput)}
- />
- </div>
+ </div>
+ </DesktopView>
+ <MobileView>
+ <div className='grid grid-cols-12 gap-2'>
+ <div className='col-span-2'>
+ <AddToQuotation
+ source='buy'
+ products={product}
+ variantId={activeVariantId}
+ quantity={Number(quantityInput)}
+ />
+ </div>
+ <div className='col-span-5'>
+ <AddToCart
+ products={product}
+ variantId={activeVariantId}
+ quantity={Number(quantityInput)}
+ />
+ </div>
+ <div className='col-span-5'>
+ {!isApproval && (
+ <AddToCart
+ source='buy'
+ products={product}
+ variantId={activeVariantId}
+ quantity={Number(quantityInput)}
+ />
+ )}
+ </div>
+ </div>
+ </MobileView>
</div>
);
};
diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx
index 192e1dc3..f32bb38e 100644
--- a/src-migrate/modules/product-detail/components/ProductDetail.tsx
+++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx
@@ -2,12 +2,11 @@ import style from '../styles/product-detail.module.css';
import Link from 'next/link';
import { useRouter } from 'next/router';
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState, UIEvent } from 'react';
import { Button } from '@chakra-ui/react';
import { MessageCircleIcon, Share2Icon } from 'lucide-react';
import { LazyLoadComponent } from 'react-lazy-load-image-component';
-import { RWebShare } from 'react-web-share';
import useDevice from '@/core/hooks/useDevice';
import { getAuth } from '~/libs/auth';
@@ -22,6 +21,8 @@ import Information from './Information';
import PriceAction from './PriceAction';
import SimilarBottom from './SimilarBottom';
import SimilarSide from './SimilarSide';
+import dynamic from 'next/dynamic';
+
import { gtagProductDetail } from '@/core/utils/googleTag';
@@ -29,12 +30,25 @@ type Props = {
product: IProductDetail;
};
+const RWebShare = dynamic(
+ () => import('react-web-share').then(m => m.RWebShare),
+ { ssr: false }
+);
+
const SELF_HOST = process.env.NEXT_PUBLIC_SELF_HOST;
const ProductDetail = ({ product }: Props) => {
const { isDesktop, isMobile } = useDevice();
const router = useRouter();
- const auth = getAuth();
+ const [auth, setAuth] = useState<any>(null);
+ useEffect(() => {
+ try { setAuth(getAuth() ?? null); } catch { }
+ }, []);
+
+ const canShare =
+ typeof navigator !== 'undefined' &&
+ typeof (navigator as any).share === 'function';
+
const {
setAskAdminUrl,
askAdminUrl,
@@ -71,70 +85,173 @@ const ProductDetail = ({ product }: Props) => {
product?.variants?.find((variant) => variant.is_in_bu) ||
product?.variants?.[0];
setSelectedVariant(selectedVariant);
- // setSelectedVariant(product?.variants[0])
}, []);
- // Gabungkan semua gambar produk (utama + tambahan)
- const allImages = product.image_carousel ? [...product.image_carousel] : [];
- if (product.image) {
- allImages.unshift(product.image); // Tambahkan gambar utama di awal array
- }
- console.log(product);
+ const allImages = (() => {
+ const arr: string[] = [];
+ if (product?.image) arr.push(product.image);
+ if (
+ 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)) {
+ arr.push(img);
+ set.add(img);
+ }
+ }
+ }
+ return arr;
+ })();
const [mainImage, setMainImage] = useState(allImages[0] || '');
+ useEffect(() => {
+
+ if (!allImages.includes(mainImage)) {
+ setMainImage(allImages[0] || '');
+ }
+ }, [allImages]);
+
+
+ const sliderRef = useRef<HTMLDivElement | null>(null);
+ const [currentIdx, setCurrentIdx] = useState(0);
+
+ const handleMobileScroll = (e: UIEvent<HTMLDivElement>) => {
+ const el = e.currentTarget;
+ if (!el) return;
+ const idx = Math.round(el.scrollLeft / el.clientWidth);
+ if (idx !== currentIdx) {
+ setCurrentIdx(idx);
+ setMainImage(allImages[idx] || '');
+ }
+ };
+
+ const scrollToIndex = (i: number) => {
+ const el = sliderRef.current;
+ if (!el) return;
+ el.scrollTo({ left: i * el.clientWidth, behavior: 'smooth' });
+ setCurrentIdx(i);
+ setMainImage(allImages[i] || '');
+ };
+
+
return (
<>
<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} />
</div>
+
<div className='md:w-9/12 md:flex md:flex-col md:pr-4 md:pt-6'>
<div className='md:flex md:flex-wrap'>
+ {/* ===== Kolom kiri: gambar ===== */}
<div className='md:w-4/12'>
- <ProductImage product={{ ...product, image: mainImage }} />
-
- {/* Carousel horizontal */}
- {allImages.length > 0 && (
- <div className='mt-4 overflow-x-auto'>
- <div className='flex space-x-3 pb-3'>
- {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'
- }`}
- onClick={() => setMainImage(img)}
- >
+ {/* === MOBILE: Slider swipeable, tanpa thumbnail carousel === */}
+ {isMobile ? (
+ <div className='relative'>
+ <div
+ ref={sliderRef}
+ onScroll={handleMobileScroll}
+ className='flex overflow-x-auto snap-x snap-mandatory scroll-smooth no-scrollbar'
+ style={{
+ scrollBehavior: 'smooth',
+ msOverflowStyle: 'none',
+ scrollbarWidth: 'none',
+ }}
+ >
+ {allImages.length > 0 ? (
+ allImages.map((img, i) => (
+
+ <div
+ key={i}
+ className='w-full flex-shrink-0 snap-center flex justify-center items-center'
+ >
+ {/* gambar diperkecil */}
+ <img
+ src={img}
+ alt={`Gambar ${i + 1}`}
+ className='w-[85%] aspect-square object-contain'
+ onError={(e) => {
+ (e.target as HTMLImageElement).src =
+ '/images/noimage.jpeg';
+ }}
+ />
+ </div>
+ ))
+ ) : (
+ <div className='w-full flex-shrink-0 snap-center flex justify-center items-center'>
<img
- src={img}
- alt={`Thumbnail ${index + 1}`}
- className='w-full h-full object-cover rounded-sm'
- loading='lazy'
- onError={(e) => {
- (e.target as HTMLImageElement).src =
- '/path/to/fallback-image.jpg';
- }}
+ src={mainImage || '/images/noimage.jpeg'}
+ alt='Gambar produk'
+ className='w-[85%] aspect-square object-contain'
/>
</div>
- ))}
+ )}
</div>
+
+ {/* Dots indicator */}
+ {allImages.length > 1 && (
+ <div className='absolute bottom-2 left-0 right-0 flex justify-center gap-2'>
+ {allImages.map((_, i) => (
+ <button
+ key={i}
+ aria-label={`Ke slide ${i + 1}`}
+ className={`w-2 h-2 rounded-full ${currentIdx === i ? 'bg-gray-800' : 'bg-gray-300'
+ }`}
+ onClick={() => scrollToIndex(i)}
+ />
+ ))}
+ </div>
+ )}
</div>
+ ) : (
+ <>
+ {/* === DESKTOP: Tetap seperti sebelumnya === */}
+ <ProductImage product={{ ...product, image: mainImage }} />
+
+ {/* Carousel horizontal (thumbnail) – hanya desktop */}
+ {allImages.length > 0 && (
+ <div className='mt-4 overflow-x-auto'>
+ <div className='flex space-x-3 pb-3'>
+ {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'
+ }`}
+ onClick={() => setMainImage(img)}
+ >
+ <img
+ src={img}
+ alt={`Thumbnail ${index + 1}`}
+ className='w-full h-full object-cover rounded-sm'
+ loading='lazy'
+ onError={(e) => {
+ (e.target as HTMLImageElement).src =
+ '/images/noimage.jpeg';
+ }}
+ />
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+ </>
)}
</div>
+ {/* <<=== 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>
</div>
@@ -154,14 +271,6 @@ const ProductDetail = ({ product }: Props) => {
/>
)}
- {/* <div className={style['section-card']}>
- <h2 className={style['heading']}>
- Variant ({product.variant_total})
- </h2>
- <div className='h-4' />
- <VariantList variants={product.variants} />
- </div> */}
-
<div className='h-0 md:h-6' />
<div className={style['section-card']}>
@@ -172,8 +281,7 @@ 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,
}}
@@ -204,21 +312,19 @@ const ProductDetail = ({ product }: Props) => {
<span>|</span>
- <RWebShare
- data={{
- text: 'Check out this product',
- title: `${product.name} - Indoteknik.com`,
- url: SELF_HOST + router.asPath,
- }}
- >
- <Button
- variant='link'
- colorScheme='gray'
- leftIcon={<Share2Icon size={18} />}
+ {canShare && (
+ <RWebShare
+ data={{
+ text: 'Check out this product',
+ title: `${product.name} - Indoteknik.com`,
+ url: (process.env.NEXT_PUBLIC_SELF_HOST || '') + (router?.asPath || '/'),
+ }}
>
- Share
- </Button>
- </RWebShare>
+ <Button variant='link' colorScheme='gray' leftIcon={<Share2Icon size={18} />}>
+ Share
+ </Button>
+ </RWebShare>
+ )}
</div>
<div className='h-6' />
@@ -244,6 +350,8 @@ const ProductDetail = ({ product }: Props) => {
</div>
</>
);
+
+
};
export default ProductDetail;
diff --git a/src-migrate/pages/shop/cart/cart.module.css b/src-migrate/pages/shop/cart/cart.module.css
index b756fb15..af5a2abc 100644
--- a/src-migrate/pages/shop/cart/cart.module.css
+++ b/src-migrate/pages/shop/cart/cart.module.css
@@ -3,7 +3,7 @@
}
.content {
- @apply flex flex-wrap ;
+ @apply flex flex-wrap;
}
.item-wrapper {
@@ -33,3 +33,15 @@
.summary-buttons-step-approval {
@apply grid grid-cols-1 gap-y-3 mt-6;
}
+
+@media (max-width: 768px) {
+ .item-wrapper {
+ /* adjust if your bar is taller/shorter */
+ padding-bottom: calc(env(safe-area-inset-bottom) + 9rem);
+ }
+
+ .summary-wrapper {
+ @apply fixed inset-x-0 bottom-0 z-50 md:sticky w-full;
+ }
+
+} \ No newline at end of file
diff --git a/src-migrate/pages/shop/cart/index.tsx b/src-migrate/pages/shop/cart/index.tsx
index 795dfa72..031aa45b 100644
--- a/src-migrate/pages/shop/cart/index.tsx
+++ b/src-migrate/pages/shop/cart/index.tsx
@@ -301,9 +301,8 @@ const CartPage: React.FC = () => {
<>
{/* Sticky Header */}
<div
- className={`${
- isTop ? 'border-b-[0px]' : 'border-b-[1px]'
- } sticky md:top-[157px] flex-col bg-white py-4 border-gray-300 z-50 sm:w-full md:w-3/4`}
+ className={`${isTop ? 'border-b-[0px]' : 'border-b-[1px]'
+ } sticky md:top-[157px] flex-col bg-white py-4 border-gray-300 z-50 sm:w-full md:w-3/4`}
>
<div className='flex items-center justify-between mb-2'>
<h1 className={style.title}>Keranjang Belanja</h1>
@@ -392,13 +391,17 @@ const CartPage: React.FC = () => {
</div>
</div>
+ <div
+ className="md:hidden"
+ style={{ height: 'calc(var(--mobile-actions-h) + var(--mobile-total-h))' }}
+ />
+
{/* Cart Summary */}
<div
- className={`${style['summary-wrapper']} ${
- device.isMobile && (!cart || cart?.product_total === 0)
+ className={`${style['summary-wrapper']} ${device.isMobile && (!cart || cart?.product_total === 0)
? 'hidden'
: ''
- }`}
+ }`}
>
<div className={style.summary}>
{device.isMobile ? (
diff --git a/src-migrate/pages/shop/product/[slug].tsx b/src-migrate/pages/shop/product/[slug].tsx
index fc72a6b0..90658544 100644
--- a/src-migrate/pages/shop/product/[slug].tsx
+++ b/src-migrate/pages/shop/product/[slug].tsx
@@ -18,34 +18,41 @@ type PageProps = {
product: IProductDetail
}
-export const getServerSideProps: GetServerSideProps<PageProps> = (async (context) => {
- const { slug } = context.query
+export const getServerSideProps: GetServerSideProps<PageProps & { canonicalPath: string }> = async (context) => {
+ const { slug } = context.query;
const cookieString = context.req.headers.cookie;
const cookies = cookieString ? cookie.parse(cookieString) : {};
const auth = cookies?.auth ? JSON.parse(cookies.auth) : {};
- const tier = auth?.pricelist || ''
+ const tier = auth?.pricelist || '';
- const productId = getIdFromSlug(slug as string)
+ const productId = getIdFromSlug(slug as string);
+ const product = await getProductById(productId, tier);
- const product = await getProductById(productId, tier)
+ // ❌ produk tidak ada → 404
+ if (!product) return { notFound: true };
- if (!product) return { notFound: true }
+ // ❌ tidak ada variants atau tidak ada yang harga > 0 → 404
+ const hasValidVariant = Array.isArray(product.variants)
+ && product.variants.some(v => (v?.price?.price ?? 0) > 0);
+ if (!hasValidVariant) return { notFound: true };
+
+ // Canonical path aman untuk SSR (hindari router.asPath di server)
+ const canonicalPath = context.resolvedUrl || `/product/${slug}`;
+
+ return { props: { product, canonicalPath } };
+};
- return {
- props: { product }
- }
-})
const SELF_HOST = process.env.NEXT_PUBLIC_SELF_HOST
-const ProductDetailPage: NextPage<PageProps> = ({ product }) => {
+const ProductDetailPage: NextPage<PageProps & { canonicalPath: string }> = ({ product, canonicalPath }) => {
const router = useRouter();
-
const { setProduct } = useProductContext();
- useEffect(() => {
- if (product) setProduct(product);
- }, [product, setProduct]);
+ useEffect(() => { if (product) setProduct(product); }, [product, setProduct]);
+
+ const origin = process.env.NEXT_PUBLIC_SELF_HOST || '';
+ const url = origin + (canonicalPath?.startsWith('/') ? canonicalPath : `/${canonicalPath}`);
return (
<BasicLayout>
@@ -53,31 +60,20 @@ const ProductDetailPage: NextPage<PageProps> = ({ product }) => {
title={`${product.name} - Indoteknik.com`}
description='Temukan pilihan produk B2B Industri &amp; Alat Teknik untuk Perusahaan, UMKM &amp; Pemerintah dengan lengkap, mudah dan transparan.'
openGraph={{
- url: SELF_HOST + router.asPath,
- images: [
- {
- url: product?.image,
- width: 800,
- height: 800,
- alt: product?.name,
- },
- ],
+ url,
+ images: [{ url: product?.image, width: 800, height: 800, alt: product?.name }],
type: 'product',
}}
- additionalMetaTags={[
- {
- name: 'keywords',
- content: `${product?.name}, Harga ${product?.name}, Beli ${product?.name}, Spesifikasi ${product?.name}`,
- }
- ]}
- canonical={SELF_HOST + router.asPath}
+ additionalMetaTags={[{ name: 'keywords', content: `${product?.name}, Harga ${product?.name}, Beli ${product?.name}, Spesifikasi ${product?.name}` }]}
+ canonical={url}
/>
<div className='md:container pt-4 md:pt-6'>
<ProductDetail product={product} />
</div>
</BasicLayout>
- )
-}
+ );
+};
+
export default ProductDetailPage \ No newline at end of file