diff options
| -rw-r--r-- | package.json | 3 | ||||
| -rw-r--r-- | src-migrate/modules/product-detail/components/Information.tsx | 126 | ||||
| -rw-r--r-- | src-migrate/modules/product-detail/components/PriceAction.tsx | 45 | ||||
| -rw-r--r-- | src-migrate/modules/product-detail/components/ProductDetail.tsx | 194 | ||||
| -rw-r--r-- | src-migrate/modules/product-detail/stores/useProductDetail.ts | 12 | ||||
| -rw-r--r-- | src/pages/_app.jsx | 2 |
6 files changed, 242 insertions, 140 deletions
diff --git a/package.json b/package.json index 28fbc5d8..98d4077c 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@chakra-ui/next-js": "^2.1.5", "@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", @@ -25,7 +26,7 @@ "clsx": "^2.0.0", "cookies-next": "^2.1.1", "flowbite": "^1.6.4", - "framer-motion": "^7.10.3", + "framer-motion": "^11.3.28", "http-proxy-middleware": "^3.0.0", "lodash-contrib": "^4.1200.1", "lucide-react": "^0.279.0", diff --git a/src-migrate/modules/product-detail/components/Information.tsx b/src-migrate/modules/product-detail/components/Information.tsx index 75ae3c41..5d70e534 100644 --- a/src-migrate/modules/product-detail/components/Information.tsx +++ b/src-migrate/modules/product-detail/components/Information.tsx @@ -1,56 +1,130 @@ -import style from '../styles/information.module.css' +import style from '../styles/information.module.css'; +import { + AutoComplete, + AutoCompleteInput, + AutoCompleteItem, + AutoCompleteList, +} from '@choc-ui/chakra-autocomplete'; -import React from 'react' -import dynamic from 'next/dynamic' -import Link from 'next/link' -import { useQuery } from 'react-query' +import React, { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import Link from 'next/link'; +import { useQuery } from 'react-query'; -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 { 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 { Icon, InputGroup, InputRightElement } from '@chakra-ui/react'; +import { CheckIcon, ChevronDownIcon, FingerPrintIcon } from '@heroicons/react/24/outline'; +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 { selectedVariant, setSelectedVariant, setSla, setActive, } = useProductDetail() + const variantOptions = product?.variants; + const querySLA = useQuery<IProductVariantSLA>({ - queryKey: ['variant-sla', product.variants[0]?.id], - queryFn: () => getVariantSLA(product.variants[0].id), - enabled: product.variant_total === 1 - }) + queryKey: ['variant-sla', selectedVariant?.id], + queryFn: () => getVariantSLA(selectedVariant?.id), + enabled: selectedVariant?.id === 1, + }); + const sla = querySLA?.data; + + useEffect(() => { + setSla(querySLA?.data); + }, [selectedVariant]); - const sla = querySLA?.data + const handleOnChange = (vals: any) => { + let code = vals.split(" ")[0]; + let variant = variantOptions.find((item) => item.code === code); + setSelectedVariant(variant); + } return ( <div className={style['wrapper']}> + <div className='realtive mb-5'> + <label className='form-label mb-2 text-lg'>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 value={selectedVariant?.code + ' - ' + selectedVariant?.attributes[0]} /> + <InputRightElement> + <ChevronDownIcon className='h-6 w-6 text-gray-500' /> + </InputRightElement> + </InputGroup> + + <AutoCompleteList> + {variantOptions.map((option, cid) => ( + <AutoCompleteItem + key={`option-${cid}`} + value={option.code + ' - ' + option.attributes[0]} + textTransform='capitalize' + > + <div className='flex gap-x-2 justify-between w-full'> + <div className='text-small'> + {option.code + ' - ' + option.attributes[0]} + </div> + <div className='grid grid-cols-3 items-start'> + <div className='badge-solid-red text-xs'> + {Math.floor(option?.price?.discount_percentage)}% + </div> + <div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'> + {currencyFormat(option?.price?.price)} + </div> + <div className='text-danger-500 font-semibold mb-2'> + {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())} + href={createSlug( + '/shop/brands/', + product.manufacture.name, + product.manufacture.id.toString() + )} className='text-danger-500 hover:underline' > {product.manufacture.name} </Link> - ) : '-'} + ) : ( + '-' + )} </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 81271f6e..405eb12b 100644 --- a/src-migrate/modules/product-detail/components/PriceAction.tsx +++ b/src-migrate/modules/product-detail/components/PriceAction.tsx @@ -22,23 +22,22 @@ 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]); return ( <div @@ -84,18 +83,26 @@ const PriceAction = ({ product }: Props) => { )} <div className='h-4' /> + <div className='flex gap-x-5 items-center'> + <div> + <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> + <div> + <span className={sla?.qty < 10 ? 'text-red-600 font-medium' : ''} > Stock : {sla?.qty} </span> + </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']} - /> <AddToCart variantId={activeVariantId} quantity={Number(quantityInput)} diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx index fad35a7d..0997466c 100644 --- a/src-migrate/modules/product-detail/components/ProductDetail.tsx +++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx @@ -1,46 +1,53 @@ -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 { 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'; 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 +55,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' /> <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> @@ -129,7 +103,9 @@ const ProductDetail = ({ product }: Props) => { )} <div className='h-4 md:h-10' /> - {!!activeVariantId && !isApproval && <ProductPromoSection productId={activeVariantId} />} + {!!activeVariantId && !isApproval && ( + <ProductPromoSection productId={activeVariantId} /> + )} <div className={style['section-card']}> <h2 className={style['heading']}> @@ -142,27 +118,61 @@ const ProductDetail = ({ product }: Props) => { <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 +181,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 +193,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/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 |
