diff options
Diffstat (limited to 'src-migrate')
6 files changed, 346 insertions, 59 deletions
diff --git a/src-migrate/modules/page-content/index.tsx b/src-migrate/modules/page-content/index.tsx index edecb855..3423ca8b 100644 --- a/src-migrate/modules/page-content/index.tsx +++ b/src-migrate/modules/page-content/index.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useQuery } from 'react-query'; import { PageContentProps } from '~/types/pageContent'; import { getPageContent } from '~/services/pageContent'; @@ -8,18 +8,38 @@ type Props = { }; const PageContent = ({ path }: Props) => { + const [localData, setData] = useState<PageContentProps>(); + const [shouldFetch, setShouldFetch] = 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 }) + async () => await getPageContent({ path }), { + enabled: shouldFetch, + onSuccess: (data) => { + if (data) { + localStorage.setItem(`page-content:${path}`, JSON.stringify(data)); + setData(data); + } + }, + } ); const parsedContent = useMemo<string>(() => { - if (!data) return ''; - return data.content.replaceAll( + if (!localData) return ''; + return localData.content.replaceAll( 'src="/web/image', `src="${process.env.NEXT_PUBLIC_ODOO_API_HOST}/web/image` ); - }, [data]); + }, [localData]); if (isLoading) return <PageContentSkeleton />; return <div dangerouslySetInnerHTML={{ __html: parsedContent || '' }}></div>; 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..09a7501d --- /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 hover:bg-slate-100 flex items-center' + > + <ImageNext + src='/images/writing.png' + alt='penawaran instan' + className='w-fit h-fit p-1' + width={20} + height={20} + /> + 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/PriceAction.tsx b/src-migrate/modules/product-detail/components/PriceAction.tsx index 3544fa26..a3126cdd 100644 --- a/src-migrate/modules/product-detail/components/PriceAction.tsx +++ b/src-migrate/modules/product-detail/components/PriceAction.tsx @@ -7,6 +7,9 @@ import formatCurrency from '~/libs/formatCurrency'; import { IProductDetail } from '~/types/product'; import { useProductDetail } from '../stores/useProductDetail'; import AddToCart from './AddToCart'; +import AddToQuotation from './AddToQuotation'; +import Link from 'next/link'; +import { getAuth } from '~/libs/auth'; type Props = { product: IProductDetail; @@ -160,6 +163,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/promo/components/FlashSaleNonDisplay.tsx b/src-migrate/modules/promo/components/FlashSaleNonDisplay.tsx new file mode 100644 index 00000000..5685b83a --- /dev/null +++ b/src-migrate/modules/promo/components/FlashSaleNonDisplay.tsx @@ -0,0 +1,17 @@ +import dynamic from 'next/dynamic'; +import React from 'react'; +import { FlashSaleSkeleton } from '@/lib/flashSale/skeleton/FlashSaleSkeleton'; +const FlashSaleNonDisplay = dynamic( + () => import('@/lib/flashSale/components/FlashSaleNonDisplay'), + { + loading: () => <FlashSaleSkeleton />, + } +); +const FlashSalePromo = () => { + return ( + <> + <FlashSaleNonDisplay /> + </> + ); +}; +export default FlashSalePromo; diff --git a/src-migrate/modules/promo/components/PromoList.tsx b/src-migrate/modules/promo/components/PromoList.tsx index d59d1867..9f808718 100644 --- a/src-migrate/modules/promo/components/PromoList.tsx +++ b/src-migrate/modules/promo/components/PromoList.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; -import { Button, Skeleton } from '@chakra-ui/react' -import clsxm from "~/libs/clsxm" +import { Button, Skeleton } from '@chakra-ui/react'; +import clsxm from '~/libs/clsxm'; import ProductPromoCard from '../../product-promo/components/Card'; import { fetchPromoItemsSolr } from '../../../../src/api/promoApi'; import { Swiper, SwiperSlide } from 'swiper/react'; @@ -8,7 +8,7 @@ import SwiperCore, { Navigation, Pagination } from 'swiper'; import useDevice from '@/core/hooks/useDevice'; import LogoSpinner from '../../../../src/core/components/elements/Spinner/LogoSpinner'; import usePromoStore from './promoStore'; -import Link from "next/link" +import Link from 'next/link'; import { IPromotion } from '~/types/promotion'; interface PromoListProps { selectedPromo: string; // Tipe selectedPromo ditetapkan sebagai string @@ -32,11 +32,11 @@ const PromoList: React.FC<PromoListProps> = ({ selectedPromo }) => { const swiperBanner = { modules: [Navigation], - className: 'h-[400px] w-full', + className: 'h-full w-full', slidesPerView: isMobile ? 1.1 : 3.25, spaceBetween: 10, - navigation:isMobile? true : false, - allowTouchMove:isMobile? false : true, + navigation: isMobile ? true : false, + allowTouchMove: isMobile ? false : true, }; useEffect(() => { @@ -56,7 +56,7 @@ const PromoList: React.FC<PromoListProps> = ({ selectedPromo }) => { const fetchPromotions = async () => { setIsLoading(true); try { - const items = await fetchPromoItemsSolr(`type_value_s:${slug}`, 0, 10); + const items = await fetchPromoItemsSolr(`type_value_s:${slug}`, 0, 10); setPromoItems(items); const promoDataPromises = items?.map(async (item) => { @@ -69,9 +69,11 @@ const PromoList: React.FC<PromoListProps> = ({ selectedPromo }) => { }); const promoDataArray = await Promise.all(promoDataPromises); - const mergedPromoData = promoDataArray?.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []); + const mergedPromoData = promoDataArray?.reduce( + (accumulator, currentValue) => accumulator.concat(currentValue), + [] + ); setPromoData(mergedPromoData); - } catch (error) { console.error('Error fetching promo items:', error); } finally { @@ -92,44 +94,49 @@ const PromoList: React.FC<PromoListProps> = ({ selectedPromo }) => { <div className='flex justify-between items-center'> <h1 className='text-h-sm md:text-h-lg font-semibold py-4'>{title}</h1> <div> - <Link href={`/shop/promo/${slug}`} className='!text-red-500 font-semibold'> + <Link + href={`/shop/promo/${slug}`} + className='!text-red-500 font-semibold' + > Lihat Semua </Link> </div> </div> {isLoading ? ( - <div className="loading-spinner flex justify-center"> + <div className='loading-spinner flex justify-center'> <LogoSpinner width={48} height={48} /> </div> ) : ( <Skeleton - isLoaded={!isLoading} - className={clsxm( - "flex gap-x-4 overflow-x-auto px-4 md:px-0", { - "min-h-[340px]": promoData[0] && promoData?.length > 0 - })} - > - {isDesktop && ( - <Swiper {...swiperBanner}> - {promoData?.map((promotion: IPromotion) => ( - <SwiperSlide key={promotion.id}> - <div className="min-w-36 max-w-[400px] mb-[20px] sm:w-full md:w-full lg:w-full xl:w-full"> - <ProductPromoCard product={promoItems} promotion={promotion} /> - </div> - </SwiperSlide> - ))} - </Swiper> - )} - {isMobile && (promoData?.map((promotion: IPromotion) => ( - <div key={promotion.id} className="min-w-[400px] max-w-[400px]"> - <ProductPromoCard product={promoItems} promotion={promotion} /> - </div> - )))} - - </Skeleton> + isLoaded={!isLoading} + className={clsxm('flex gap-x-4 overflow-x-auto px-4 md:px-0', { + 'min-h-[340px]': promoData[0] && promoData?.length > 0, + })} + > + {isDesktop && ( + <Swiper {...swiperBanner}> + {promoData?.map((promotion: IPromotion) => ( + <SwiperSlide key={promotion.id}> + <div className='min-w-36 max-w-[400px] mb-[20px] sm:w-full md:w-full lg:w-full xl:w-full'> + <ProductPromoCard + product={promoItems} + promotion={promotion} + /> + </div> + </SwiperSlide> + ))} + </Swiper> + )} + {isMobile && + promoData?.map((promotion: IPromotion) => ( + <div key={promotion.id} className='min-w-[400px] max-w-[400px]'> + <ProductPromoCard product={promoItems} promotion={promotion} /> + </div> + ))} + </Skeleton> )} </div> ); }; -export default PromoList;
\ No newline at end of file +export default PromoList; diff --git a/src-migrate/pages/shop/promo/index.tsx b/src-migrate/pages/shop/promo/index.tsx index febe31a4..689c2537 100644 --- a/src-migrate/pages/shop/promo/index.tsx +++ b/src-migrate/pages/shop/promo/index.tsx @@ -1,13 +1,14 @@ -import dynamic from 'next/dynamic' -import React, { useState } from 'react' -import { LazyLoadComponent } from 'react-lazy-load-image-component' -import Hero from '~/modules/promo/components/Hero' -import PromotionProgram from '~/modules/promo/components/PromotinProgram' -import Voucher from '~/modules/promo/components/Voucher' -import FlashSale from '../../../modules/promo/components/FlashSale' -const PromoList = dynamic(() => import('../../../modules/promo/components/PromoList')); - - +import dynamic from 'next/dynamic'; +import React, { useState } from 'react'; +import { LazyLoadComponent } from 'react-lazy-load-image-component'; +import Hero from '~/modules/promo/components/Hero'; +import PromotionProgram from '~/modules/promo/components/PromotinProgram'; +import Voucher from '~/modules/promo/components/Voucher'; +import FlashSale from '../../../modules/promo/components/FlashSale'; +import FlashSaleNonDisplay from '../../../modules/promo/components/FlashSaleNonDisplay'; +const PromoList = dynamic( + () => import('../../../modules/promo/components/PromoList') +); const PromoPage = () => { const [selectedPromo, setSelectedPromo] = useState('Bundling'); @@ -17,22 +18,26 @@ const PromoPage = () => { <Hero /> </LazyLoadComponent> <LazyLoadComponent> - <PromotionProgram - selectedPromo={selectedPromo} - onSelectPromo={setSelectedPromo} - /> + <PromotionProgram + selectedPromo={selectedPromo} + onSelectPromo={setSelectedPromo} + /> <PromoList selectedPromo={selectedPromo} /> </LazyLoadComponent> - + <LazyLoadComponent> <FlashSale /> </LazyLoadComponent> <h1 className='h-1'></h1> <LazyLoadComponent> + <FlashSaleNonDisplay /> + </LazyLoadComponent> + <h1 className='h-1'></h1> + <LazyLoadComponent> <Voucher /> </LazyLoadComponent> </> - ) -} + ); +}; -export default PromoPage
\ No newline at end of file +export default PromoPage; |
