summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorit-fixcomart <it@fixcomart.co.id>2024-10-24 10:11:31 +0700
committerit-fixcomart <it@fixcomart.co.id>2024-10-24 10:11:31 +0700
commit5f0f6b865bafd1570b24b8caffdb992ffbb476fc (patch)
treecb10cb24f45823c118155bff5f30490691aa0b42
parentf073b22e917acde22c21808906a37270e274085f (diff)
parentca30c28dd0b19977eb771fc32ff5e520cdef1068 (diff)
Merge branch 'CR/product_detail' into Feature/penawaran-instan
-rw-r--r--package.json1
-rw-r--r--src-migrate/modules/product-detail/components/Information.tsx222
-rw-r--r--src-migrate/modules/product-detail/components/PriceAction.tsx92
-rw-r--r--src-migrate/modules/product-detail/components/ProductDetail.tsx195
-rw-r--r--src-migrate/modules/product-detail/stores/useProductDetail.ts12
-rw-r--r--src-migrate/modules/product-detail/styles/information.module.css4
-rw-r--r--src-migrate/modules/product-detail/styles/price-action.module.css5
-rw-r--r--src-migrate/pages/shop/cart/index.tsx10
-rw-r--r--src-migrate/types/product.ts2
-rw-r--r--src/core/components/layouts/BasicLayout.jsx33
-rw-r--r--src/lib/product/components/Product/ProductDesktopVariant.jsx307
-rw-r--r--src/pages/_app.jsx2
-rw-r--r--src/pages/api/shop/variant-detail.js2
-rw-r--r--src/pages/shop/product/variant/[slug].jsx7
-rw-r--r--src/utils/solrMapping.js2
15 files changed, 612 insertions, 284 deletions
diff --git a/package.json b/package.json
index a846749c..130e2f0c 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
},
"dependencies": {
"@chakra-ui/react": "^2.8.1",
+ "@choc-ui/chakra-autocomplete": "^5.6.2",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@heroicons/react": "^2.0.13",
diff --git a/src-migrate/modules/product-detail/components/Information.tsx b/src-migrate/modules/product-detail/components/Information.tsx
index 75ae3c41..8655517d 100644
--- a/src-migrate/modules/product-detail/components/Information.tsx
+++ b/src-migrate/modules/product-detail/components/Information.tsx
@@ -1,56 +1,220 @@
-import style from '../styles/information.module.css'
+import {
+ AutoComplete,
+ AutoCompleteInput,
+ AutoCompleteItem,
+ AutoCompleteList,
+} from '@choc-ui/chakra-autocomplete';
+import style from '../styles/information.module.css';
-import React from 'react'
-import dynamic from 'next/dynamic'
-import Link from 'next/link'
-import { useQuery } from 'react-query'
+import dynamic from 'next/dynamic';
+import Link from 'next/link';
+import { useEffect, useRef, useState } from 'react';
-import { IProductDetail } from '~/types/product'
-import { IProductVariantSLA } from '~/types/productVariant'
-import { createSlug } from '~/libs/slug'
-import { getVariantSLA } from '~/services/productVariant'
-import { formatToShortText } from '~/libs/formatNumber'
+import currencyFormat from '@/core/utils/currencyFormat';
+import { InputGroup, InputRightElement } from '@chakra-ui/react';
+import { ChevronDownIcon } from '@heroicons/react/24/outline';
+import Image from 'next/image';
+import { formatToShortText } from '~/libs/formatNumber';
+import { createSlug } from '~/libs/slug';
+import { getVariantSLA } from '~/services/productVariant';
+import { IProductDetail } from '~/types/product';
+import { useProductDetail } from '../stores/useProductDetail';
-const Skeleton = dynamic(() => import('@chakra-ui/react').then((mod) => mod.Skeleton))
+const Skeleton = dynamic(() =>
+ import('@chakra-ui/react').then((mod) => mod.Skeleton)
+);
type Props = {
- product: IProductDetail
-}
+ product: IProductDetail;
+};
const Information = ({ product }: Props) => {
- const querySLA = useQuery<IProductVariantSLA>({
- queryKey: ['variant-sla', product.variants[0]?.id],
- queryFn: () => getVariantSLA(product.variants[0].id),
- enabled: product.variant_total === 1
- })
+ const { selectedVariant, setSelectedVariant, setSla, setActive, sla } =
+ useProductDetail();
- const sla = querySLA?.data
+ const [inputValue, setInputValue] = useState<string | null>(
+ selectedVariant?.code + ' - ' + selectedVariant?.attributes[0]
+ );
+
+ const inputRef = useRef<HTMLInputElement>(null);
+
+ const [variantOptions, setVariantOptions] = useState<any[]>(
+ product?.variants
+ );
+ // let variantOptions = product?.variants;
+
+ // const querySLA = useQuery<IProductVariantSLA>({
+ // queryKey: ['variant-sla', selectedVariant?.id],
+ // queryFn: () => getVariantSLA(selectedVariant?.id),
+ // enabled: !!selectedVariant?.id,
+ // });
+ // const sla = querySLA?.data;
+
+ const getsla = async () => {
+ const querySLA = await getVariantSLA(selectedVariant?.id);
+ setSla(querySLA);
+ };
+
+ useEffect(() => {
+ getsla();
+ setInputValue(
+ selectedVariant?.code +
+ (selectedVariant?.attributes[0]
+ ? ' - ' + selectedVariant?.attributes[0]
+ : '')
+ );
+ }, [selectedVariant]);
+
+ const handleOnChange = (vals: any) => {
+ let code = vals.split(' ')[0];
+ let variant = variantOptions.find((item) => item.code === code);
+ setSelectedVariant(variant);
+ setInputValue(
+ variant?.code +
+ (variant?.attributes[0] ? ' - ' + variant?.attributes[0] : '')
+ );
+ if (variant) {
+ const filteredOptions = product?.variants.filter(
+ (item) => item !== variant
+ );
+ const newOptions = [variant, ...filteredOptions];
+ setVariantOptions(newOptions);
+ }
+ };
+
+ const handleOnKeyUp = (e: any) => {
+ setInputValue(e.target.value);
+ };
return (
<div className={style['wrapper']}>
+ <div className='realtive mb-5'>
+ <label className='form-label mb-2 text-lg text-red-600'>
+ Pilih Variant * :{' '}
+ <span className='text-gray_r-9 text-sm'>
+ {product?.variant_total} Variants
+ </span>{' '}
+ </label>
+ <AutoComplete
+ openOnFocus
+ className='form-input'
+ onChange={(vals) => handleOnChange(vals)}
+ >
+ <InputGroup>
+ <AutoCompleteInput
+ ref={inputRef}
+ value={inputValue as string}
+ onChange={(e) => handleOnKeyUp(e)}
+ />
+ <InputRightElement>
+ <ChevronDownIcon
+ className='h-6 w-6 text-gray-500'
+ onClick={() => inputRef?.current?.focus()}
+ />
+ </InputRightElement>
+ </InputGroup>
+
+ <AutoCompleteList>
+ {variantOptions.map((option, cid) => (
+ <AutoCompleteItem
+ key={`option-${cid}`}
+ value={
+ option.code +
+ (option?.attributes[0] ? ' - ' + option?.attributes[0] : '')
+ }
+ _selected={
+ option.id === selectedVariant?.id
+ ? {
+ bg: 'gray.300',
+ }
+ : undefined
+ }
+ textTransform='capitalize'
+ >
+ <div
+ key={cid}
+ className='flex gap-x-2 w-full justify-between px-3 items-center p-2'
+ >
+ <div className='text-small'>
+ {option.code +
+ (option?.attributes[0]
+ ? ' - ' + option?.attributes[0]
+ : '')}
+ </div>
+ <div
+ className={
+ option?.price?.discount_percentage
+ ? 'flex gap-x-4 items-center justify-between'
+ : ''
+ }
+ >
+ {option?.price?.discount_percentage > 0 && (
+ <>
+ <div className='badge-solid-red text-xs'>
+ {Math.floor(option?.price?.discount_percentage)}%
+ </div>
+ <div className='min-w-16 sm:min-w-24 text-gray_r-11 line-through text-[11px] sm:text-caption-2'>
+ {currencyFormat(option?.price?.price)}
+ </div>
+ </>
+ )}
+ <div className='min-w-20 sm:min-w-28 text-danger-500 font-semibold'>
+ {currencyFormat(option?.price?.price_discount)}
+ </div>
+ </div>
+ </div>
+ </AutoCompleteItem>
+ ))}
+ </AutoCompleteList>
+ </AutoComplete>
+ </div>
+
<div className={style['row']}>
- <div className={style['label']}>SKU Number</div>
- <div className={style['value']}>SKU-{product.id}</div>
+ <div className={style['label']}>Item Code</div>
+ <div className={style['value']}>{selectedVariant?.code}</div>
</div>
<div className={style['row']}>
<div className={style['label']}>Manufacture</div>
<div className={style['value']}>
{!!product.manufacture.name ? (
<Link
- href={createSlug('/shop/brands/', product.manufacture.name, product.manufacture.id.toString())}
- className='text-danger-500 hover:underline'
+ href={createSlug(
+ '/shop/brands/',
+ product.manufacture.name,
+ product.manufacture.id.toString()
+ )}
>
- {product.manufacture.name}
+ <Image
+ height={50}
+ width={100}
+ src={product.manufacture.logo}
+ alt={product.manufacture.name}
+ className='h-8 object-cover'
+ />
</Link>
- ) : '-'}
+ ) : (
+ '-'
+ )}
+ </div>
+ </div>
+ <div className={style['row']}>
+ <div className={style['label']}>Berat Barang</div>
+ <div className={style['value']}>
+ {selectedVariant?.weight > 0 ? `${selectedVariant?.weight} Kg` : '-'}
</div>
</div>
<div className={style['row']}>
<div className={style['label']}>Terjual</div>
- <div className={style['value']}>{product.qty_sold > 0 ? formatToShortText(product.qty_sold) : '-'}</div>
+ <div className={style['value']}>
+ {product.qty_sold > 0 ? formatToShortText(product.qty_sold) : '-'}
+ </div>
+ </div>
+ <div className={style['row']}>
+ <div className={style['label']}>Persiapan Barang</div>
+ <div className={style['value']}>{sla?.sla_date}</div>
</div>
</div>
- )
-}
+ );
+};
-export default Information \ No newline at end of file
+export default Information;
diff --git a/src-migrate/modules/product-detail/components/PriceAction.tsx b/src-migrate/modules/product-detail/components/PriceAction.tsx
index a69e896c..a3126cdd 100644
--- a/src-migrate/modules/product-detail/components/PriceAction.tsx
+++ b/src-migrate/modules/product-detail/components/PriceAction.tsx
@@ -1,6 +1,8 @@
import style from '../styles/price-action.module.css';
-import React, { useEffect } from 'react';
+import Image from 'next/image';
+import Link from 'next/link';
+import { useEffect } from 'react';
import formatCurrency from '~/libs/formatCurrency';
import { IProductDetail } from '~/types/product';
import { useProductDetail } from '../stores/useProductDetail';
@@ -23,27 +25,38 @@ const PriceAction = ({ product }: Props) => {
askAdminUrl,
isApproval,
setIsApproval,
+ selectedVariant,
+ sla,
} = useProductDetail();
useEffect(() => {
- setActive(product.variants[0])
- if(product.variants.length > 2 && product.variants[0].price.price === 0){
- const variants = product.variants
+ setActive(selectedVariant);
+ if (product.variants.length > 2 && product.variants[0].price.price === 0) {
+ const variants = product.variants;
for (let i = 0; i < variants.length; i++) {
- if(variants[i].price.price > 0){
- setActive(variants[i])
+ if (variants[i].price.price > 0) {
+ setActive(variants[i]);
break;
}
}
}
-
- }, [product, setActive]);
+ }, [product, setActive, selectedVariant]);
+ let voucherPastiHemat = 0;
+ if (
+ product?.voucher_pasti_hemat
+ ? product?.voucher_pasti_hemat.length
+ : voucherPastiHemat > 0
+ ) {
+ const stringVoucher = product?.voucher_pasti_hemat[0];
+ const validJsonString = stringVoucher.replace(/'/g, '"');
+ voucherPastiHemat = JSON.parse(validJsonString);
+ }
return (
<div
- className='block md:sticky top-[150px] bg-white py-0 md:py-6 z-10'
+ className='block md:sticky md:top-[150px] md:py-6 fixed bottom-0 left-0 right-0 bg-white p-2 z-10'
id='price-section'
>
{!!activePrice && activePrice.price > 0 && (
@@ -85,18 +98,57 @@ const PriceAction = ({ product }: Props) => {
)}
<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>
+
+ <div>
+ <span className={sla?.qty < 10 ? 'text-red-600 font-medium' : ''}>
+ {' '}
+ Stock : {sla?.qty}{' '}
+ </span>
+ </div>
+ <div>
+ {product?.is_in_bu && (
+ <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 className='h-4' />
- <div className={style['action-wrapper']}>
- <label htmlFor='quantity' className='hidden'>
- Quantity
- </label>
- <input
- type='number'
- id='quantity'
- value={quantityInput}
- onChange={(e) => setQuantityInput(e.target.value)}
- className={style['quantity-input']}
- />
+ <div className={`${style['action-wrapper']}`}>
<AddToCart
products={product}
variantId={activeVariantId}
diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx
index e4555913..b036cc2d 100644
--- a/src-migrate/modules/product-detail/components/ProductDetail.tsx
+++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx
@@ -1,46 +1,52 @@
-import style from '../styles/product-detail.module.css'
-
-import Link from 'next/link'
-import { useRouter } from 'next/router'
-import { useEffect } 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 { whatsappUrl } from '~/libs/whatsappUrl'
-import ProductPromoSection from '~/modules/product-promo/components/Section'
-import { IProductDetail } from '~/types/product'
-import { useProductDetail } from '../stores/useProductDetail'
-import AddToWishlist from './AddToWishlist'
-import Breadcrumb from './Breadcrumb'
-import ProductImage from './Image'
-import Information from './Information'
-import PriceAction from './PriceAction'
-import SimilarBottom from './SimilarBottom'
-import SimilarSide from './SimilarSide'
-import VariantList from './VariantList'
-import { getAuth } from '~/libs/auth'
-
-import { gtagProductDetail } from '@/core/utils/googleTag'
+import style from '../styles/product-detail.module.css';
+
+import Link from 'next/link';
+import { useRouter } from 'next/router';
+import { useEffect } 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';
+import { whatsappUrl } from '~/libs/whatsappUrl';
+import ProductPromoSection from '~/modules/product-promo/components/Section';
+import { IProductDetail } from '~/types/product';
+import { useProductDetail } from '../stores/useProductDetail';
+import AddToWishlist from './AddToWishlist';
+import Breadcrumb from './Breadcrumb';
+import ProductImage from './Image';
+import Information from './Information';
+import PriceAction from './PriceAction';
+import SimilarBottom from './SimilarBottom';
+import SimilarSide from './SimilarSide';
+
+import { gtagProductDetail } from '@/core/utils/googleTag';
type Props = {
- product: IProductDetail
-}
+ product: IProductDetail;
+};
-const SELF_HOST = process.env.NEXT_PUBLIC_SELF_HOST
+const SELF_HOST = process.env.NEXT_PUBLIC_SELF_HOST;
const ProductDetail = ({ product }: Props) => {
- const { isDesktop, isMobile } = useDevice()
- const router = useRouter()
- const auth = getAuth()
- const { setAskAdminUrl, askAdminUrl, activeVariantId, setIsApproval, isApproval } = useProductDetail()
+ const { isDesktop, isMobile } = useDevice();
+ const router = useRouter();
+ const auth = getAuth();
+ const {
+ setAskAdminUrl,
+ askAdminUrl,
+ activeVariantId,
+ setIsApproval,
+ isApproval,
+ setSelectedVariant,
+ } = useProductDetail();
useEffect(() => {
gtagProductDetail(product);
- },[product])
+ }, [product]);
useEffect(() => {
const createdAskUrl = whatsappUrl({
@@ -48,76 +54,43 @@ const ProductDetail = ({ product }: Props) => {
payload: {
manufacture: product.manufacture.name,
productName: product.name,
- url: process.env.NEXT_PUBLIC_SELF_HOST + router.asPath
+ url: process.env.NEXT_PUBLIC_SELF_HOST + router.asPath,
},
- fallbackUrl: router.asPath
- })
+ fallbackUrl: router.asPath,
+ });
- setAskAdminUrl(createdAskUrl)
- }, [router.asPath, product.manufacture.name, product.name, setAskAdminUrl])
+ setAskAdminUrl(createdAskUrl);
+ }, [router.asPath, product.manufacture.name, product.name, setAskAdminUrl]);
useEffect(() => {
if (typeof auth === 'object') {
setIsApproval(auth?.feature?.soApproval);
}
+ setSelectedVariant(product?.variants[0])
}, []);
return (
<>
<div className='md:flex md:flex-wrap'>
- <div className="w-full mb-4 md:mb-0 px-4 md:px-0">
+ <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'>
- <div className="md:w-4/12">
+ <div className='md:w-4/12'>
<ProductImage product={product} />
</div>
<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>
+ <h1 className={style['title']}>{product.name}</h1>
- <div className='h-6 md:h-8' />
+ <div className='h-3 md:h-0' />
<Information product={product} />
<div className='h-6' />
-
- <div className="flex gap-x-5">
- <Button
- as={Link}
- href={askAdminUrl}
- variant='link'
- target='_blank'
- colorScheme='gray'
- leftIcon={<MessageCircleIcon size={18} />}
- >
- Ask Admin
- </Button>
-
- <AddToWishlist productId={product.id} />
-
- <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} />}
- >
- Share
- </Button>
- </RWebShare>
- </div>
-
</div>
</div>
@@ -131,38 +104,72 @@ const ProductDetail = ({ product }: Props) => {
<div className='h-4 md:h-10' />
{!!activeVariantId && !isApproval && <ProductPromoSection product={product} productId={activeVariantId} />}
- <div className={style['section-card']}>
+ {/* <div className={style['section-card']}>
<h2 className={style['heading']}>
Variant ({product.variant_total})
</h2>
<div className='h-4' />
<VariantList variants={product.variants} />
- </div>
+ </div> */}
<div className='h-0 md:h-6' />
<div className={style['section-card']}>
- <h2 className={style['heading']}>
- Informasi Produk
- </h2>
+ <h2 className={style['heading']}>Informasi Produk</h2>
<div className='h-4' />
<div
className={style['description']}
- dangerouslySetInnerHTML={{ __html: !product.description || product.description == '<p><br></p>' ? 'Belum ada deskripsi' : product.description }}
+ dangerouslySetInnerHTML={{
+ __html:
+ !product.description || product.description == '<p><br></p>'
+ ? 'Belum ada deskripsi'
+ : product.description,
+ }}
/>
</div>
</div>
</div>
{isDesktop && (
- <div className="md:w-3/12">
+ <div className='md:w-3/12'>
<PriceAction product={product} />
+ <div className='flex gap-x-5 items-center justify-center'>
+ <Button
+ as={Link}
+ href={askAdminUrl}
+ variant='link'
+ target='_blank'
+ colorScheme='gray'
+ leftIcon={<MessageCircleIcon size={18} />}
+ >
+ Ask Admin
+ </Button>
+
+ <span>|</span>
+
+ <AddToWishlist productId={product.id} />
+
+ <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} />}
+ >
+ Share
+ </Button>
+ </RWebShare>
+ </div>
<div className='h-6' />
-
- <div className={style['heading']}>
- Produk Serupa
- </div>
+ <div className={style['heading']}>Produk Serupa</div>
<div className='h-4' />
@@ -171,9 +178,7 @@ const ProductDetail = ({ product }: Props) => {
)}
<div className='md:w-full pt-4 md:py-10 px-4 md:px-0'>
- <div className={style['heading']}>
- Kamu Mungkin Juga Suka
- </div>
+ <div className={style['heading']}>Kamu Mungkin Juga Suka</div>
<div className='h-6' />
@@ -185,7 +190,7 @@ const ProductDetail = ({ product }: Props) => {
<div className='h-6 md:h-0' />
</div>
</>
- )
-}
+ );
+};
-export default ProductDetail \ No newline at end of file
+export default ProductDetail;
diff --git a/src-migrate/modules/product-detail/stores/useProductDetail.ts b/src-migrate/modules/product-detail/stores/useProductDetail.ts
index eb409930..dee6b342 100644
--- a/src-migrate/modules/product-detail/stores/useProductDetail.ts
+++ b/src-migrate/modules/product-detail/stores/useProductDetail.ts
@@ -7,6 +7,8 @@ type State = {
quantityInput: string;
askAdminUrl: string;
isApproval : boolean;
+ selectedVariant : any;
+ sla : any;
};
type Action = {
@@ -14,6 +16,8 @@ type Action = {
setQuantityInput: (value: string) => void;
setAskAdminUrl: (url: string) => void;
setIsApproval : (value : boolean) => void;
+ setSelectedVariant : (value : any) => void;
+ setSla : (value : any) => void;
};
export const useProductDetail = create<State & Action>((set, get) => ({
@@ -22,6 +26,8 @@ export const useProductDetail = create<State & Action>((set, get) => ({
quantityInput: '1',
askAdminUrl: '',
isApproval : false,
+ selectedVariant: null,
+ sla : null,
setActive: (variant) => {
set({ activeVariantId: variant?.id, activePrice: variant?.price });
},
@@ -33,5 +39,11 @@ export const useProductDetail = create<State & Action>((set, get) => ({
},
setIsApproval : (value : boolean) => {
set({ isApproval : value })
+ },
+ setSelectedVariant : (value : any) => {
+ set({ selectedVariant : value })
+ },
+ setSla : (value : any ) => {
+ set({ sla : value })
}
}));
diff --git a/src-migrate/modules/product-detail/styles/information.module.css b/src-migrate/modules/product-detail/styles/information.module.css
index c9b29020..5aa64fe5 100644
--- a/src-migrate/modules/product-detail/styles/information.module.css
+++ b/src-migrate/modules/product-detail/styles/information.module.css
@@ -3,11 +3,11 @@
}
.row {
- @apply flex p-3 rounded;
+ @apply flex p-4 rounded-sm bg-gray-100;
}
.row:nth-child(odd) {
- @apply bg-gray-100;
+ @apply bg-white;
}
.label {
diff --git a/src-migrate/modules/product-detail/styles/price-action.module.css b/src-migrate/modules/product-detail/styles/price-action.module.css
index 651de958..cea50bff 100644
--- a/src-migrate/modules/product-detail/styles/price-action.module.css
+++ b/src-migrate/modules/product-detail/styles/price-action.module.css
@@ -8,7 +8,10 @@
@apply flex gap-x-2.5;
}
.quantity-input {
- @apply px-2 rounded text-center border border-gray-300 w-14 h-10 focus:outline-none;
+ @apply w-24 h-10 text-center border border-gray-300 rounded focus:outline-none;
+ /* Padding di kiri dan kanan untuk memberi ruang bagi tombol */
+ padding-left: 2rem;
+ padding-right: 2rem;
}
.contact-us {
diff --git a/src-migrate/pages/shop/cart/index.tsx b/src-migrate/pages/shop/cart/index.tsx
index c5386c91..70a28073 100644
--- a/src-migrate/pages/shop/cart/index.tsx
+++ b/src-migrate/pages/shop/cart/index.tsx
@@ -35,6 +35,8 @@ const CartPage = () => {
const [hasChanged, setHasChanged] = useState(false);
const prevCartRef = useRef<CartItem[] | null>(null);
+ console.log('ini cart', cart);
+
useEffect(() => {
const handleScroll = () => {
setIsTop(window.scrollY < 200);
@@ -84,19 +86,19 @@ const CartPage = () => {
const hasSelectedPromo = useMemo(() => {
if (!cart) return false;
- return cart.products.some(
+ return cart?.products?.some(
(item) => item.cart_type === 'promotion' && item.selected
);
}, [cart]);
const hasSelected = useMemo(() => {
if (!cart) return false;
- return cart.products.some((item) => item.selected);
+ return cart?.products?.some((item) => item.selected);
}, [cart]);
const hasSelectNoPrice = useMemo(() => {
if (!cart) return false;
- return cart.products.some(
+ return cart?.products?.some(
(item) => item.selected && item.price.price_discount === 0
);
}, [cart]);
@@ -230,7 +232,7 @@ const CartPage = () => {
</div>
<div className={style['items']}>
- {cart?.products.map((item) => (
+ {cart?.products?.map((item) => (
<CartItemModule key={item.id} item={item} />
))}
diff --git a/src-migrate/types/product.ts b/src-migrate/types/product.ts
index 31ea0ce1..fb9c888c 100644
--- a/src-migrate/types/product.ts
+++ b/src-migrate/types/product.ts
@@ -31,7 +31,9 @@ export interface IProduct {
manufacture: {
id: number;
name: string;
+ logo: string;
};
+ voucher_pasti_hemat : any;
}
export interface IProductDetail extends IProduct {
diff --git a/src/core/components/layouts/BasicLayout.jsx b/src/core/components/layouts/BasicLayout.jsx
index c4674344..d6b96f36 100644
--- a/src/core/components/layouts/BasicLayout.jsx
+++ b/src/core/components/layouts/BasicLayout.jsx
@@ -8,6 +8,7 @@ import odooApi from '@/core/api/odooApi';
import whatsappUrl from '@/core/utils/whatsappUrl';
import Navbar from '../elements/Navbar/Navbar';
import styles from './BasicLayout.module.css'; // Import modul CSS
+import useDevice from '@/core/hooks/useDevice';
const AnimationLayout = dynamic(() => import('./AnimationLayout'), {
ssr: false,
@@ -24,6 +25,8 @@ const BasicLayout = ({ children }) => {
const [buttonPosition, setButtonPosition] = useState(null);
const [wobble, setWobble] = useState(false);
+ const { isDesktop, isMobile } = useDevice();
+
const router = useRouter();
const buttonRef = useRef(null);
@@ -49,7 +52,7 @@ const BasicLayout = ({ children }) => {
const handleMouseOut = (event) => {
const rect = buttonRef.current.getBoundingClientRect();
if (event.clientY <= 0) {
- setButtonPosition(rect)
+ setButtonPosition(rect);
setHighlight(true);
} else {
setHighlight(false);
@@ -92,13 +95,15 @@ const BasicLayout = ({ children }) => {
return (
<>
- {highlight && buttonPosition && (
+ {highlight && buttonPosition && (
<div
className={styles['overlay-highlight']}
style={{
- '--button-x': `${buttonPosition.x + buttonPosition.width / 2}px`,
+ '--button-x': `${buttonPosition.x + buttonPosition.width / 2}px`,
'--button-y': `${buttonPosition.y + buttonPosition.height / 2}px`,
- '--button-radius': `${Math.max(buttonPosition.width, buttonPosition.height) / 2}px`
+ '--button-radius': `${
+ Math.max(buttonPosition.width, buttonPosition.height) / 2
+ }px`,
}}
onAnimationEnd={() => setHighlight(false)}
/>
@@ -106,11 +111,21 @@ const BasicLayout = ({ children }) => {
<Navbar />
<AnimationLayout>
{children}
- <div className='fixed bottom-4 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)}>
- Whatsapp
+ <div className='fixed 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
diff --git a/src/lib/product/components/Product/ProductDesktopVariant.jsx b/src/lib/product/components/Product/ProductDesktopVariant.jsx
index 09b30a44..55effdfb 100644
--- a/src/lib/product/components/Product/ProductDesktopVariant.jsx
+++ b/src/lib/product/components/Product/ProductDesktopVariant.jsx
@@ -1,7 +1,6 @@
-
-import { Box, Skeleton, Tooltip } from '@chakra-ui/react';
+import { Box, Button, Skeleton, Tooltip } from '@chakra-ui/react';
import { HeartIcon } from '@heroicons/react/24/outline';
-import { Info } from 'lucide-react';
+import { Info, MessageCircleIcon, Share2Icon } from 'lucide-react';
import { useRouter } from 'next/router';
import { useCallback, useEffect, useRef, useState } from 'react';
import { toast } from 'react-hot-toast';
@@ -19,9 +18,13 @@ import currencyFormat from '@/core/utils/currencyFormat';
import { createSlug } from '@/core/utils/slug';
import whatsappUrl from '@/core/utils/whatsappUrl';
+import { RWebShare } from 'react-web-share';
import productSimilarApi from '../../api/productSimilarApi';
import ProductCard from '../ProductCard';
import ProductSimilar from '../ProductSimilar';
+import ProductPromoSection from '~/modules/product-promo/components/Section';
+
+const SELF_HOST = process.env.NEXT_PUBLIC_SELF_HOST;
const ProductDesktopVariant = ({
product,
@@ -33,6 +36,8 @@ const ProductDesktopVariant = ({
const auth = useAuth();
const { slug } = router.query;
+ console.log('ini product variant', product);
+
const [lowestPrice, setLowestPrice] = useState(null);
const [addCartAlert, setAddCartAlert] = useState(false);
@@ -40,11 +45,20 @@ const ProductDesktopVariant = ({
const { setRefreshCart } = useProductCartContext();
+ const [quantityInput, setQuantityInput] = useState(1);
+
+ const createdAskUrl = whatsappUrl({
+ template: 'product',
+ payload: {
+ manufacture: product.manufacture.name,
+ productName: product.name,
+ url: process.env.NEXT_PUBLIC_SELF_HOST + router.asPath,
+ },
+ fallbackUrl: router.asPath,
+ });
+
const getLowestPrice = useCallback(() => {
const lowest = product.price;
- /* const lowest = prices.reduce((lowest, price) => {
- return price.priceDiscount < lowest.priceDiscount ? price : lowest
- }, prices[0])*/
return lowest;
}, [product]);
@@ -77,7 +91,7 @@ const ProductDesktopVariant = ({
router.push(`/login?next=/shop/product/${slug}`);
return;
}
- const quantity = variantQuantityRefs.current[product.id].value;
+ const quantity = quantityInput;
if (!validQuantity(quantity)) return;
updateItemCart({
productId: product.id,
@@ -92,7 +106,7 @@ const ProductDesktopVariant = ({
};
const handleBuy = (variant) => {
- const quantity = variantQuantityRefs.current[product.id].value;
+ const quantity = quantityInput;
if (!validQuantity(quantity)) return;
updateItemCart({
@@ -160,87 +174,39 @@ const ProductDesktopVariant = ({
<Image
src={product.image + '?variant=True'}
alt={product.name}
- className='h-[430px] object-contain object-center w-full border border-gray_r-4'
+ className='w-full h-[350px]'
/>
</div>
- <div className='w-7/12 px-4'>
+ <div className='w-7/12 px-6'>
<h1 className='text-title-md leading-10 font-medium'>
{product?.name}
</h1>
<div className='mt-10'>
- <div className='flex p-3'>
- <div className='w-4/12 text-gray_r-12/70'>Nomor SKU</div>
- <div className='w-8/12'>SKU-{product.id}</div>
- </div>
<div className='flex p-3 bg-gray_r-4'>
- <div className='w-4/12 text-gray_r-12/70'>Part Number</div>
- <div className='w-8/12'>{product.code || '-'}</div>
+ <div className='w-4/12 text-gray_r-12/70'>Item Code</div>
+ <div className='w-8/12'>{product.code}</div>
</div>
- <div className='flex p-3'>
+ <div className='flex p-3 items-center '>
<div className='w-4/12 text-gray_r-12/70'>Manufacture</div>
<div className='w-8/12'>
- {product.manufacture?.name ? (
- <Link
- href={createSlug(
- '/shop/brands/',
- product.manufacture?.name,
- product.manufacture?.id
- )}
- >
- {product.manufacture?.name}
- </Link>
- ) : (
- <div>-</div>
- )}
- </div>
- </div>
-
- <div className='flex p-3 items-center bg-gray_r-4'>
- <div className='w-4/12 text-gray_r-12/70'>
- Persiapan Barang
- </div>
- <div className='w-8/12'>
- {!product?.sla && <Skeleton width='20%' height='16px' />}
- {product?.sla && (
- <Tooltip
- placement='top'
- label={`Masa Persiapan Barang ${product?.sla?.slaDate}`}
- >
- <Box className='w-fit flex items-center gap-x-2'>
- {product?.sla?.slaDate}
- <Info size={16} />
- </Box>
- </Tooltip>
- )}
+ <Link
+ href={createSlug(
+ '/shop/brands/',
+ product.manufacture.name,
+ product.manufacture.id.toString()
+ )}
+ >
+ <Image
+ width={100}
+ src={product.manufacture.logo}
+ alt={product.manufacture.name}
+ />
+ </Link>
</div>
</div>
- <div className='flex p-3'>
- <div className='w-4/12 text-gray_r-12/70'>Stock</div>
- <div className='w-8/12'>
- {!product?.sla && <Skeleton width='10%' height='16px' />}
- {product?.sla?.qty > 0 && <span>{product?.sla?.qty}</span>}
- {product?.sla?.qty == 0 && (
- <a
- href={whatsappUrl('product', {
- name: product.name,
- manufacture: product?.manufacture?.name,
- url: createSlug(
- '/shop/product/',
- product.name,
- product.id,
- true
- ),
- })}
- className='text-danger-500 font-medium'
- >
- Tanya Admin
- </a>
- )}
- </div>
- </div>
- <div className='flex p-3 bg-gray_r-4'>
+ <div className='flex p-3 bg-gray_r-4 '>
<div className='w-4/12 text-gray_r-12/70'>Berat Barang</div>
<div className='w-8/12'>
{product?.weight > 0 && <span>{product?.weight} KG</span>}
@@ -262,24 +228,55 @@ const ProductDesktopVariant = ({
)}
</div>
</div>
+ <div className='flex p-3 items-center '>
+ <div className='w-4/12 text-gray_r-12/70'>Terjual</div>
+ <div className='w-8/12'>-</div>
+ </div>
+
+ <div className='flex p-3 items-center bg-gray_r-4 '>
+ <div className='w-4/12 text-gray_r-12/70'>
+ Persiapan Barang
+ </div>
+ <div className='w-8/12'>
+ {!product?.sla && <Skeleton width='20%' height='16px' />}
+ {product?.sla && (
+ <Tooltip
+ placement='top'
+ label={`Masa Persiapan Barang ${product?.sla?.slaDate}`}
+ >
+ <Box className='w-fit flex items-center gap-x-2'>
+ {product?.sla?.slaDate}
+ <Info size={16} />
+ </Box>
+ </Tooltip>
+ )}
+ </div>
+ </div>
</div>
</div>
- <div className='p-4 md:p-6 md:bg-gray-50 rounded-xl'>
- <h2 className='text-h-md md:text-h-lg font-medium'>Informasi Produk</h2>
- <div className='h-4' />
- <div
- className='leading-relaxed text-gray-700'
- dangerouslySetInnerHTML={{
- __html:
- !product.parent.description || product.parent.description == '<p><br></p>'
- ? 'Belum ada deskripsi'
- : product.parent.description,
- }}
- />
+ <div className='p-4 md:p-6 '>
+ <ProductPromoSection product={product} productId={product.id} />
+
+ <div className='p-4 md:p-6 md:bg-gray-50 rounded-xl'>
+ <h2 className='text-h-md md:text-h-lg font-medium'>
+ Informasi Produk
+ </h2>
+ <div className='h-4' />
+ <div
+ className='leading-relaxed text-gray-700'
+ dangerouslySetInnerHTML={{
+ __html:
+ !product.parent.description ||
+ product.parent.description == '<p><br></p>'
+ ? 'Belum ada deskripsi'
+ : product.parent.description,
+ }}
+ />
+ </div>
</div>
</div>
- <div className='w-[25%]'>
+ <div className='w-[33%]'>
{product?.isFlashsale > 0 &&
product?.price?.discountPercentage > 0 ? (
<>
@@ -337,40 +334,120 @@ const ProductDesktopVariant = ({
)}
</h3>
)}
- <div className='flex gap-x-3 mt-4'>
- <input
- type='number'
- className='form-input w-16 py-2 text-center bg-gray_r-1'
- ref={setVariantQuantityRef(product.id)}
- defaultValue={1}
- />
- <button
- type='button'
+ <div className='flex justify-between items-center py-5 px-3'>
+ <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=' w-24 h-10 text-center border border-gray-300 rounded focus:outline-none'
+ />
+ <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>
+ <span
+ className={
+ product?.sla?.qty < 10 ? 'text-red-600 font-medium' : ''
+ }
+ >
+ {' '}
+ Stock : {product?.sla?.qty}{' '}
+ </span>
+ </div>
+ <div>
+ {product?.sla?.qty > 0 && (
+ <Link href='/panduan-pick-up-service' className='group'>
+ <Image
+ src='/images/PICKUP-NOW.png'
+ className='group-hover:scale-105 transition-transform duration-200 w-28'
+ alt='pickup now'
+ />
+ </Link>
+ )}
+ </div>
+ </div>
+ <div className='flex gap-x-3'>
+ <Button
onClick={() => handleAddToCart(product.id)}
- className='flex-1 py-2 btn-yellow'
+ className='w-full'
+ colorScheme='yellow'
>
Keranjang
- </button>
- <button
- type='button'
+ </Button>
+ <Button
onClick={() => handleBuy(product.id)}
- className='flex-1 py-2 btn-solid-red'
+ className='w-full'
+ colorScheme='red'
>
Beli
- </button>
+ </Button>
</div>
- <div className='flex mt-4'>
- <button
- className='flex items-center gap-x-1'
- onClick={toggleWishlist}
- >
- {wishlist.data?.productTotal > 0 ? (
- <HeartIcon className='w-6 fill-danger-500 text-danger-500' />
- ) : (
- <HeartIcon className='w-6' />
- )}
- Wishlist
- </button>
+ <div className='flex py-5'>
+ <div className='flex gap-x-5 items-center justify-center'>
+ <Button
+ as={Link}
+ href={createdAskUrl}
+ variant='link'
+ target='_blank'
+ colorScheme='gray'
+ leftIcon={<MessageCircleIcon size={18} />}
+ >
+ Ask Admin
+ </Button>
+
+ <span>|</span>
+
+ <button
+ className='flex items-center gap-x-1'
+ onClick={toggleWishlist}
+ >
+ {wishlist.data?.productTotal > 0 ? (
+ <HeartIcon className='w-6 fill-danger-500 text-danger-500' />
+ ) : (
+ <HeartIcon className='w-6' />
+ )}
+ Wishlist
+ </button>
+
+ <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} />}
+ >
+ Share
+ </Button>
+ </RWebShare>
+ </div>
</div>
<div className='border border-gray_r-6 overflow-auto mt-4'>
<div className='font-medium text-center p-4 bg-gray_r-1 border-b border-gray_r-6 sticky top-0 z-10'>
diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx
index bcb41dd6..f52aa5f7 100644
--- a/src/pages/_app.jsx
+++ b/src/pages/_app.jsx
@@ -85,7 +85,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }) {
return (
<SessionProvider session={session}>
<ScrollToTop />
-
+
<AnimatePresence>
{animateLoader && (
<motion.div
diff --git a/src/pages/api/shop/variant-detail.js b/src/pages/api/shop/variant-detail.js
index 08ce75b8..160ec979 100644
--- a/src/pages/api/shop/variant-detail.js
+++ b/src/pages/api/shop/variant-detail.js
@@ -1,4 +1,4 @@
-import { productMappingSolr, variantsMappingSolr } from '@/utils/solrMapping'
+import { variantsMappingSolr } from '@/utils/solrMapping'
import axios from 'axios'
export default async function handler(req, res) {
diff --git a/src/pages/shop/product/variant/[slug].jsx b/src/pages/shop/product/variant/[slug].jsx
index 42f38774..2c0dd64b 100644
--- a/src/pages/shop/product/variant/[slug].jsx
+++ b/src/pages/shop/product/variant/[slug].jsx
@@ -32,16 +32,9 @@ export async function getServerSideProps(context) {
tier
);
let product = response.data;
- // let product = await variantApi({ id: getIdFromSlug(slug), headers: { Token: authToken } })
if (product?.length == 1) {
product = product[0];
- /* const regexHtmlTags = /(<([^>]+)>)/gi
- const regexHtmlTagsExceptP = /<\/?(?!p\b)[^>]*>/g
- product.description = product.description
- .replace(regexHtmlTagsExceptP, ' ')
- .replace(regexHtmlTags, ' ')
- .trim()*/
} else {
product = null;
}
diff --git a/src/utils/solrMapping.js b/src/utils/solrMapping.js
index f73e966a..f896a6a8 100644
--- a/src/utils/solrMapping.js
+++ b/src/utils/solrMapping.js
@@ -74,6 +74,7 @@ export const productMappingSolr = (products, pricelist) => {
name: product.manufacture_name_s || '',
imagePromotion1: product.image_promotion_1_s || '',
imagePromotion2: product.image_promotion_2_s || '',
+ logo : product.x_logo_manufacture_s || '',
};
}
@@ -133,6 +134,7 @@ export const variantsMappingSolr = (parent, products, pricelist) => {
productMapped.manufacture = {
id: product.manufacture_id_i || '',
name: product.manufacture_name_s || '',
+ logo : parent[0]?.x_logo_manufacture_s
};
}
productMapped.parent = {