diff options
| author | it-fixcomart <it@fixcomart.co.id> | 2024-11-29 10:06:30 +0700 |
|---|---|---|
| committer | it-fixcomart <it@fixcomart.co.id> | 2024-11-29 10:06:30 +0700 |
| commit | c7177ab35e35abac0aa322b054ad10d5690bf8de (patch) | |
| tree | c3da3010e31a4fe453520dfc33fa5e0e52daa0b5 /src-migrate/modules | |
| parent | 88198c83806b69ce6ab7815cee5c139536849c9f (diff) | |
| parent | 952421c810b53ec4d25ad5ef605bae1bd1d5d616 (diff) | |
Merge branch 'new-release' into CR/quotation-noPo
Diffstat (limited to 'src-migrate/modules')
12 files changed, 873 insertions, 335 deletions
diff --git a/src-migrate/modules/cart/components/Item.tsx b/src-migrate/modules/cart/components/Item.tsx index 6ffbb524..ab2e7ce1 100644 --- a/src-migrate/modules/cart/components/Item.tsx +++ b/src-migrate/modules/cart/components/Item.tsx @@ -36,26 +36,29 @@ const CartItem = ({ item, editable = true, selfPicking}: Props) => { )} <div className='w-2' /> <div> - Selamat! Pembelian anda lebih hemat {' '} + Selamat! Pembelian anda lebih hemat{' '} <span className={style.savingAmt}> - Rp{formatCurrency((item.package_price || 0) * item.quantity - item.subtotal)} + Rp + {formatCurrency( + (item.package_price || 0) * item.quantity - item.subtotal + )} </span> </div> </div> )} <div className={style.mainProdWrapper}> - {editable && ( - <CartItemSelect item={item} /> - )} + {editable && <CartItemSelect item={item} />} <div className='w-4' /> <CartItem.Image item={item} /> <div className={style.details}> - {(item.is_in_bu) && (item.on_hand_qty >= item.quantity) && ( + {item?.available_quantity > 0 && ( <div className='text-[10px] text-red-500 italic'> - *Barang ini bisa di pickup maksimal pukul 16.00 + {item.quantity <= item?.available_quantity + ? '*Barang ini bisa di pickup maksimal pukul 16.00' + : `*${item?.available_quantity} Barang ini bisa di pickup maksimal pukul 16.00`} </div> )} <CartItem.Name item={item} /> diff --git a/src-migrate/modules/page-content/index.tsx b/src-migrate/modules/page-content/index.tsx index 3423ca8b..54ee0a04 100644 --- a/src-migrate/modules/page-content/index.tsx +++ b/src-migrate/modules/page-content/index.tsx @@ -10,28 +10,21 @@ type Props = { const PageContent = ({ path }: Props) => { const [localData, setData] = useState<PageContentProps>(); const [shouldFetch, setShouldFetch] = useState(false); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { - const localData = localStorage.getItem(`page-content:${path}`); - if (localData) { - setData(JSON.parse(localData)); - }else{ - setShouldFetch(true); - } - },[]) - - const { data, isLoading } = useQuery<PageContentProps>( - `page-content:${path}`, - async () => await getPageContent({ path }), { - enabled: shouldFetch, - onSuccess: (data) => { - if (data) { - localStorage.setItem(`page-content:${path}`, JSON.stringify(data)); - setData(data); - } - }, - } - ); + const fetchData = async () => { + setIsLoading(true); + const res = await fetch(`/api/page-content?path=${path}`); + const { data } = await res.json(); + if (data) { + setData(data); + } + setIsLoading(false); + }; + + fetchData(); + }, []); const parsedContent = useMemo<string>(() => { if (!localData) return ''; diff --git a/src-migrate/modules/product-card/components/ProductCard.tsx b/src-migrate/modules/product-card/components/ProductCard.tsx index 0febfadb..a439cdc8 100644 --- a/src-migrate/modules/product-card/components/ProductCard.tsx +++ b/src-migrate/modules/product-card/components/ProductCard.tsx @@ -1,95 +1,108 @@ -import style from '../styles/product-card.module.css' +import style from '../styles/product-card.module.css'; import ImageNext from 'next/image'; -import clsx from 'clsx' -import Link from 'next/link' -import React, { useEffect, useMemo, useState } from 'react' -import Image from '~/components/ui/image' -import useUtmSource from '~/hooks/useUtmSource' -import clsxm from '~/libs/clsxm' -import formatCurrency from '~/libs/formatCurrency' -import { formatToShortText } from '~/libs/formatNumber' -import { createSlug } from '~/libs/slug' -import { IProduct } from '~/types/product' - +import clsx from 'clsx'; +import Link from 'next/link'; +import React, { useEffect, useMemo, useState } from 'react'; +import Image from '~/components/ui/image'; +import useUtmSource from '~/hooks/useUtmSource'; +import clsxm from '~/libs/clsxm'; +import formatCurrency from '~/libs/formatCurrency'; +import { formatToShortText } from '~/libs/formatNumber'; +import { createSlug } from '~/libs/slug'; +import { IProduct } from '~/types/product'; +import useDevice from '@/core/hooks/useDevice'; type Props = { - product: IProduct - layout?: 'vertical' | 'horizontal' -} + product: IProduct; + layout?: 'vertical' | 'horizontal'; +}; const ProductCard = ({ product, layout = 'vertical' }: Props) => { - const utmSource = useUtmSource() - + const utmSource = useUtmSource(); + const { isDesktop, isMobile } = useDevice(); const URL = { - product: createSlug('/shop/product/', product.name, product.id.toString()) + `?utm_source=${utmSource}`, - manufacture: createSlug('/shop/brands/', product.manufacture.name, product.manufacture.id.toString()), - } + product: + createSlug('/shop/product/', product.name, product.id.toString()) + + `?utm_source=${utmSource}`, + manufacture: createSlug( + '/shop/brands/', + product.manufacture.name, + product.manufacture.id.toString() + ), + }; const image = useMemo(() => { - if (product.image) return product.image + '?ratio=square' - return '/images/noimage.jpeg' - }, [product.image]) + if (!isDesktop && product.image_mobile) { + return product.image_mobile + '?ratio=square'; + } else { + if (product.image) return product.image + '?ratio=square'; + return '/images/noimage.jpeg'; + } + }, [product.image, product.image_mobile]); return ( - <div className={clsxm(style['wrapper'], { - [style['wrapper-v']]: layout === 'vertical', - [style['wrapper-h']]: layout === 'horizontal', - })} + <div + className={clsxm(style['wrapper'], { + [style['wrapper-v']]: layout === 'vertical', + [style['wrapper-h']]: layout === 'horizontal', + })} > - <div className={clsxm('relative', { - [style['image-v']]: layout === 'vertical', - [style['image-h']]: layout === 'horizontal', - })}> + <div + className={clsxm('relative', { + [style['image-v']]: layout === 'vertical', + [style['image-h']]: layout === 'horizontal', + })} + > <Link href={URL.product}> - - <div className="relative"> - <Image - src={image} - alt={product.name} - width={128} - height={128} - className='object-contain object-center h-full w-full' - /> - <div className="absolute top-0 right-0 flex mt-2"> - <div className="gambarB "> - {product.isSni && ( - <ImageNext - src="/images/sni-logo.png" - alt="SNI Logo" - className="w-3 h-4 object-contain object-top sm:h-4" - width={50} - height={50} - /> - )} - </div> - <div className="gambarC "> - {product.isTkdn && ( - <ImageNext - src="/images/TKDN.png" - alt="TKDN" - className="w-5 h-4 object-contain object-top ml-1 mr-1 sm:h-6" - width={50} - height={50} - /> - )} + <div className='relative'> + <Image + src={image} + alt={product.name} + width={128} + height={128} + className='object-contain object-center h-full w-full' + /> + <div className='absolute top-0 right-0 flex mt-2'> + <div className='gambarB '> + {product.isSni && ( + <ImageNext + src='/images/sni-logo.png' + alt='SNI Logo' + className='w-3 h-4 object-contain object-top sm:h-4' + width={50} + height={50} + /> + )} + </div> + <div className='gambarC '> + {product.isTkdn && ( + <ImageNext + src='/images/TKDN.png' + alt='TKDN' + className='w-5 h-4 object-contain object-top ml-1 mr-1 sm:h-6' + width={50} + height={50} + /> + )} + </div> </div> </div> - </div> {product.variant_total > 1 && ( - <div className={style['variant-badge']}>{product.variant_total} Varian</div> + <div className={style['variant-badge']}> + {product.variant_total} Varian + </div> )} </Link> </div> - <div className={clsxm({ - [style['content-v']]: layout === 'vertical', - [style['content-h']]: layout === 'horizontal', - })}> - <Link - href={URL.manufacture} - className={style['brand']} - > + <div + className={clsxm({ + [style['content-v']]: layout === 'vertical', + [style['content-h']]: layout === 'horizontal', + })} + > + <Link href={URL.manufacture} className={style['brand']}> {product.manufacture.name} </Link> @@ -113,17 +126,15 @@ const ProductCard = ({ product, layout = 'vertical' }: Props) => { <div className='h-1.5' /> <div className={style['price-inc']}> - Inc PPN: - Rp {formatCurrency(Math.round(product.lowest_price.price * 1.11))} + Inc PPN: Rp{' '} + {formatCurrency(Math.round(product.lowest_price.price * 1.11))} </div> <div className='h-1' /> <div className='flex items-center gap-x-2.5'> {product.stock_total > 0 && ( - <div className={style['ready-stock']}> - Ready Stock - </div> + <div className={style['ready-stock']}>Ready Stock</div> )} {product.qty_sold > 0 && ( <div className={style['sold']}> @@ -131,14 +142,11 @@ const ProductCard = ({ product, layout = 'vertical' }: Props) => { </div> )} </div> - </div> </div> - ) -} - -const classPrefix = ({ layout }: Props) => { + ); +}; -} +const classPrefix = ({ layout }: Props) => {}; -export default ProductCard
\ No newline at end of file +export default ProductCard; diff --git a/src-migrate/modules/product-detail/components/AddToQuotation.tsx b/src-migrate/modules/product-detail/components/AddToQuotation.tsx new file mode 100644 index 00000000..f9b6c2b3 --- /dev/null +++ b/src-migrate/modules/product-detail/components/AddToQuotation.tsx @@ -0,0 +1,227 @@ +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 { ArrowDownTrayIcon } from '@heroicons/react/24/outline'; +import product from 'next-seo/lib/jsonld/product'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import Image from '~/components/ui/image'; +import { getAuth } from '~/libs/auth'; +import { upsertUserCart } from '~/services/cart'; +import LazyLoad from 'react-lazy-load'; +import ProductSimilar from '../../../../src/lib/product/components/ProductSimilar'; +import { IProductDetail } from '~/types/product'; +import ImageNext from 'next/image'; +import { useProductCartContext } from '@/contexts/ProductCartContext'; +import { createSlug } from '~/libs/slug'; +import formatCurrency from '~/libs/formatCurrency'; +import { useProductDetail } from '../stores/useProductDetail'; + +type Props = { + variantId: number | null; + quantity?: number; + source?: 'buy' | 'add_to_cart'; + products: IProductDetail; +}; + +type Status = 'idle' | 'loading' | 'success'; + +const AddToQuotation = ({ + variantId, + quantity = 1, + source = 'add_to_cart', + products, +}: Props) => { + const auth = getAuth(); + const router = useRouter(); + const toast = useToast({ + position: 'top', + isClosable: true, + }); + + const { askAdminUrl } = useProductDetail(); + + const [product, setProducts] = useState(products); + const [status, setStatus] = useState<Status>('idle'); + const { + productCart, + setRefreshCart, + setProductCart, + refreshCart, + isLoading, + setIsloading, + } = useProductCartContext(); + + const productSimilarQuery = [ + product?.name, + `fq=-product_id_i:${product.id}`, + `fq=-manufacture_id_i:${product.manufacture?.id || 0}`, + ].join('&'); + const [addCartAlert, setAddCartAlert] = useState(false); + + const handleButton = async () => { + if (typeof auth !== 'object') { + const currentUrl = encodeURIComponent(router.asPath); + router.push(`/login?next=${currentUrl}`); + return; + } + + if (!variantId || isNaN(quantity) || typeof auth !== 'object') return; + if (status === 'success') return; + setStatus('loading'); + await upsertUserCart({ + userId: auth.id, + type: 'product', + id: variantId, + qty: quantity, + selected: true, + source: source, + qtyAppend: true, + }); + setStatus('idle'); + setRefreshCart(true); + setAddCartAlert(true); + + toast({ + title: 'Tambah ke keranjang', + description: 'Berhasil menambahkan barang ke keranjang belanja', + status: 'success', + duration: 3000, + isClosable: true, + position: 'top', + }); + + if (source === 'buy') { + router.push('/shop/quotation?source=buy'); + } + }; + useEffect(() => { + if (status === 'success') + setTimeout(() => { + setStatus('idle'); + }, 3000); + }, [status]); + + const btnConfig = { + add_to_cart: { + colorScheme: 'yellow', + + text: 'Keranjang', + }, + buy: { + colorScheme: 'red', + text: 'Beli', + }, + }; + + return ( + <div className='w-full'> + <Button + onClick={handleButton} + color={'red'} + colorScheme='white' + className='w-full border-2 p-2 gap-1 hover:bg-slate-100 flex items-center' + > + <ImageNext + src='/images/writing.png' + alt='penawaran instan' + className='' + width={25} + height={25} + /> + Penawaran Harga Instan + </Button> + <BottomPopup + className='!container' + title='Berhasil Ditambahkan' + active={addCartAlert} + 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() + )} + 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> + <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> + </BottomPopup> + </div> + ); +}; + +export default AddToQuotation; diff --git a/src-migrate/modules/product-detail/components/Image.tsx b/src-migrate/modules/product-detail/components/Image.tsx index 30ca0d34..96ae2027 100644 --- a/src-migrate/modules/product-detail/components/Image.tsx +++ b/src-migrate/modules/product-detail/components/Image.tsx @@ -1,22 +1,22 @@ import style from '../styles/image.module.css'; import ImageNext from 'next/image'; -import React, { useEffect, useMemo, useState } from 'react' -import { InfoIcon } from 'lucide-react' -import { Tooltip } from '@chakra-ui/react' +import React, { useEffect, useMemo, useState } from 'react'; +import { InfoIcon } from 'lucide-react'; +import { Tooltip } from '@chakra-ui/react'; -import { IProductDetail } from '~/types/product' -import ImageUI from '~/components/ui/image' +import { IProductDetail } from '~/types/product'; +import ImageUI from '~/components/ui/image'; import moment from 'moment'; - +import useDevice from '@/core/hooks/useDevice'; type Props = { - product: IProductDetail -} + product: IProductDetail; +}; const Image = ({ product }: Props) => { - const flashSale = product.flash_sale + const flashSale = product.flash_sale; const [count, setCount] = useState(flashSale?.remaining_time || 0); - + const { isDesktop, isMobile } = useDevice(); useEffect(() => { let interval: NodeJS.Timeout; @@ -34,59 +34,60 @@ const Image = ({ product }: Props) => { }; }, [flashSale?.remaining_time]); - const duration = moment.duration(count, 'seconds') - + const duration = moment.duration(count, 'seconds'); const image = useMemo(() => { - if (product.image) return product.image + '?ratio=square' - return '/images/noimage.jpeg' - }, [product.image]) + if (!isDesktop && product.image_mobile) { + return product.image_mobile + '?ratio=square'; + } else { + if (product.image) return product.image + '?ratio=square'; + return '/images/noimage.jpeg'; + } + }, [product.image, product.image_mobile]); return ( <div className={style['wrapper']}> {/* <div className="relative"> */} - <ImageUI - src={image} - alt={product.name} - width={256} - height={256} - className={style['image']} - loading='eager' - priority - /> - <div className="absolute top-4 right-10 flex "> - <div className="gambarB "> - {product.isSni && ( - <ImageNext - src="/images/sni-logo.png" - alt="SNI Logo" - className="w-12 h-8 object-contain object-top sm:h-6" - width={50} - height={50} - /> - )} - </div> - <div className="gambarC "> - {product.isTkdn && ( - <ImageNext - src="/images/TKDN.png" - alt="TKDN" - className="w-16 h-8 object-contain object-top ml-1 mr-1 sm:h-6" - width={50} - height={50} - /> - )} - </div> - </div> - {/* </div> */} - - + <ImageUI + src={image} + alt={product.name} + width={256} + height={256} + className={style['image']} + loading='eager' + priority + /> + <div className='absolute top-4 right-10 flex '> + <div className='gambarB '> + {product.isSni && ( + <ImageNext + src='/images/sni-logo.png' + alt='SNI Logo' + className='w-12 h-8 object-contain object-top sm:h-6' + width={50} + height={50} + /> + )} + </div> + <div className='gambarC '> + {product.isTkdn && ( + <ImageNext + src='/images/TKDN.png' + alt='TKDN' + className='w-16 h-8 object-contain object-top ml-1 mr-1 sm:h-6' + width={50} + height={50} + /> + )} + </div> + </div> + {/* </div> */} <div className={style['absolute-info']}> <Tooltip placement='bottom-end' label='Gambar atau foto berperan sebagai ilustrasi produk. Kadang tidak sesuai dengan kondisi terbaru dengan berbagai perubahan dan perbaikan. Hubungi admin kami untuk informasi yang lebih baik perihal gambar.' > - <div className="text-gray-600"> + <div className='text-gray-600'> <InfoIcon size={20} /> </div> </Tooltip> @@ -94,7 +95,7 @@ const Image = ({ product }: Props) => { {flashSale.remaining_time > 0 && ( <div className='absolute bottom-0 w-full h-14'> - <div className="relative w-full h-full"> + <div className='relative w-full h-full'> <ImageUI src='/images/BG-FLASH-SALE.jpg' alt='Flash Sale Indoteknik' @@ -105,7 +106,9 @@ const Image = ({ product }: Props) => { <div className={style['flashsale']}> <div className='flex items-center gap-x-3'> - <div className={style['disc-badge']}>{Math.floor(product.lowest_price.discount_percentage)}%</div> + <div className={style['disc-badge']}> + {Math.floor(product.lowest_price.discount_percentage)}% + </div> <div className={style['flashsale-text']}> <ImageUI src='/images/ICON_FLASH_SALE_WEBSITE_INDOTEKNIK.svg' @@ -122,12 +125,11 @@ const Image = ({ product }: Props) => { <span>{duration.seconds().toString().padStart(2, '0')}</span> </div> </div> - </div> </div> )} </div> - ) -} + ); +}; -export default Image
\ No newline at end of file +export default Image; diff --git a/src-migrate/modules/product-detail/components/Information.tsx b/src-migrate/modules/product-detail/components/Information.tsx index 75ae3c41..5e1ea186 100644 --- a/src-migrate/modules/product-detail/components/Information.tsx +++ b/src-migrate/modules/product-detail/components/Information.tsx @@ -1,56 +1,232 @@ -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 [disableFilter, setDisableFilter] = useState<boolean>(false); + 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(() => { + if (selectedVariant) { + getsla(); + setInputValue( + selectedVariant?.code + + (selectedVariant?.attributes[0] + ? ' - ' + selectedVariant?.attributes[0] + : '') + ); + } + }, [selectedVariant]); + + const handleOnChange = (vals: any) => { + setDisableFilter(true); + let code = vals.replace(/\s-\s.*$/, '').trim(); + 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) => { + setDisableFilter(false); + 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?.variants?.length} Variants + </span>{' '} + </label> + <AutoComplete + disableFilter={disableFilter} + openOnFocus + className='form-input' + onChange={(vals) => handleOnChange(vals)} + > + <InputGroup> + <AutoCompleteInput + ref={inputRef} + value={inputValue as string} + onChange={(e) => handleOnKeyUp(e)} + onFocus={() => setDisableFilter(true)} + /> + <InputRightElement className='mr-4'> + <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} + {product?.manufacture.logo ? ( + <Image + height={50} + width={100} + src={product.manufacture.logo} + alt={product.manufacture.name} + className='h-8 object-fit' + /> + ) : ( + <p className='font-bold text-red-500'> + {product.manufacture.name} + </p> + )} </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 9021264e..0b27b1b3 100644 --- a/src-migrate/modules/product-detail/components/PriceAction.tsx +++ b/src-migrate/modules/product-detail/components/PriceAction.tsx @@ -1,12 +1,17 @@ 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, useState } from 'react'; import formatCurrency from '~/libs/formatCurrency'; import { IProductDetail } from '~/types/product'; import { useProductDetail } from '../stores/useProductDetail'; import AddToCart from './AddToCart'; -import Link from 'next/link'; +import AddToQuotation from './AddToQuotation'; import { getAuth } from '~/libs/auth'; +import useDevice from '@/core/hooks/useDevice'; +import odooApi from '~/libs/odooApi'; +import { Button, Skeleton } from '@chakra-ui/react'; type Props = { product: IProductDetail; @@ -22,27 +27,58 @@ const PriceAction = ({ product }: Props) => { askAdminUrl, isApproval, setIsApproval, + selectedVariant, + sla, } = useProductDetail(); - + const [qtyPickUp, setQtyPickUp] = useState(0); + const { isDesktop, isMobile } = useDevice(); 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]); + useEffect(() => { + const fetchData = async () => { + const qty_available = await odooApi( + 'GET', + `/api/v1/product_variant/${selectedVariant.id}/qty_available` + ); + + setQtyPickUp(qty_available?.qty); + }; + fetchData(); + }, [selectedVariant]); + useEffect(() => { + setQuantityInput('1'); + }, [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 ${ + isMobile && + 'pb-6 pt-6 rounded-lg shadow-[rgba(0,0,4,0.1)_0px_-4px_4px_0px] ' + }`} id='price-section' > {!!activePrice && activePrice.price > 0 && ( @@ -84,18 +120,69 @@ 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> + <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> + {selectedVariant?.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> + {qtyPickUp > 0 && ( + <div className='text-[12px] mt-1 text-red-500 italic'> + * {qtyPickUp} barang bisa di pickup + </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} @@ -110,6 +197,14 @@ const PriceAction = ({ product }: Props) => { /> )} </div> + <div className='mt-4'> + <AddToQuotation + source='buy' + products={product} + variantId={activeVariantId} + quantity={Number(quantityInput)} + /> + </div> </div> ); }; 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/modules/register/components/TermCondition.tsx b/src-migrate/modules/register/components/TermCondition.tsx index d54fe921..44275917 100644 --- a/src-migrate/modules/register/components/TermCondition.tsx +++ b/src-migrate/modules/register/components/TermCondition.tsx @@ -1,34 +1,48 @@ -import { Checkbox } from '@chakra-ui/react' -import React from 'react' -import { Modal } from '~/components/ui/modal' -import { useRegisterStore } from "../stores/useRegisterStore"; -import PageContent from '~/modules/page-content' +import { Checkbox } from '@chakra-ui/react'; +import React from 'react'; +import { Modal } from '~/components/ui/modal'; +import { useRegisterStore } from '../stores/useRegisterStore'; + +import dynamic from 'next/dynamic'; +const PageContent = dynamic( + () => import('@/lib/content/components/PageContent') +); const TermCondition = () => { - const { isOpenTNC, closeTNC, isCheckedTNC, toggleCheckTNC, openTNC } = useRegisterStore() + const { isOpenTNC, closeTNC, isCheckedTNC, toggleCheckTNC, openTNC } = + useRegisterStore(); return ( <> - <div className="mt-4 flex items-center gap-x-2"> - <Checkbox id='tnc' name='tnc' colorScheme='red' isChecked={isCheckedTNC} onChange={toggleCheckTNC} /> + <div className='mt-4 flex items-center gap-x-2'> + <Checkbox + id='tnc' + name='tnc' + colorScheme='red' + isChecked={isCheckedTNC} + onChange={toggleCheckTNC} + /> <div> - <label htmlFor="tnc" className="cursor-pointer">Dengan ini saya menyetujui</label> - {' '} + <label htmlFor='tnc' className='cursor-pointer'> + Dengan ini saya menyetujui + </label>{' '} <span - className="font-medium text-danger-500 cursor-pointer" + className='font-medium text-danger-500 cursor-pointer' onClick={openTNC} > syarat dan ketentuan </span> - <label htmlFor="tnc" className="ml-2 cursor-pointer">yang berlaku</label> + <label htmlFor='tnc' className='ml-2 cursor-pointer'> + yang berlaku + </label> </div> </div> - <Modal active={isOpenTNC} close={closeTNC} > - <PageContent path='/register#tnd' /> + <Modal active={isOpenTNC} close={closeTNC}> + <PageContent path='/registerTnd' /> </Modal> </> - ) -} + ); +}; -export default TermCondition
\ No newline at end of file +export default TermCondition; |
