diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/core/utils/googleTag.js | 2 | ||||
| -rw-r--r-- | src/lib/cart/components/Cartheader.jsx | 181 | ||||
| -rw-r--r-- | src/lib/checkout/components/Checkout.jsx | 930 | ||||
| -rw-r--r-- | src/lib/home/api/categoryHomeApi.js | 15 | ||||
| -rw-r--r-- | src/lib/product/components/Product/ProductDesktop.jsx | 763 | ||||
| -rw-r--r-- | src/lib/product/components/Product/ProductMobile.jsx | 411 | ||||
| -rw-r--r-- | src/lib/product/components/ProductSearch.jsx | 379 | ||||
| -rw-r--r-- | src/pages/api/product-variant/[id].js | 2 | ||||
| -rw-r--r-- | src/pages/api/product-variant/[id]/promotion/[category].js | 2 | ||||
| -rw-r--r-- | src/pages/api/product-variant/[id]/promotion/highlight.js | 2 | ||||
| -rw-r--r-- | src/pages/api/promotion-program/[id].js | 2 | ||||
| -rw-r--r-- | src/pages/shop/cart.jsx | 41 | ||||
| -rw-r--r-- | src/pages/shop/product/[slug].jsx | 98 |
13 files changed, 1439 insertions, 1389 deletions
diff --git a/src/core/utils/googleTag.js b/src/core/utils/googleTag.js index 6d7476bd..cc6d1283 100644 --- a/src/core/utils/googleTag.js +++ b/src/core/utils/googleTag.js @@ -2,7 +2,7 @@ const mapVariants = (variants) => { return variants.map((variant) => { const res = { item_id: variant.id, - item_name: variant.parent.name, + item_name: variant.name, discount: variant.price.price - variant.price.priceDiscount, item_brand: variant.manufacture?.name, item_variant: variant.code || variant.id, diff --git a/src/lib/cart/components/Cartheader.jsx b/src/lib/cart/components/Cartheader.jsx index 580dfc8c..19f79bc9 100644 --- a/src/lib/cart/components/Cartheader.jsx +++ b/src/lib/cart/components/Cartheader.jsx @@ -1,14 +1,9 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { getCartApi } from '../api/CartApi' -import currencyFormat from '@/core/utils/currencyFormat' -import Image from '@/core/components/elements/Image/Image' -import { createSlug } from '@/core/utils/slug' import useAuth from '@/core/hooks/useAuth' import { useRouter } from 'next/router' import odooApi from '@/core/api/odooApi' import { useProductCartContext } from '@/contexts/ProductCartContext' -import whatsappUrl from '@/core/utils/whatsappUrl' -import { AnimatePresence, motion } from 'framer-motion' const { ShoppingCartIcon, PhotoIcon } = require('@heroicons/react/24/outline') const { default: Link } = require('next/link') @@ -114,182 +109,6 @@ const Cardheader = (cartCount) => { </span> </Link> </div> - - <AnimatePresence> - {isHovered && ( - <> - <motion.div - initial={{ opacity: 0 }} - animate={{ opacity: 1 }} - exit={{ opacity: 0 }} - transition={{ duration: 0.15 }} - className='fixed top-[155px] left-0 w-full h-full bg-black/50 z-10' - /> - - <motion.div - initial={{ opacity: 0 }} - animate={{ opacity: 1, transition: { duration: 0.2 } }} - exit={{ opacity: 0, transition: { duration: 0.3 } }} - className='absolute z-10 left-0 w-96' - onMouseEnter={handleMouseEnter} - onMouseLeave={handleMouseLeave} - > - <motion.div - initial={{ height: 0 }} - animate={{ height: 'auto' }} - exit={{ height: 0 }} - className='w-full max-w-md p-2 bg-white border border-gray-200 rounded-lg shadow overflow-hidden' - > - <div className='p-2 flex justify-between items-center'> - <h5 className='text-base font-semibold leading-none'>Keranjang Belanja</h5> - <Link href='/shop/cart' class='text-sm font-medium text-red-600 underline'> - Lihat Semua - </Link> - </div> - <hr className='mt-3 mb-3 border border-gray-100' /> - <div className='flow-root max-h-[250px] overflow-y-auto'> - {!auth && ( - <div className='justify-center p-4'> - <p className='text-gray-500 text-center '> - Silahkan{' '} - <Link href='/login' className='text-red-600 underline leading-6'> - Login - </Link>{' '} - Untuk Melihat Daftar Keranjang Belanja Anda - </p> - </div> - )} - {isLoading && - itemLoading.map((item) => ( - <div key={item} role='status' className='max-w-sm animate-pulse'> - <div className='flex items-center space-x-4 mb- 2'> - <div className='flex-shrink-0'> - <PhotoIcon className='h-16 w-16 text-gray-500' /> - </div> - <div className='flex-1 min-w-0'> - <div className='h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4'></div> - <div className='h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px] mb-2.5'></div> - <div className='h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5'></div> - </div> - </div> - </div> - ))} - {auth && products.length === 0 && !isLoading && ( - <div className='justify-center p-4'> - <p className='text-gray-500 text-center '> - Tidak Ada Produk di Keranjang Belanja Anda - </p> - </div> - )} - {auth && products.length > 0 && !isLoading && ( - <> - <ul role='list' className='divide-y divide-gray-200 dark:divide-gray-700'> - {products && - products?.map((product, index) => ( - <> - <li className='py-1 sm:py-2'> - <div className='flex items-center space-x-4'> - <div className='flex-shrink-0'> - <Link - href={createSlug( - '/shop/product/', - product?.parent.name, - product?.parent.id - )} - className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' - > - <Image - src={product?.parent?.image} - alt={product?.name} - className='object-contain object-center border border-gray_r-6 h-16 w-16 rounded-md' - /> - </Link> - </div> - <div className='flex-1 min-w-0'> - <Link - href={createSlug( - '/shop/product/', - product?.parent.name, - product?.parent.id - )} - className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' - > - {' '} - <p className='text-caption-2 font-medium text-gray-900 truncate dark:text-white'> - {product.parent.name} - </p> - </Link> - - {product?.hasFlashsale && ( - <div className='flex gap-x-1 items-center mb-2 mt-1'> - <div className='badge-solid-red'> - {product?.price?.discountPercentage}% - </div> - <div className='text-gray_r-11 line-through text-caption-2'> - {currencyFormat(product?.price?.price)} - </div> - </div> - )} - <div className='flex justify-between items-center'> - <div className='font-semibold text-sm text-red-600'> - {product?.price?.priceDiscount > 0 ? ( - currencyFormat(product?.price?.priceDiscount) - ) : ( - <span className='text-gray_r-12/90 font-normal text-caption-1'> - <a - href={whatsappUrl('product', { - name: product.name, - manufacture: product.manufacture?.name, - url: createSlug( - '/shop/product/', - product.name, - product.id, - true - ) - })} - className='text-danger-500 underline' - rel='noopener noreferrer' - target='_blank' - > - Call For Price - </a> - </span> - )} - </div> - </div> - </div> - </div> - </li> - </> - ))} - </ul> - <hr /> - </> - )} - </div> - {auth && products.length > 0 && !isLoading && ( - <> - <div className='mt-3'> - <span className='text-gray-400 text-caption-2'>Subtotal Sebelum PPN : </span> - <span className='font-semibold text-red-600'>{currencyFormat(subTotal)}</span> - </div> - <div className='mt-5 mb-2'> - <button - type='button' - className='btn-solid-red rounded-lg w-full' - onClick={handleCheckout} - disabled={buttonLoading} - > - {buttonLoading ? 'Loading...' : 'Lanjutkan Ke Pembayaran'} - </button> - </div> - </> - )} - </motion.div> - </motion.div> - </> - )} - </AnimatePresence> </div> ) } diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 9a799010..eb31b9dc 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -1,191 +1,199 @@ -import Alert from '@/core/components/elements/Alert/Alert' -import Divider from '@/core/components/elements/Divider/Divider' -import Link from '@/core/components/elements/Link/Link' -import useAuth from '@/core/hooks/useAuth' -import { getItemAddress } from '@/core/utils/address' -import addressesApi from '@/lib/address/api/addressesApi' +import Alert from '@/core/components/elements/Alert/Alert'; +import Divider from '@/core/components/elements/Divider/Divider'; +import Link from '@/core/components/elements/Link/Link'; +import useAuth from '@/core/hooks/useAuth'; +import { getItemAddress } from '@/core/utils/address'; +import addressesApi from '@/lib/address/api/addressesApi'; import { BanknotesIcon, ChevronLeftIcon, ClockIcon, - ExclamationCircleIcon -} from '@heroicons/react/24/outline' -import React, { useEffect, useRef, useState } from 'react' -import _ from 'lodash' -import { deleteItemCart, getCartApi } from '@/core/utils/cart' -import currencyFormat from '@/core/utils/currencyFormat' -import { toast } from 'react-hot-toast' -import getFileBase64 from '@/core/utils/getFileBase64' -import { useRouter } from 'next/router' -import VariantGroupCard from '@/lib/variant/components/VariantGroupCard' -import axios from 'axios' -import Image from '@/core/components/elements/Image/Image' -import MobileView from '@/core/components/views/MobileView' -import DesktopView from '@/core/components/views/DesktopView' -import ExpedisiList from '../api/ExpedisiList' -import whatsappUrl from '@/core/utils/whatsappUrl' -import BottomPopup from '@/core/components/elements/Popup/BottomPopup' -import { useQuery } from 'react-query' -import { gtagPurchase } from '@/core/utils/googleTag' -import { findVoucher, getVoucher } from '../api/getVoucher' -import CardProdcuctsList from '@/core/components/elements/Product/cartProductsList' -import { Spinner } from '@chakra-ui/react' -import { AnimatePresence, motion } from 'framer-motion' - -const SELF_PICKUP_ID = 32 - -const { checkoutApi } = require('../api/checkoutApi') -const { getProductsCheckout } = require('../api/checkoutApi') + ExclamationCircleIcon, +} from '@heroicons/react/24/outline'; +import React, { useEffect, useRef, useState } from 'react'; +import _ from 'lodash'; +import { deleteItemCart } from '@/core/utils/cart'; +import currencyFormat from '@/core/utils/currencyFormat'; +import { toast } from 'react-hot-toast'; +import getFileBase64 from '@/core/utils/getFileBase64'; +import { useRouter } from 'next/router'; +import axios from 'axios'; +import Image from '@/core/components/elements/Image/Image'; +import MobileView from '@/core/components/views/MobileView'; +import DesktopView from '@/core/components/views/DesktopView'; +import ExpedisiList from '../api/ExpedisiList'; +import whatsappUrl from '@/core/utils/whatsappUrl'; +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; +import { useQuery } from 'react-query'; +import { gtagPurchase } from '@/core/utils/googleTag'; +import { findVoucher, getVoucher } from '../api/getVoucher'; +import { Spinner } from '@chakra-ui/react'; +import { AnimatePresence, motion } from 'framer-motion'; +import snakecaseKeys from 'snakecase-keys'; +import CartItem from '~/modules/cart/components/Item.tsx'; + +const SELF_PICKUP_ID = 32; + +const { checkoutApi } = require('../api/checkoutApi'); +const { getProductsCheckout } = require('../api/checkoutApi'); const Checkout = () => { - const router = useRouter() - const query = router.query.source ?? null - const auth = useAuth() + const router = useRouter(); + const query = router.query.source ?? null; + const auth = useAuth(); - const [activeVoucher, SetActiveVoucher] = useState(null) + const [activeVoucher, SetActiveVoucher] = useState(null); const { data: cartCheckout } = useQuery('cartCheckout-' + activeVoucher, () => getProductsCheckout(activeVoucher, query) - ) + ); const [selectedAddress, setSelectedAddress] = useState({ shipping: null, - invoicing: null - }) - const [addresses, setAddresses] = useState(null) + invoicing: null, + }); + const [addresses, setAddresses] = useState(null); useEffect(() => { - if (!auth) return + if (!auth) return; const getAddresses = async () => { - const dataAddresses = await addressesApi() - setAddresses(dataAddresses) - } + const dataAddresses = await addressesApi(); + setAddresses(dataAddresses); + }; - getAddresses() - }, [auth]) + getAddresses(); + }, [auth]); useEffect(() => { - if (!addresses) return + if (!addresses) return; const matchAddress = (key) => { - const addressToMatch = getItemAddress(key) - const foundAddress = addresses.filter((address) => address.id == addressToMatch) + const addressToMatch = getItemAddress(key); + const foundAddress = addresses.filter( + (address) => address.id == addressToMatch + ); if (foundAddress.length > 0) { - return foundAddress[0] + return foundAddress[0]; } - return addresses[0] - } + return addresses[0]; + }; setSelectedAddress({ shipping: matchAddress('shipping'), - invoicing: matchAddress('invoicing') - }) - }, [addresses]) - - const [products, setProducts] = useState(null) - const [totalWeight, setTotalWeight] = useState(0) - const [priceCheck, setPriceCheck] = useState(false) - const [listExpedisi, setExpedisi] = useState([]) - const [listserviceExpedisi, setListServiceExpedisi] = useState([]) - const [selectedExpedisi, setSelectedExpedisi] = useState(0) - const [selectedCarrierId, setselectedCarrierId] = useState(0) - const [selectedCarrier, setselectedCarrier] = useState(0) - const [biayaKirim, setBiayaKirim] = useState(0) - const [checkWeigth, setCheckWeight] = useState(false) - const [selectedServiceType, setSelectedServiceType] = useState(null) - const [selectedExpedisiService, setselectedExpedisiService] = useState(null) - const [etd, setEtd] = useState(null) - const [etdFix, setEtdFix] = useState(null) - const [bottomPopup, SetBottomPopup] = useState(null) - const [bottomPopupTnC, SetBottomPopupTnC] = useState(null) - const [itemTnC, setItemTnC] = useState(null) - const [listVouchers, SetListVoucher] = useState(null) - const [discountVoucher, SetDiscountVoucher] = useState(0) - const [codeVoucher, SetCodeVoucher] = useState(null) - const [findCodeVoucher, SetFindVoucher] = useState(null) - const [selisihHargaCode, SetSelisihHargaCode] = useState(null) - const [buttonTerapkan, SetButtonTerapkan] = useState(false) - const [checkoutValidation, setCheckoutValidation] = useState(false) - const [loadingVoucher, setLoadingVoucher] = useState(true) - const [loadingRajaOngkir, setLoadingRajaOngkir] = useState(false) - - const expedisiValidation = useRef(null) + invoicing: matchAddress('invoicing'), + }); + }, [addresses]); + + const [products, setProducts] = useState(null); + const [totalWeight, setTotalWeight] = useState(0); + const [priceCheck, setPriceCheck] = useState(false); + const [listExpedisi, setExpedisi] = useState([]); + const [listserviceExpedisi, setListServiceExpedisi] = useState([]); + const [selectedExpedisi, setSelectedExpedisi] = useState(0); + const [selectedCarrierId, setselectedCarrierId] = useState(0); + const [selectedCarrier, setselectedCarrier] = useState(0); + const [biayaKirim, setBiayaKirim] = useState(0); + const [checkWeigth, setCheckWeight] = useState(false); + const [selectedServiceType, setSelectedServiceType] = useState(null); + const [selectedExpedisiService, setselectedExpedisiService] = useState(null); + const [etd, setEtd] = useState(null); + const [etdFix, setEtdFix] = useState(null); + const [bottomPopup, SetBottomPopup] = useState(null); + const [bottomPopupTnC, SetBottomPopupTnC] = useState(null); + const [itemTnC, setItemTnC] = useState(null); + const [listVouchers, SetListVoucher] = useState(null); + const [discountVoucher, SetDiscountVoucher] = useState(0); + const [codeVoucher, SetCodeVoucher] = useState(null); + const [findCodeVoucher, SetFindVoucher] = useState(null); + const [selisihHargaCode, SetSelisihHargaCode] = useState(null); + const [buttonTerapkan, SetButtonTerapkan] = useState(false); + const [checkoutValidation, setCheckoutValidation] = useState(false); + const [loadingVoucher, setLoadingVoucher] = useState(true); + const [loadingRajaOngkir, setLoadingRajaOngkir] = useState(false); + + const expedisiValidation = useRef(null); const voucher = async () => { if (!listVouchers) { try { - let dataVoucher = await getVoucher(auth?.id, query) - SetListVoucher(dataVoucher) + let dataVoucher = await getVoucher(auth?.id, query); + SetListVoucher(dataVoucher); } finally { - setLoadingVoucher(false) + setLoadingVoucher(false); } } - } + }; const VoucherCode = async (code) => { - let dataVoucher = await findVoucher(code, auth.id, query) + let dataVoucher = await findVoucher(code, auth.id, query); if (dataVoucher.length <= 0) { - SetFindVoucher(1) - return + SetFindVoucher(1); + return; } - let addNewLine = dataVoucher[0] - let checkList = listVouchers.findIndex((voucher) => voucher.code == addNewLine.code) + let addNewLine = dataVoucher[0]; + let checkList = listVouchers.findIndex( + (voucher) => voucher.code == addNewLine.code + ); if (checkList >= 0) { if (listVouchers[checkList].canApply) { - ToggleSwitch(code) - SetCodeVoucher(null) + ToggleSwitch(code); + SetCodeVoucher(null); } else { - SetSelisihHargaCode(listVouchers[checkList].differenceToApply) - SetFindVoucher(2) + SetSelisihHargaCode(listVouchers[checkList].differenceToApply); + SetFindVoucher(2); } - return + return; } if (cartCheckout?.subtotal < addNewLine.minPurchaseAmount) { - SetSelisihHargaCode(currencyFormat(addNewLine.minPurchaseAmount - cartCheckout?.subtotal)) - SetFindVoucher(2) - return + SetSelisihHargaCode( + currencyFormat(addNewLine.minPurchaseAmount - cartCheckout?.subtotal) + ); + SetFindVoucher(2); + return; } else { - SetFindVoucher(3) - SetButtonTerapkan(true) + SetFindVoucher(3); + SetButtonTerapkan(true); } - SetListVoucher((prevList) => [addNewLine, ...prevList]) - SetActiveVoucher(addNewLine.code) - } + SetListVoucher((prevList) => [addNewLine, ...prevList]); + SetActiveVoucher(addNewLine.code); + }; useEffect(() => { - SetFindVoucher(null) - }, [bottomPopup]) + SetFindVoucher(null); + }, [bottomPopup]); useEffect(() => { const loadExpedisi = async () => { - let dataExpedisi = await ExpedisiList() + let dataExpedisi = await ExpedisiList(); dataExpedisi = dataExpedisi.map((expedisi) => ({ value: expedisi.id, label: expedisi.name, - carrierId: expedisi.deliveryCarrierId - })) - setExpedisi(dataExpedisi) - } - loadExpedisi() + carrierId: expedisi.deliveryCarrierId, + })); + setExpedisi(dataExpedisi); + }; + loadExpedisi(); const handlePopState = () => { - router.push('/shop/cart') - } + router.push('/shop/cart'); + }; - window.onpopstate = handlePopState + window.onpopstate = handlePopState; return () => { - window.onpopstate = null - } + window.onpopstate = null; + }; // voucher() - }, []) + }, []); const hitungDiscountVoucher = (code) => { - let dataVoucherIndex = listVouchers.findIndex((voucher) => voucher.code == code) - let dataActiveVoucher = listVouchers[dataVoucherIndex] + let dataVoucherIndex = listVouchers.findIndex( + (voucher) => voucher.code == code + ); + let dataActiveVoucher = listVouchers[dataVoucherIndex]; - let countDiscount = dataActiveVoucher.discountVoucher + let countDiscount = dataActiveVoucher.discountVoucher; /*if (dataActiveVoucher.discountType === 'percentage') { countDiscount = cartCheckout?.subtotal * (dataActiveVoucher.discountAmount / 100) @@ -199,116 +207,122 @@ const Checkout = () => { countDiscount = dataActiveVoucher.discountAmount }*/ - return countDiscount - } + return countDiscount; + }; useEffect(() => { - if (!listVouchers) return - if (!activeVoucher) return + if (!listVouchers) return; + if (!activeVoucher) return; - const countDiscount = hitungDiscountVoucher(activeVoucher) + const countDiscount = hitungDiscountVoucher(activeVoucher); - SetDiscountVoucher(countDiscount) - }, [activeVoucher, listVouchers]) + SetDiscountVoucher(countDiscount); + }, [activeVoucher, listVouchers]); useEffect(() => { - setProducts(cartCheckout?.products) - setCheckWeight(cartCheckout?.hasProductWithoutWeight) - setTotalWeight(cartCheckout?.totalWeight.g) - }, [cartCheckout]) + setProducts(cartCheckout?.products); + setCheckWeight(cartCheckout?.hasProductWithoutWeight); + setTotalWeight(cartCheckout?.totalWeight.g); + }, [cartCheckout]); useEffect(() => { - setCheckoutValidation(false) + setCheckoutValidation(false); const loadServiceRajaOngkir = async () => { - setLoadingRajaOngkir(true) + setLoadingRajaOngkir(true); const body = { origin: 2127, destination: selectedAddress.shipping.rajaongkirCityId, weight: totalWeight, courier: selectedCarrier, originType: 'subdistrict', - destinationType: 'subdistrict' - } - setBiayaKirim(0) - const dataService = await axios('/api/rajaongkir-service?body=' + JSON.stringify(body)) - setLoadingRajaOngkir(false) - setListServiceExpedisi(dataService.data[0].costs) + destinationType: 'subdistrict', + }; + setBiayaKirim(0); + const dataService = await axios( + '/api/rajaongkir-service?body=' + JSON.stringify(body) + ); + setLoadingRajaOngkir(false); + setListServiceExpedisi(dataService.data[0].costs); if (dataService.data[0].costs[0]) { - setBiayaKirim(dataService.data[0].costs[0]?.cost[0].value) + setBiayaKirim(dataService.data[0].costs[0]?.cost[0].value); setselectedExpedisiService( - dataService.data[0].costs[0]?.description + '-' + dataService.data[0].costs[0]?.service - ) - setEtd(dataService.data[0].costs[0]?.cost[0].etd) - toast.success('Harap pilih tipe layanan pengiriman') + dataService.data[0].costs[0]?.description + + '-' + + dataService.data[0].costs[0]?.service + ); + setEtd(dataService.data[0].costs[0]?.cost[0].etd); + toast.success('Harap pilih tipe layanan pengiriman'); } else { - toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.') + toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.'); } - } + }; if (selectedCarrier != 0 && selectedCarrier != 1 && totalWeight > 0) { - loadServiceRajaOngkir() + loadServiceRajaOngkir(); } else { - setListServiceExpedisi() - setBiayaKirim(0) - setselectedExpedisiService() - setEtd() + setListServiceExpedisi(); + setBiayaKirim(0); + setselectedExpedisiService(); + setEtd(); } - }, [selectedCarrier, selectedAddress, totalWeight]) + }, [selectedCarrier, selectedAddress, totalWeight]); useEffect(() => { if (selectedServiceType) { - let serviceType = selectedServiceType.split(',') - setBiayaKirim(serviceType[0]) - setselectedExpedisiService(serviceType[1]) - setEtd(serviceType[2]) + let serviceType = selectedServiceType.split(','); + setBiayaKirim(serviceType[0]); + setselectedExpedisiService(serviceType[1]); + setEtd(serviceType[2]); } - }, [selectedServiceType]) + }, [selectedServiceType]); useEffect(() => { - if (etd) setEtdFix(calculateEstimatedArrival(etd)) - }, [etd]) + if (etd) setEtdFix(calculateEstimatedArrival(etd)); + }, [etd]); useEffect(() => { if (selectedExpedisi) { - let serviceType = selectedExpedisi.split(',') - if (serviceType[0] === 0) return + let serviceType = selectedExpedisi.split(','); + if (serviceType[0] === 0) return; - setselectedCarrier(serviceType[0]) - setselectedCarrierId(serviceType[1]) - setListServiceExpedisi([]) + setselectedCarrier(serviceType[0]); + setselectedCarrierId(serviceType[1]); + setListServiceExpedisi([]); } - }, [selectedExpedisi]) + }, [selectedExpedisi]); - const poNumber = useRef(null) - const poFile = useRef(null) + const poNumber = useRef(null); + const poFile = useRef(null); - const [isLoading, setIsLoading] = useState(false) + const [isLoading, setIsLoading] = useState(false); const checkout = async () => { - const file = poFile.current.files[0] + const file = poFile.current.files[0]; if (typeof file !== 'undefined' && file.size > 5000000) { - toast.error('Maksimal ukuran file adalah 5MB', { position: 'bottom-center' }) - return + toast.error('Maksimal ukuran file adalah 5MB', { + position: 'bottom-center', + }); + return; } if (selectedExpedisi === 0) { - setCheckoutValidation(true) + setCheckoutValidation(true); if (expedisiValidation.current) { - const position = expedisiValidation.current.getBoundingClientRect() + const position = expedisiValidation.current.getBoundingClientRect(); window.scrollTo({ top: position.top - 300 + window.pageYOffset, - behavior: 'smooth' - }) + behavior: 'smooth', + }); } - return + return; } if (selectedCarrier != 1 && biayaKirim == 0) { - toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.') - return + toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.'); + return; } - setIsLoading(true) + setIsLoading(true); const productOrder = products.map((product) => ({ product_id: product.id, - quantity: product.quantity - })) + quantity: product.quantity, + })); let data = { partner_shipping_id: auth.partnerId, partner_invoice_id: auth.partnerId, @@ -319,80 +333,82 @@ const Checkout = () => { estimated_arrival_days: splitDuration(etd), delivery_service_type: selectedExpedisiService, voucher: activeVoucher, - type: 'sale_order' - } + type: 'sale_order', + }; if (query) { - data.source = 'buy' + data.source = 'buy'; } - if (poNumber.current.value) data.po_number = poNumber.current.value - if (typeof file !== 'undefined') data.po_file = await getFileBase64(file) + if (poNumber.current.value) data.po_number = poNumber.current.value; + if (typeof file !== 'undefined') data.po_file = await getFileBase64(file); - const isCheckouted = await checkoutApi({ data }) + const isCheckouted = await checkoutApi({ data }); if (!isCheckouted?.id) { - toast.error('Gagal melakukan transaksi, terjadi kesalahan internal') - return + toast.error('Gagal melakukan transaksi, terjadi kesalahan internal'); + return; } - gtagPurchase(products, biayaKirim, isCheckouted.name) + gtagPurchase(products, biayaKirim, isCheckouted.name); const midtrans = async () => { - for (const product of products) deleteItemCart({ productId: product.id }) + for (const product of products) deleteItemCart({ productId: product.id }); const payment = await axios.post( `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/midtrans-payment?transactionId=${isCheckouted.id}` - ) - setIsLoading(false) - window.location.href = payment.data.redirectUrl - } + ); + setIsLoading(false); + window.location.href = payment.data.redirectUrl; + }; gtag('event', 'conversion', { send_to: 'AW-954540379/nDymCL3BhaQYENvClMcD', - value: cartCheckout?.grandTotal + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000, + value: + cartCheckout?.grandTotal + + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000, currency: 'IDR', transaction_id: isCheckouted.id, - event_callback: midtrans - }) - } + event_callback: midtrans, + }); + }; const handlingActivateCode = async () => { - VoucherCode(codeVoucher) - } + VoucherCode(codeVoucher); + }; const handleUseVoucher = async (code, isCheck) => { if (isCheck) { if (code === activeVoucher) { - SetActiveVoucher(null) - SetDiscountVoucher(0) + SetActiveVoucher(null); + SetDiscountVoucher(0); } else { - SetActiveVoucher(code) - SetFindVoucher(null) - document.getElementById('uniqCode').value = '' - SetButtonTerapkan(false) + SetActiveVoucher(code); + SetFindVoucher(null); + document.getElementById('uniqCode').value = ''; + SetButtonTerapkan(false); } } else { - SetActiveVoucher(code) - SetFindVoucher(null) - document.getElementById('uniqCode').value = '' - SetButtonTerapkan(false) + SetActiveVoucher(code); + SetFindVoucher(null); + document.getElementById('uniqCode').value = ''; + SetButtonTerapkan(false); } - } + }; const onChangeCodeVoucher = async (e) => { - SetCodeVoucher(e.target.value) - SetButtonTerapkan(false) - } + SetCodeVoucher(e.target.value); + SetButtonTerapkan(false); + }; - const [isChecked, setIsChecked] = useState(false) + const [isChecked, setIsChecked] = useState(false); const ToggleSwitch = (code) => { - setIsChecked(!isChecked) - handleUseVoucher(code, !isChecked) - } + setIsChecked(!isChecked); + handleUseVoucher(code, !isChecked); + }; const handlingTnC = async (item) => { - setItemTnC(item) - SetBottomPopupTnC(true) - } + setItemTnC(item); + SetBottomPopupTnC(true); + }; // const taxTotal = (totalAmount - totalDiscountAmount - discountVoucher) * 0.11 return ( @@ -401,13 +417,17 @@ const Checkout = () => { className='w-full md:!w-[40%] !min-h-[90vh]' active={bottomPopupTnC} close={() => { - SetBottomPopupTnC(false) - SetBottomPopup(false) + SetBottomPopupTnC(false); + SetBottomPopup(false); }} title={ <div> - <button className='flex gap-x-2 items-center' onClick={() => SetBottomPopupTnC(false)}> - <ChevronLeftIcon class='h- w-5 text-black' /> <span className='text-lg'>Voucher</span> + <button + className='flex gap-x-2 items-center' + onClick={() => SetBottomPopupTnC(false)} + > + <ChevronLeftIcon class='h- w-5 text-black' />{' '} + <span className='text-lg'>Voucher</span> </button>{' '} </div> } @@ -420,13 +440,17 @@ const Checkout = () => { <span className='text-sm'> {' '} Berakhir dalam :{' '} - <span className='text-sm text-red-500'>{itemTnC?.remainingTime} lagi</span> + <span className='text-sm text-red-500'> + {itemTnC?.remainingTime} lagi + </span> </span> </div> <div className='flex items-center gap-x-1'> <BanknotesIcon class='h-6 w-6 text-green-500' /> <span className='text-sm'> Kode Voucher : </span> - <span className='text-red-500 font-semibold'>{itemTnC?.code}</span> + <span className='text-red-500 font-semibold'> + {itemTnC?.code} + </span> </div> </div> <div> @@ -441,6 +465,7 @@ const Checkout = () => { </div> </div> </BottomPopup> + <BottomPopup className='w-full md:!w-[40%] !min-h-[350px]' active={bottomPopup} @@ -448,8 +473,8 @@ const Checkout = () => { title='Gunakan Promo' > <div className='row'> - <div className='flex justify-between items-center'> - <div className='flex md:w-[70%]'> + <div className='flex justify-between items-center gap-x-4'> + <div className='flex flex-1 md:w-[70%]'> <input type='text' id='uniqCode' @@ -481,15 +506,16 @@ const Checkout = () => { {findCodeVoucher === 1 && ( <div className='mt-2'> <span className='text-caption-1 mt-2 text-red-600'> - Kode voucher salah / sudah tidak berlaku lagi. Coba voucher lainnya, ya. + Kode voucher salah / sudah tidak berlaku lagi. Coba voucher + lainnya, ya. </span> </div> )} {findCodeVoucher === 2 && ( <div className='mt-2'> <span className='text-caption-1 mt-2 text-red-600'> - Tambah <span className='text-red-600'>{selisihHargaCode}</span> untuk pakai promo - ini + Tambah <span className='text-red-600'>{selisihHargaCode}</span>{' '} + untuk pakai promo ini </span> </div> )} @@ -500,15 +526,21 @@ const Checkout = () => { <div className='flex items-center justify-center mt-4 mb-4'> <div className='text-center'> <h1 className='font-bold mb-4'>Tidak ada voucher tersedia</h1> - <p className='text-gray-500'>Maaf, saat ini tidak ada voucher yang tersedia.</p> + <p className='text-gray-500'> + Maaf, saat ini tidak ada voucher yang tersedia. + </p> </div> </div> ) : ( - <h3 className='font-semibold mb-4'>Promo Khusus Untuk {auth?.name}</h3> + <h3 className='font-semibold mb-4'> + Promo Khusus Untuk {auth?.name} + </h3> )} {loadingVoucher && ( <> - <div className={`border border-solid w-full hover:cursor-pointer p-2`}> + <div + className={`border border-solid w-full hover:cursor-pointer p-2`} + > <div class='flex items-center space-x-3'> <div class='flex items-center justify-center h-28 w-48 mb-4 bg-gray-300 rounded dark:bg-gray-700'> <svg @@ -529,7 +561,9 @@ const Checkout = () => { </div> </div> </div> - <div className={`border border-solid w-full hover:cursor-pointer p-2`}> + <div + className={`border border-solid w-full hover:cursor-pointer p-2`} + > <div class='flex items-center space-x-3'> <div class='flex items-center justify-center h-28 w-48 mb-4 bg-gray-300 rounded dark:bg-gray-700'> <svg @@ -579,7 +613,9 @@ const Checkout = () => { > <p> Voucher tidak bisa di gunakan,{' '} - <span className='text-red font-bold'>Baca Selengkapnya !</span> + <span className='text-red font-bold'> + Baca Selengkapnya ! + </span> </p> </div> )} @@ -589,14 +625,20 @@ const Checkout = () => { <div className='absolute w-full h-full bg-gray_r-3/40 top-0 left-0 z-50' /> )} <div className='hidden md:w-[250px] md:block'> - <Image src={item.image} alt={item.name} className={`object-cover`} /> + <Image + src={item.image} + alt={item.name} + className={`object-cover`} + /> </div> <div className='w-full'> <div className='flex justify-between gap-x-2 mb-1 items-center'> <div className=''> <h3 className='font-semibold'>{item.name}</h3> <div className='mt-1'> - <span className='text-sm line-clamp-3'>{item.description} </span> + <span className='text-sm line-clamp-3'> + {item.description}{' '} + </span> </div> </div> <div className='flex justify-end'> @@ -605,7 +647,9 @@ const Checkout = () => { type='checkbox' value='' class='sr-only peer' - checked={activeVoucher === item.code ? true : false} + checked={ + activeVoucher === item.code ? true : false + } onChange={() => ToggleSwitch(item.code)} /> <div class="w-11 h-6 bg-gray-200 rounded-full peer dark:bg-gray-700 peer-focus:ring-4 peer-focus:ring-green-300 dark:peer-focus:ring-green-800 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-green-600"></div> @@ -616,11 +660,15 @@ const Checkout = () => { <div className='flex justify-between items-center'> <p className='text-justify text-sm md:text-xs'> Kode Voucher :{' '} - <span className='text-red-500 font-semibold'>{item.code}</span> + <span className='text-red-500 font-semibold'> + {item.code} + </span> </p> <p className='text-sm md:text-xs'> {activeVoucher === item.code && ( - <span className=' text-green-600'>Voucher digunakan </span> + <span className=' text-green-600'> + Voucher digunakan{' '} + </span> )} </p> </div> @@ -642,7 +690,10 @@ const Checkout = () => { <div className='flex justify-between items-center'> <div className='text-left ml-3 text-sm '> Berakhir dalam{' '} - <span className='text-red-600'>{item.remainingTime}</span> lagi,{' '} + <span className='text-red-600'> + {item.remainingTime} + </span>{' '} + lagi,{' '} </div> <div className='text-sm ml-2 text-red-600' @@ -670,6 +721,7 @@ const Checkout = () => { </div> </div> </BottomPopup> + <MobileView> <div className='p-4'> <Alert type='info' className='text-caption-2 flex gap-x-3'> @@ -677,8 +729,8 @@ const Checkout = () => { <ExclamationCircleIcon className='w-7 text-blue-700' /> </div> <span className='leading-5'> - Jika mengalami kesulitan dalam melakukan pembelian di website Indoteknik. Hubungi kami - disini + Jika mengalami kesulitan dalam melakukan pembelian di website + Indoteknik. Hubungi kami disini </span> </Alert> </div> @@ -701,14 +753,17 @@ const Checkout = () => { </svg> <span class='sr-only'>Info</span> <div className='text-justify'> - Fitur Self Pickup, hanya berlaku untuk customer di area jakarta. Apa bila memilih - fitur ini, anda akan dihubungi setelah barang siap diambil. + Fitur Self Pickup, hanya berlaku untuk customer di area jakarta. + Apa bila memilih fitur ini, anda akan dihubungi setelah barang + siap diambil. </div> </div> </div> )} - {selectedCarrierId == SELF_PICKUP_ID && <PickupAddress label='Alamat Pickup' />} + {selectedCarrierId == SELF_PICKUP_ID && ( + <PickupAddress label='Alamat Pickup' /> + )} {selectedCarrierId != SELF_PICKUP_ID && ( <> <SectionAddress @@ -742,7 +797,10 @@ const Checkout = () => { /> <div className='p-4 flex flex-col gap-y-4'> - {products && <VariantGroupCard openOnClick={false} variants={products} />} + {!!products && + snakecaseKeys(products).map((item, index) => ( + <CartItem key={index} item={item} editable={false} /> + ))} </div> <Divider /> @@ -750,7 +808,6 @@ const Checkout = () => { <div className='p-4'> <div className='flex justify-between items-center'> <div className='font-medium'>Ringkasan Pesanan</div> - <div className='text-gray_r-11 text-caption-1'>{products?.length} Barang</div> </div> <hr className='my-4 border-gray_r-6' /> {!cartCheckout ? ( @@ -804,7 +861,9 @@ const Checkout = () => { {activeVoucher && ( <div className='flex gap-x-2 justify-between'> <div className='text-gray_r-11'>Diskon Voucher</div> - <div className='text-danger-500'>- {currencyFormat(discountVoucher)}</div> + <div className='text-danger-500'> + - {currencyFormat(discountVoucher)} + </div> </div> )} <div className='flex gap-x-2 justify-between'> @@ -819,7 +878,11 @@ const Checkout = () => { <div className='text-gray_r-11'> Biaya Kirim <p className='text-xs mt-3'>{etdFix}</p> </div> - <div>{currencyFormat(Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000)}</div> + <div> + {currencyFormat( + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 + )} + </div> </div> </div> )} @@ -840,7 +903,8 @@ const Checkout = () => { <div>Grand Total</div> <div className='font-semibold text-gray_r-12'> {currencyFormat( - cartCheckout?.grandTotal + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 + cartCheckout?.grandTotal + + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 )} </div> </div> @@ -852,8 +916,8 @@ const Checkout = () => { <button type='button' onClick={() => { - SetBottomPopup(true) - voucher() + SetBottomPopup(true); + voucher(); }} className='text-gray-900 p-4 flex items-center justify-between rounded-lg bg-white border font-medium border-gray-300 hover:bg-gray-100 py-2.5 h-[50px] w-[100%]' > @@ -886,7 +950,8 @@ const Checkout = () => { </div> {/* <p className='text-caption-2 text-gray_r-10 mb-2'>*) Belum termasuk biaya pengiriman</p> */} <p className='text-caption-2 text-gray_r-10 leading-5'> - Dengan melakukan pembelian melalui website Indoteknik, saya menyetujui{' '} + Dengan melakukan pembelian melalui website Indoteknik, saya + menyetujui{' '} <Link href='/syarat-ketentuan' className='inline font-normal'> Syarat & Ketentuan </Link>{' '} @@ -911,10 +976,16 @@ const Checkout = () => { </div> <div className='w-6/12'> <label className='form-label font-normal'>Nomor PO</label> - <input type='text' className='form-input mt-2 h-12' ref={poNumber} /> + <input + type='text' + className='form-input mt-2 h-12' + ref={poNumber} + /> </div> </div> - <p className='text-caption-2 text-gray_r-11 mt-2'>Ukuran dokumen PO Maksimal 5MB</p> + <p className='text-caption-2 text-gray_r-11 mt-2'> + Ukuran dokumen PO Maksimal 5MB + </p> </div> <Divider /> @@ -923,7 +994,9 @@ const Checkout = () => { <button className='flex-1 btn-yellow' onClick={checkout} - disabled={isLoading || !products || products?.length == 0 || priceCheck} + disabled={ + isLoading || !products || products?.length == 0 || priceCheck + } > {isLoading ? 'Loading...' : 'Lanjut Pembayaran'} </button> @@ -957,8 +1030,9 @@ const Checkout = () => { </svg> <span class='sr-only'>Info</span> <div> - Fitur Self Pickup, hanya berlaku untuk customer di area jakarta. Apa bila memilih - fitur ini, anda akan dihubungi setelah barang siap diambil. + Fitur Self Pickup, hanya berlaku untuk customer di area jakarta. + Apa bila memilih fitur ini, anda akan dihubungi setelah barang + siap diambil. </div> </div> )} @@ -966,7 +1040,9 @@ const Checkout = () => { <div className='flex'> {' '} <div className='w-3/4 border border-gray_r-6 rounded bg-white'> - {selectedCarrierId == SELF_PICKUP_ID && <PickupAddress label='Alamat Pickup' />} + {selectedCarrierId == SELF_PICKUP_ID && ( + <PickupAddress label='Alamat Pickup' /> + )} {selectedCarrierId != SELF_PICKUP_ID && ( <> <SectionAddress @@ -1000,170 +1076,13 @@ const Checkout = () => { /> <div className='p-4'> - <div className='font-medium'>Detail Pesanan</div> - <CardProdcuctsList isLoading={isLoading} products={products} /> - - {/* <table className='table-checkout'> - <thead> - <tr> - <th>Nama Produk</th> - <th>Jumlah</th> - <th>Harga</th> - <th>Subtotal</th> - </tr> - </thead> - <tbody> - {!products ? ( - <tr> - <td colSpan={4}> - <div className='container my-4'> - <div class='h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4'></div> - <div class='h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5'></div> - <div class='h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5'></div> - <div class='h-2 bg-gray-200 rounded-full dark:bg-gray-700'></div> - </div> - </td> - </tr> - ) : ( - products?.map((product) => ( - <> - <tr - key={product.id} - className={`${product.program ? '!border-t-0 !border-b-0' : ''}`} - > - <td className='flex'> - <div className='w-[20%] flex-shrink-0'> - <Image - src={product?.parent?.image} - alt={product?.name} - className='object-contain object-center border border-gray_r-6 h-40 w-full rounded-md' - /> - </div> - <div className='px-2 text-left'> - <div className='line-clamp-2 leading-6 !text-gray_r-12 font-normal'> - {product?.parent?.name} - </div> - <div className='text-gray_r-11 mt-2'> - {product?.code}{' '} - {product?.attributes.length > 0 - ? `| ${product?.attributes.join(', ')}` - : ''} - </div> - <div className='text-gray_r-11 mt-2'> - Berat item : {product?.weight} Kg - </div> - </div> - </td> - <td> - <input - className='form-input w-16 py-2 text-center bg-gray_r-1' - type='number' - value={product?.quantity} - disabled - /> - </td> - <td> - {product?.hasFlashsale ? ( - <> - <div className='flex gap-x-1 items-center justify-center mt-3'> - <div className='text-gray_r-11 line-through text-caption-1'> - {currencyFormat(product?.price?.price)} - </div> - <div className='badge-solid-red'> - {product?.price?.discountPercentage}% - </div> - </div> - <div className='font-normal mt-1'> - {currencyFormat(product?.price?.priceDiscount)} - </div> - </> - ) : ( - <div className='font-normal mt-1'> - {product.price.priceDiscount > 0 - ? currencyFormat(product?.price?.priceDiscount) - : 'Call for Inquiry'} - </div> - )} - </td> - <td> - <div className='text-danger-500 font-medium'> - {product.price.priceDiscount > 0 ? ( - currencyFormat(product?.price?.priceDiscount * product?.quantity) - ) : ( - <a - href={whatsappUrl('product', { - name: product.name, - url: createSlug( - '/shop/product/', - product.name, - product.id, - true - ) - })} - className='underline' - > - Call for Inquiry{' '} - </a> - )} - </div> - </td> - </tr> - {product.program && - product.program.items && - product.program.items.map((item) => ( - <> - <tr key={product?.program?.id} className='!border-t-0'> - <td className='flex'> - <div className='w-[20%] flex-shrink-0'> - <Image - src={item.parent.image} - alt={item.name} - className='object-contain object-center border border-gray_r-6 h-40 w-full rounded-md' - /> - </div> - <div className='px-2 text-left'> - <div className=''> - <span className='border border-solid border-red-600 rounded-md p-1 text-red-600'> - {product.program.type.label} - </span> - </div> - <div className='mt-2 line-clamp-2 leading-6'>{item.name}</div> - </div> - </td> - <td> - <input - className='form-input w-16 py-2 text-center bg-gray_r-1' - type='number' - value={1} - disabled - /> - </td> - <td> - {item?.price?.discountPercentage > 0 && ( - <div className='flex gap-x-1 items-center justify-center mt-3'> - <div className='text-gray_r-11 line-through text-caption-1'> - {currencyFormat(product?.price?.price)} - </div> - </div> - )} - <div className='font-normal mt-1'> - {item?.price.priceDiscount > 0 ? 'Gratis' : ''} - </div> - </td> - <td> - <div className='text-danger-500 font-medium'> - {item.price.priceDiscount > 0 ? 'Gratis' : ''} - </div> - </td> - <td></td> - </tr> - </> - ))} - </> - )) - )} - </tbody> - </table> */} + <div className='font-medium mb-6'>Detail Pesanan</div> + <div className='flex flex-col gap-y-8 border-t border-gray-300 pt-8'> + {!!products && + snakecaseKeys(products).map((item, index) => ( + <CartItem key={index} item={item} editable={false} /> + ))} + </div> </div> </div> <div className='w-1/4 pl-4'> @@ -1171,7 +1090,8 @@ const Checkout = () => { <div className='flex justify-between items-center'> <div className='font-medium'>Ringkasan Pesanan</div> <div className='text-gray_r-11 text-caption-1'> - {products?.length} Barang - {cartCheckout?.totalWeight.kg} Kg + {cartCheckout?.totalWeight.kg}{' '} + Kg </div> </div> @@ -1227,7 +1147,9 @@ const Checkout = () => { {activeVoucher && ( <div className='flex gap-x-2 justify-between'> <div className='text-gray_r-11'>Diskon Voucher</div> - <div className='text-danger-500'>- {currencyFormat(discountVoucher)}</div> + <div className='text-danger-500'> + - {currencyFormat(discountVoucher)} + </div> </div> )} <div className='flex gap-x-2 justify-between'> @@ -1244,7 +1166,9 @@ const Checkout = () => { <p className='text-xs mt-3'>{etdFix}</p> </div> <div> - {currencyFormat(Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000)} + {currencyFormat( + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 + )} </div> </div> </div> @@ -1279,8 +1203,8 @@ const Checkout = () => { <button type='button' onClick={() => { - SetBottomPopup(true) - voucher() + SetBottomPopup(true); + voucher(); }} className='text-gray-900 p-3 flex items-center justify-between rounded-lg bg-white border font-medium border-gray-300 hover:bg-gray-100 py-2.5 h-[50px] w-[100%]' > @@ -1312,7 +1236,8 @@ const Checkout = () => { </div> <p className='text-caption-2 text-gray_r-11 leading-5'> - Dengan melakukan pembelian melalui website Indoteknik, saya menyetujui{' '} + Dengan melakukan pembelian melalui website Indoteknik, saya + menyetujui{' '} <Link href='/syarat-ketentuan' className='inline font-normal'> Syarat & Ketentuan </Link>{' '} @@ -1322,7 +1247,8 @@ const Checkout = () => { <hr className='my-4 border-gray_r-6' /> <div className='font-medium mt-4'> - Purchase Order <span className='font-normal text-gray_r-11'>(Opsional)</span> + Purchase Order{' '} + <span className='font-normal text-gray_r-11'>(Opsional)</span> </div> <div className='mt-4 flex gap-x-3'> @@ -1337,17 +1263,28 @@ const Checkout = () => { </div> <div className='w-6/12'> <label className='form-label font-normal'>Nomor PO</label> - <input type='text' className='form-input mt-2 h-12' ref={poNumber} /> + <input + type='text' + className='form-input mt-2 h-12' + ref={poNumber} + /> </div> </div> - <p className='text-caption-2 text-gray_r-11 mt-2'>Ukuran dokumen PO Maksimal 5MB</p> + <p className='text-caption-2 text-gray_r-11 mt-2'> + Ukuran dokumen PO Maksimal 5MB + </p> <hr className='my-4 border-gray_r-6' /> <button className='w-full btn-yellow mt-4' onClick={checkout} - disabled={isLoading || !products || products?.length == 0 || priceCheck} + disabled={ + isLoading || + !products || + products?.length == 0 || + priceCheck + } > {isLoading ? 'Loading...' : 'Lanjut Pembayaran'} </button> @@ -1367,8 +1304,8 @@ const Checkout = () => { </div> </DesktopView> </> - ) -} + ); +}; const SectionAddress = ({ address, label, url }) => ( <div className='p-4'> @@ -1382,7 +1319,9 @@ const SectionAddress = ({ address, label, url }) => ( {address && ( <div className='mt-4 text-caption-1'> <div className='badge-red mb-2'> - {address.type.charAt(0).toUpperCase() + address.type.slice(1) + ' Address'} + {address.type.charAt(0).toUpperCase() + + address.type.slice(1) + + ' Address'} </div> <p className='font-medium'>{address.name}</p> <p className='mt-2 text-gray_r-11'>{address.mobile}</p> @@ -1392,7 +1331,7 @@ const SectionAddress = ({ address, label, url }) => ( </div> )} </div> -) +); const SectionValidation = ({ address }) => address?.rajaongkirCityId == 0 && ( @@ -1409,7 +1348,7 @@ const SectionValidation = ({ address }) => </Link> </div> </BottomPopup> - ) + ); const SectionExpedisi = ({ address, @@ -1418,7 +1357,7 @@ const SectionExpedisi = ({ checkWeigth, checkoutValidation, expedisiValidation, - loadingRajaOngkir + loadingRajaOngkir, }) => address?.rajaongkirCityId > 0 && ( <div className='p-4' ref={expedisiValidation}> @@ -1427,7 +1366,9 @@ const SectionExpedisi = ({ <div className='w-[250px]'> <div className='flex items-center gap-x-4'> <select - className={`form-input ${checkoutValidation ? 'border-red-500 shake' : ''}`} + className={`form-input ${ + checkoutValidation ? 'border-red-500 shake' : '' + }`} onChange={(e) => setSelectedExpedisi(e.target.value)} required > @@ -1453,7 +1394,7 @@ const SectionExpedisi = ({ animate={{ opacity: 1, width: '28px' }} exit={{ opacity: 0, width: 0 }} transition={{ - duration: 0.25 + duration: 0.25, }} className='overflow-hidden' > @@ -1463,7 +1404,9 @@ const SectionExpedisi = ({ </AnimatePresence> </div> {checkoutValidation && ( - <span className='text-sm text-red-500'>*silahkan pilih expedisi</span> + <span className='text-sm text-red-500'> + *silahkan pilih expedisi + </span> )} </div> <style jsx>{` @@ -1474,8 +1417,9 @@ const SectionExpedisi = ({ </div> {checkWeigth == true && ( <p className='mt-4 text-gray_r-11 leading-6'> - Mohon maaf, pengiriman hanya tersedia untuk self pickup karena terdapat barang yang belum - diatur beratnya. Mohon atur berat barang dengan menghubungi admin melalui{' '} + Mohon maaf, pengiriman hanya tersedia untuk self pickup karena + terdapat barang yang belum diatur beratnya. Mohon atur berat barang + dengan menghubungi admin melalui{' '} <a className='text-danger-500 inline' href='https://api.whatsapp.com/send?phone=628128080622' @@ -1485,7 +1429,7 @@ const SectionExpedisi = ({ </p> )} </div> - ) + ); const SectionListService = ({ listserviceExpedisi, setSelectedServiceType }) => listserviceExpedisi?.length > 0 && ( @@ -1494,7 +1438,10 @@ const SectionListService = ({ listserviceExpedisi, setSelectedServiceType }) => <div className='flex justify-between items-center'> <div className='font-medium'>Tipe Layanan Ekspedisi: </div> <div> - <select className='form-input' onChange={(e) => setSelectedServiceType(e.target.value)}> + <select + className='form-input' + onChange={(e) => setSelectedServiceType(e.target.value)} + > {listserviceExpedisi.map((service) => ( <option value={ @@ -1511,7 +1458,9 @@ const SectionListService = ({ listserviceExpedisi, setSelectedServiceType }) => {' '} {service.description} - {service.service.toUpperCase()} {extractDuration(service.cost[0].etd) && - ` (Estimasi Tiba ${extractDuration(service.cost[0].etd)} Hari)`} + ` (Estimasi Tiba ${extractDuration( + service.cost[0].etd + )} Hari)`} </option> ))} </select> @@ -1520,73 +1469,73 @@ const SectionListService = ({ listserviceExpedisi, setSelectedServiceType }) => </div> <Divider /> </> - ) + ); function addDays(date, days) { - const result = new Date(date) - result.setDate(result.getDate() + days) - return result + const result = new Date(date); + result.setDate(result.getDate() + days); + return result; } function formatDate(date) { - const day = date.getDate() - const month = date.toLocaleString('default', { month: 'short' }) - return `${day} ${month}` + const day = date.getDate(); + const month = date.toLocaleString('default', { month: 'short' }); + return `${day} ${month}`; } function calculateEstimatedArrival(duration) { if (duration) { - let estimationDate = duration.split('-') - estimationDate[0] = parseInt(estimationDate[0]) - estimationDate[1] = parseInt(estimationDate[1]) - const from = addDays(new Date(), estimationDate[0] + 3) - const to = addDays(new Date(), estimationDate[1] + 3) + let estimationDate = duration.split('-'); + estimationDate[0] = parseInt(estimationDate[0]); + estimationDate[1] = parseInt(estimationDate[1]); + const from = addDays(new Date(), estimationDate[0] + 3); + const to = addDays(new Date(), estimationDate[1] + 3); - let etdText = `*Estimasi tiba ${formatDate(from)}` + let etdText = `*Estimasi tiba ${formatDate(from)}`; if (estimationDate[1] > estimationDate[0]) { - etdText += ` - ${formatDate(to)}` + etdText += ` - ${formatDate(to)}`; } - return etdText + return etdText; } - return '' + return ''; } function splitDuration(duration) { if (duration) { - let estimationDate = null + let estimationDate = null; if (duration.includes('-')) { - estimationDate = duration.split('-') - estimationDate = parseInt(estimationDate[1]) + estimationDate = duration.split('-'); + estimationDate = parseInt(estimationDate[1]); } else { - estimationDate = parseInt(duration) + estimationDate = parseInt(duration); } - return estimationDate + return estimationDate; } - return '' + return ''; } const extractDuration = (text) => { - const matches = text.match(/\d+(?:-\d+)?/g) + const matches = text.match(/\d+(?:-\d+)?/g); if (matches && matches.length === 1) { - const parts = matches[0].split('-') - const min = parseInt(parts[0]) - const max = parseInt(parts[1]) + const parts = matches[0].split('-'); + const min = parseInt(parts[0]); + const max = parseInt(parts[1]); if (min === max) { - return min.toString() + return min.toString(); } - return matches[0] + return matches[0]; } - return '' -} + return ''; +}; const PickupAddress = ({ label }) => ( <div className='p-4'> @@ -1596,13 +1545,14 @@ const PickupAddress = ({ label }) => ( <div className='mt-4 text-caption-1'> <p className='font-medium'>Indoteknik</p> <p className='mt-2 mb-2 text-gray_r-11 leading-6'> - Jl. Bandengan Utara Raya No.85, RT.3/RW.16, Penjaringan, Kec. Penjaringan, Kota Jkt Utara, - Daerah Khusus Ibukota Jakarta, Indonesia Kodepos : 14440 + Jl. Bandengan Utara Raya No.85, RT.3/RW.16, Penjaringan, Kec. + Penjaringan, Kota Jkt Utara, Daerah Khusus Ibukota Jakarta, Indonesia + Kodepos : 14440 </p> <p className='mt-1 text-gray_r-11'>Telp : 021-2933 8828/29</p> <p className='mt-1 text-gray_r-11'>Mobile : 0813 9000 7430</p> </div> </div> -) +); -export default Checkout +export default Checkout; diff --git a/src/lib/home/api/categoryHomeApi.js b/src/lib/home/api/categoryHomeApi.js index 9e7d1402..e5def608 100644 --- a/src/lib/home/api/categoryHomeApi.js +++ b/src/lib/home/api/categoryHomeApi.js @@ -1,11 +1,10 @@ -import odooApi from '@/core/api/odooApi' -import axios from 'axios' +import axios from 'axios'; const categoryHomeIdApi = async ({ id }) => { - // const dataCategoryHomeIdO = await odooApi('GET', `/api/v1/product/category-homepage?id=${id}`) - // console.log('ini adalah odoo', dataCategoryHomeIdO) - const dataCategoryHomeId = await axios(`${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/product-homepage?id=` + id) - return dataCategoryHomeId.data -} + const dataCategoryHomeId = await axios( + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/product-homepage?id=` + id + ); + return dataCategoryHomeId.data; +}; -export default categoryHomeIdApi +export default categoryHomeIdApi; diff --git a/src/lib/product/components/Product/ProductDesktop.jsx b/src/lib/product/components/Product/ProductDesktop.jsx index 5f034c09..4074f243 100644 --- a/src/lib/product/components/Product/ProductDesktop.jsx +++ b/src/lib/product/components/Product/ProductDesktop.jsx @@ -1,416 +1,441 @@ -import Image from '@/core/components/elements/Image/Image' -import Link from '@/core/components/elements/Link/Link' -import DesktopView from '@/core/components/views/DesktopView' -import currencyFormat from '@/core/utils/currencyFormat' -import { HeartIcon } from '@heroicons/react/24/outline' -import { useCallback, useEffect, useRef, useState } from 'react' -import LazyLoad from 'react-lazy-load' -import ProductSimilar from '../ProductSimilar' -import { toast } from 'react-hot-toast' -import { updateItemCart } from '@/core/utils/cart' -import { useRouter } from 'next/router' -import { createSlug } from '@/core/utils/slug' -import BottomPopup from '@/core/components/elements/Popup/BottomPopup' -import ProductCard from '../ProductCard' -import productSimilarApi from '../../api/productSimilarApi' -import whatsappUrl from '@/core/utils/whatsappUrl' -import odooApi from '@/core/api/odooApi' -import PromotionType from '@/lib/promotinProgram/components/PromotionType' -import useAuth from '@/core/hooks/useAuth' -import ImageNext from 'next/image' -import CountDown2 from '@/core/components/elements/CountDown/CountDown2' -import { LazyLoadComponent } from 'react-lazy-load-image-component' -import ColumnsSLA from './ColumnsSLA' -import { useProductCartContext } from '@/contexts/ProductCartContext' -import { Box, Skeleton, Tooltip } from '@chakra-ui/react' -import { Info } from 'lucide-react' -import Breadcrumb from './Breadcrumb' -import { sellingProductFormat } from '@/core/utils/formatValue' +import { useEffect, useRef, useState } from 'react'; +import ImageNext from 'next/image'; +import { LazyLoadComponent } from 'react-lazy-load-image-component'; +import { Box, Skeleton, Tooltip } from '@chakra-ui/react'; +import { HeartIcon } from '@heroicons/react/24/outline'; +import { Info } from 'lucide-react'; +import LazyLoad from 'react-lazy-load'; +import { toast } from 'react-hot-toast'; +import { useRouter } from 'next/router'; + +import Image from '@/core/components/elements/Image/Image'; +import Link from '@/core/components/elements/Link/Link'; +import DesktopView from '@/core/components/views/DesktopView'; +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; +import CountDown2 from '@/core/components/elements/CountDown/CountDown2'; + +import currencyFormat from '@/core/utils/currencyFormat'; +import { updateItemCart } from '@/core/utils/cart'; +import { createSlug } from '@/core/utils/slug'; +import whatsappUrl from '@/core/utils/whatsappUrl'; +import { sellingProductFormat } from '@/core/utils/formatValue'; + +import odooApi from '@/core/api/odooApi'; +import useAuth from '@/core/hooks/useAuth'; + +import { useProductCartContext } from '@/contexts/ProductCartContext'; + +import PromotionType from '@/lib/promotinProgram/components/PromotionType'; + +import ProductSimilar from '../ProductSimilar'; +import ProductCard from '../ProductCard'; +import productSimilarApi from '../../api/productSimilarApi'; +import ColumnsSLA from './ColumnsSLA'; +import Breadcrumb from './Breadcrumb'; + +import ProductPromoSection from '~/modules/product-promo/components/Section'; const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { - const router = useRouter() - const auth = useAuth() - const { slug } = router.query + const router = useRouter(); + const auth = useAuth(); + const { slug } = router.query; - const [quantityActive, setQuantity] = useState(null) - const [lowestPrice, setLowestPrice] = useState(null) - const [product, setProducts] = useState(products) + const [quantityActive, setQuantity] = useState(null); + const [lowestPrice, setLowestPrice] = useState(null); + const [product, setProducts] = useState(products); - const [addCartAlert, setAddCartAlert] = useState(false) - const [isLoadingSLA, setIsLoadingSLA] = useState(true) - const [promotionType, setPromotionType] = useState(false) - const [promotionActiveId, setPromotionActiveId] = useState(null) - const [selectVariantPromoActive, setSelectVariantPromoActive] = useState(null) - const [backgorundFlashSale, setBackgorundFlashSale] = useState(null) + const [addCartAlert, setAddCartAlert] = useState(false); + const [isLoadingSLA, setIsLoadingSLA] = useState(true); + const [promotionType, setPromotionType] = useState(false); + const [promotionActiveId, setPromotionActiveId] = useState(null); + const [selectVariantPromoActive, setSelectVariantPromoActive] = + useState(null); + const [backgorundFlashSale, setBackgorundFlashSale] = useState(null); - const { setRefreshCart, refreshCart } = useProductCartContext() + const { setRefreshCart, refreshCart } = useProductCartContext(); useEffect(() => { - setLowestPrice({ price: product?.lowestPrice }) - }, [product]) + setLowestPrice({ price: product?.lowestPrice }); + }, [product]); useEffect(() => { const getBackgound = async () => { - const get = await odooApi('GET', '/api/v1/banner?type=flash-sale-background-banner') - setBackgorundFlashSale(get[0].image) - } - getBackgound() - }, []) + const get = await odooApi( + 'GET', + '/api/v1/banner?type=flash-sale-background-banner' + ); + setBackgorundFlashSale(get[0].image); + }; + getBackgound(); + }, []); - const [informationTab, setInformationTab] = useState(informationTabOptions[0].value) + const [informationTab, setInformationTab] = useState( + informationTabOptions[0].value + ); - const variantQuantityRefs = useRef([]) + const variantQuantityRefs = useRef([]); const setVariantQuantityRef = (variantId) => (element) => { if (element) { - let variantIndex = product.variants.findIndex((varian) => varian.id == variantId) - product.variants[variantIndex].quantity = element?.value + let variantIndex = product.variants.findIndex( + (varian) => varian.id == variantId + ); + product.variants[variantIndex].quantity = element?.value; } - variantQuantityRefs.current[variantId] = element - } + variantQuantityRefs.current[variantId] = element; + }; const validQuantity = (quantity) => { - let isValid = true + let isValid = true; if (!quantity || quantity < 1 || isNaN(parseInt(quantity))) { - toast.error('Jumlah barang minimal 1') - isValid = false + toast.error('Jumlah barang minimal 1'); + isValid = false; } - return isValid - } + return isValid; + }; const updateCart = (variantId, quantity, source) => { let dataUpdate = { productId: variantId, quantity, selected: true, - source: source === 'buy' ? 'buy' : null - } + source: source === 'buy' ? 'buy' : null, + }; if (product.variants.length > 1) { - let variantIndex = product.variants.findIndex((varian) => varian.id == variantId) - dataUpdate['programLineId'] = product.variants[variantIndex].programActive + let variantIndex = product.variants.findIndex( + (varian) => varian.id == variantId + ); + dataUpdate['programLineId'] = + product.variants[variantIndex].programActive; } else { - dataUpdate['programLineId'] = promotionActiveId + dataUpdate['programLineId'] = promotionActiveId; } - updateItemCart(dataUpdate) - } + updateItemCart(dataUpdate); + }; const redirectToLogin = (action, variantId, quantity) => { - const nextURL = `/shop/product/${slug}?action=${action}&variantId=${variantId}&qty=${quantity}` - router.push(`/login?next=${encodeURIComponent(nextURL)}`) - return true - } + const nextURL = `/shop/product/${slug}?action=${action}&variantId=${variantId}&qty=${quantity}`; + router.push(`/login?next=${encodeURIComponent(nextURL)}`); + return true; + }; const handleAddToCart = (variantId) => { - const quantity = variantQuantityRefs.current[variantId].value + const quantity = variantQuantityRefs.current[variantId].value; - if (!validQuantity(quantity)) return + if (!validQuantity(quantity)) return; if (!auth) { - return redirectToLogin('add_to_cart', variantId, quantity) + return redirectToLogin('add_to_cart', variantId, quantity); } - let source = 'cart' - updateCart(variantId, quantity, source) - setRefreshCart(true) - setAddCartAlert(true) - } + let source = 'cart'; + updateCart(variantId, quantity, source); + setRefreshCart(true); + setAddCartAlert(true); + }; const handleQuantityChange = (variantId) => (event) => { - const { value } = event.target - const variantIndex = product.variants.findIndex((variant) => variant.id === variantId) + const { value } = event.target; + const variantIndex = product.variants.findIndex( + (variant) => variant.id === variantId + ); if (variantIndex !== -1) { - product.variants[variantIndex].quantity = parseInt(value, 10) // Pastikan untuk mengubah ke tipe number jika diperlukan + product.variants[variantIndex].quantity = parseInt(value, 10); // Pastikan untuk mengubah ke tipe number jika diperlukan // Lakukan sesuatu jika nilai quantity diubah } - } + }; const handleBuy = (variant) => { - const quantity = variantQuantityRefs.current[variant].value - if (!validQuantity(quantity)) return + const quantity = variantQuantityRefs.current[variant].value; + if (!validQuantity(quantity)) return; if (!auth) { - return redirectToLogin('buy', variant, quantity) + return redirectToLogin('buy', variant, quantity); } - let source = 'buy' - updateCart(variant, quantity, source) - router.push(`/shop/checkout?source=buy`) - } + let source = 'buy'; + updateCart(variant, quantity, source); + router.push(`/shop/checkout?source=buy`); + }; - const variantSectionRef = useRef(null) + const variantSectionRef = useRef(null); const goToVariantSection = () => { if (variantSectionRef.current) { - const position = variantSectionRef.current.getBoundingClientRect() + const position = variantSectionRef.current.getBoundingClientRect(); window.scrollTo({ top: position.top - 120 + window.pageYOffset, - behavior: 'smooth' - }) + behavior: 'smooth', + }); } - } + }; const handlePromoClick = (variantId) => { - setSelectVariantPromoActive(variantId) - setPromotionType(true) - } + setSelectVariantPromoActive(variantId); + setPromotionType(true); + }; const productSimilarQuery = [ product?.name, `fq=-product_id_i:${product.id}`, - `fq=-manufacture_id_i:${product.manufacture?.id || 0}` - ].join('&') + `fq=-manufacture_id_i:${product.manufacture?.id || 0}`, + ].join('&'); - const [productSimilarInBrand, setProductSimilarInBrand] = useState(null) + const [productSimilarInBrand, setProductSimilarInBrand] = useState(null); useEffect(() => { const loadProductSimilarInBrand = async () => { - const productSimilarQuery = [product?.name, `fq=-product_id_i:${product.id}`].join('&') - const source = 'right' - const dataProductSimilar = await productSimilarApi({ query: productSimilarQuery, source }) - setProductSimilarInBrand(dataProductSimilar.products) - } - if (!productSimilarInBrand) loadProductSimilarInBrand() - }, [product, productSimilarInBrand]) + const productSimilarQuery = [ + product?.name, + `fq=-product_id_i:${product.id}`, + ].join('&'); + const source = 'right'; + const dataProductSimilar = await productSimilarApi({ + query: productSimilarQuery, + source, + }); + setProductSimilarInBrand(dataProductSimilar.products); + }; + if (!productSimilarInBrand) loadProductSimilarInBrand(); + }, [product, productSimilarInBrand]); useEffect(() => { const fetchData = async () => { const promises = product.variants.map(async (variant) => { - const dataSLA = await odooApi('GET', `/api/v1/product_variant/${variant.id}/stock`) + const dataSLA = await odooApi( + 'GET', + `/api/v1/product_variant/${variant.id}/stock` + ); return { ...variant, - sla: dataSLA - } - }) - const variantData = await Promise.all(promises) - product.variants = variantData + sla: dataSLA, + }; + }); + const variantData = await Promise.all(promises); + product.variants = variantData; - setIsLoadingSLA(false) - } - if (product.variantTotal == 1) fetchData() - }, [product]) + setIsLoadingSLA(false); + }; + if (product.variantTotal == 1) fetchData(); + }, [product]); return ( <DesktopView> <div className='container mx-auto pt-10'> <Breadcrumb productId={product.id} productName={product.name} /> - <div className='flex'> - <div className='w-full flex flex-wrap'> - <div className='w-5/12'> - <div className='relative mb-2'> - {product?.flashSale?.remainingTime > 0 && - lowestPrice?.price.discountPercentage > 0 && ( - <div className={`absolute bottom-0 w-full`}> - <div className='absolute bottom-0 w-full h-full'> - <ImageNext - src={backgorundFlashSale || '/images/GAMBAR-BG-FLASH-SALE.jpg'} - width={1000} - height={100} - /> - </div> - <div className='relative'> - <div className='flex gap-x-2 items-center p-2'> - <div className='bg-yellow-400 rounded-full p-1 h-9 w-20 flex items-center justify-center '> - <span className='text-lg font-bold'> - {Math.floor(product.lowestPrice.discountPercentage)}% - </span> - </div> - <div - className={`bg-red-600 border border-solid border-yellow-400 rounded-full h-9 p-2 flex w-[50%] items-center justify-center gap-x-4`} - > - <ImageNext - src='/images/ICON_FLASH_SALE_WEBSITE_INDOTEKNIK.svg' - width={17} - height={10} - /> - <span className='text-white text-lg font-semibold'> - {product?.flashSale?.tag != 'false' || product?.flashSale?.tag - ? product?.flashSale?.tag - : 'FLASH SALE'} - </span> - </div> - <div> - <CountDown2 initialTime={product.flashSale.remainingTime} /> - </div> + + <div className='w-full flex flex-wrap'> + <div className='w-3/12'> + <div className='relative mb-2'> + {product?.flashSale?.remainingTime > 0 && + lowestPrice?.price.discountPercentage > 0 && ( + <div className={`absolute bottom-0 w-full`}> + <div className='absolute bottom-0 w-full h-full'> + <ImageNext + src={ + backgorundFlashSale || + '/images/GAMBAR-BG-FLASH-SALE.jpg' + } + width={1000} + height={100} + /> + </div> + <div className='relative'> + <div className='flex gap-x-2 items-center p-2'> + <div className='bg-yellow-400 rounded-full p-1 h-9 w-20 flex items-center justify-center '> + <span className='text-lg font-bold'> + {Math.floor(product.lowestPrice.discountPercentage)} + % + </span> + </div> + <div + className={`bg-red-600 border border-solid border-yellow-400 rounded-full h-9 p-2 flex w-[50%] items-center justify-center gap-x-4`} + > + <ImageNext + src='/images/ICON_FLASH_SALE_WEBSITE_INDOTEKNIK.svg' + width={17} + height={10} + /> + <span className='text-white text-lg font-semibold'> + {product?.flashSale?.tag != 'false' || + product?.flashSale?.tag + ? product?.flashSale?.tag + : 'FLASH SALE'} + </span> + </div> + <div> + <CountDown2 + initialTime={product.flashSale.remainingTime} + /> </div> </div> </div> - )} - <Image - src={product.image} - alt={product.name} - className='h-[430px] object-contain object-center w-full border border-gray_r-4' - /> - </div> - <div> - <p className='text-justify text-xs leading-5'> - <span className='font-semibold '>Keterangan : </span>Gambar atau foto berperan - sebagai ilustrasi produk. Kadang tidak sesuai dengan kondisi terbaru dengan - berbagai perubahan dan perbaikan. Hubungi tim sales kami untuk informasi yang - lebih baik perihal gambar di 021-2933 8828. - </p> - </div> - </div> - - <div className='w-7/12 px-4'> - <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> - <div className='flex p-3'> - <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.variants.length > 1 && ( - <button - type='button' - onClick={goToVariantSection} - className={`flex gap-x-1 items-center p-2 rounded-lg w-auto btn-light`} - > - <span className='text-red-600 text-sm'>Lihat Selengkapnya</span> - </button> - )} + )} + <Image + src={product.image} + alt={product.name} + className='h-[430px] object-contain object-center w-full border border-gray_r-4' + /> + </div> + <div> + <p className='text-justify text-xs leading-5'> + <span className='font-semibold '>Keterangan : </span>Gambar atau + foto berperan sebagai ilustrasi produk. Kadang tidak sesuai + dengan kondisi terbaru dengan berbagai perubahan dan perbaikan. + Hubungi tim sales kami untuk informasi yang lebih baik perihal + gambar di 021-2933 8828. + </p> + </div> + </div> - {product.variants.length === 1 && ( - <> - {!product.variants[0]?.sla && <Skeleton width='20%' height='16px' />} - {product.variants[0]?.sla && ( - <Tooltip - placement='top' - label={`Masa Persiapan Barang ${product.variants[0]?.sla?.slaDate}`} - > - <Box className='w-fit flex items-center gap-x-2'> - {product.variants[0]?.sla?.slaDate} - <Info size={16} /> - </Box> - </Tooltip> - )} - </> - )} - </div> + <div className='w-6/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> + <div className='flex p-3'> + <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.variants.length > 1 && ( + <button + type='button' + onClick={goToVariantSection} + className={`flex gap-x-1 items-center p-2 rounded-lg w-auto btn-light`} + > + <span className='text-red-600 text-sm'> + Lihat Selengkapnya + </span> + </button> + )} - {product.variants.length === 1 && ( - <div className='flex p-3 '> - <div className='w-4/12 text-gray_r-12/70'>Stock</div> - <div className='w-8/12'> - {!product.variants[0]?.sla && <Skeleton width='10%' height='16px' />} - {product?.variants[0].sla?.qty > 0 && ( - <span>{product?.variants[0].sla?.qty}</span> + {product.variants.length === 1 && ( + <> + {!product.variants[0]?.sla && ( + <Skeleton width='20%' height='16px' /> )} - {product?.variants[0].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' + {product.variants[0]?.sla && ( + <Tooltip + placement='top' + label={`Masa Persiapan Barang ${product.variants[0]?.sla?.slaDate}`} > - Tanya Admin - </a> + <Box className='w-fit flex items-center gap-x-2'> + {product.variants[0]?.sla?.slaDate} + <Info size={16} /> + </Box> + </Tooltip> )} - </div> - </div> - )} + </> + )} + </div> + </div> - <div className={`flex p-3 ${product.variants.length > 1 ? '' : 'bg-gray_r-4'} `}> - <div className='w-4/12 text-gray_r-12/70'>Berat Barang</div> + {product.variants.length === 1 && ( + <div className='flex p-3 '> + <div className='w-4/12 text-gray_r-12/70'>Stock</div> <div className='w-8/12'> - {product?.weight > 0 && <span>{product?.weight} KG</span>} - {product?.weight == 0 && ( + {!product.variants[0]?.sla && ( + <Skeleton width='10%' height='16px' /> + )} + {product?.variants[0].sla?.qty > 0 && ( + <span>{product?.variants[0].sla?.qty}</span> + )} + {product?.variants[0].sla?.qty == 0 && ( <a - href={whatsappUrl('productWeight', { + href={whatsappUrl('product', { name: product.name, - url: createSlug('/shop/product/', product.name, product.id, true) + manufacture: product?.manufacture?.name, + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ), })} className='text-danger-500 font-medium' > - Tanya Berat + Tanya Admin </a> )} </div> </div> - {product.variants.length <= 1 && ( - <div className='pt-3'> - <div className='flex mt-1'> - <PromotionType - variantId={product.variants[0].id} - setPromotionActiveId={setPromotionActiveId} - promotionActiveId={promotionActiveId} - quantity={quantityActive} - product={product} - ></PromotionType> - </div> - </div> - )} - </div> - </div> + )} - <div className='w-full'> - <div className='mt-12'> - <div className='text-h-lg font-semibold'>Informasi Produk</div> - <div className='flex gap-x-4 mt-6 mb-4'> - {informationTabOptions.map((option) => ( - <TabButton - value={option.value} - key={option.value} - active={informationTab == option.value} - onClick={() => setInformationTab(option.value)} + <div + className={`flex p-3 ${ + product.variants.length > 1 ? '' : '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>} + {product?.weight == 0 && ( + <a + href={whatsappUrl('productWeight', { + name: product.name, + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ), + })} + className='text-danger-500 font-medium' > - {option.label} - </TabButton> - ))} + Tanya Berat + </a> + )} </div> - <div className='flex'> - <div className='w-3/4 leading-8 product__description'> - <TabContent active={informationTab == 'description'}> - <span - dangerouslySetInnerHTML={{ - __html: - product.description != '' - ? product.description - : 'Belum ada deskripsi produk.' - }} - /> - </TabContent> - - <TabContent active={informationTab == 'information'}> - Belum ada informasi. - </TabContent> + </div> + {product.variants.length <= 1 && ( + <div className='pt-3'> + <div className='flex mt-1'> + <PromotionType + variantId={product.variants[0].id} + setPromotionActiveId={setPromotionActiveId} + promotionActiveId={promotionActiveId} + quantity={quantityActive} + product={product} + ></PromotionType> + <ProductPromoSection productId={product.variants[0].id} /> </div> </div> - </div> + )} </div> </div> - <div className='w-[30%]'> - {product.variants.length > 1 && product.lowestPrice.priceDiscount > 0 && ( - <div className='text-gray_r-12/80'>Harga mulai dari: </div> - )} + <div className='w-3/12'> + {product.variants.length > 1 && + product.lowestPrice.priceDiscount > 0 && ( + <div className='text-gray_r-12/80'>Harga mulai dari: </div> + )} {/* {lowestPrice?.discountPercentage > 0 && ( <div className='flex gap-x-1 items-center mt-2'> @@ -441,7 +466,8 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { {sellingProductFormat(product?.qtySold) + ' Terjual'} </div> )} - {product?.flashSale?.id && lowestPrice?.price.discountPercentage > 0 ? ( + {product?.flashSale?.id && + lowestPrice?.price.discountPercentage > 0 ? ( <> <div className='flex gap-x-1 items-center mt-2'> <div className='badge-solid-red text-caption-1'> @@ -456,7 +482,10 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { </div> <div className='text-gray_r-9 text-base font-normal mt-1'> Termasuk PPN:{' '} - {currencyFormat(lowestPrice?.price.priceDiscount * process.env.NEXT_PUBLIC_PPN)} + {currencyFormat( + lowestPrice?.price.priceDiscount * + process.env.NEXT_PUBLIC_PPN + )} </div> </> ) : ( @@ -466,7 +495,9 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { {currencyFormat(lowestPrice?.price.price)} <div className='text-gray_r-9 text-base font-normal mt-1'> Termasuk PPN:{' '} - {currencyFormat(lowestPrice?.price.price * process.env.NEXT_PUBLIC_PPN)} + {currencyFormat( + lowestPrice?.price.price * process.env.NEXT_PUBLIC_PPN + )} </div> </> ) : ( @@ -476,7 +507,12 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { href={whatsappUrl('product', { name: product.name, manufacture: product.manufacture?.name, - url: createSlug('/shop/product/', product.name, product.id, true) + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ), })} className='text-danger-500 underline' rel='noopener noreferrer' @@ -524,7 +560,10 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { )} <div className='flex mt-4'> - <button className='flex items-center gap-x-1' onClick={toggleWishlist}> + <button + className='flex items-center gap-x-1' + onClick={toggleWishlist} + > {wishlist.data?.productTotal > 0 ? ( <HeartIcon className='w-6 fill-danger-500 text-danger-500' /> ) : ( @@ -538,7 +577,7 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { <div className='font-medium text-center p-4 bg-gray_r-1 border-b border-gray_r-6 sticky top-0 z-10'> Produk Serupa </div> - <div className='h-full divide-y divide-gray_r-6 max-h-96'> + <div className='h-full divide-y divide-gray_r-6 max-h-[550px]'> {productSimilarInBrand && productSimilarInBrand?.map((product) => ( <div className='py-2' key={product.id}> @@ -550,6 +589,42 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { </div> </div> + <div className='w-full'> + <div className='mt-12'> + <div className='text-h-lg font-semibold'>Informasi Produk</div> + <div className='flex gap-x-4 mt-6 mb-4'> + {informationTabOptions.map((option) => ( + <TabButton + value={option.value} + key={option.value} + active={informationTab == option.value} + onClick={() => setInformationTab(option.value)} + > + {option.label} + </TabButton> + ))} + </div> + <div className='flex'> + <div className='w-3/4 leading-8 product__description'> + <TabContent active={informationTab == 'description'}> + <span + dangerouslySetInnerHTML={{ + __html: + product.description != '' + ? product.description + : 'Belum ada deskripsi produk.', + }} + /> + </TabContent> + + <TabContent active={informationTab == 'information'}> + Belum ada informasi. + </TabContent> + </div> + </div> + </div> + </div> + {product.variants.length > 1 && ( <div className='mt-12' ref={variantSectionRef}> <div className='text-h-lg font-semibold mb-6'>Varian Produk</div> @@ -571,7 +646,9 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { <tr key={variant.id}> <td className='flex items-center justify-center gap-x-1'> {variant.isFlashsale && ( - <span className='blink-color-flash-sale'>🗲</span> + <span className='blink-color-flash-sale'> + 🗲 + </span> )} {variant.code} </td> @@ -580,11 +657,13 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { <ColumnsSLA variant={variant} product={product} /> </LazyLoadComponent> <td> - {variant.isFlashsale && variant?.price?.discountPercentage > 0 ? ( + {variant.isFlashsale && + variant?.price?.discountPercentage > 0 ? ( <> <div className='flex items-center gap-x-1 justify-center'> <div className='badge-solid-red text-caption-1'> - {Math.floor(variant?.price?.discountPercentage)}% + {Math.floor(variant?.price?.discountPercentage)} + % </div> <div className='line-through text-caption-1 text-gray_r-11'> {currencyFormat(variant?.price?.price)} @@ -596,7 +675,8 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { <div className=' text-caption-1 text-gray_r-11 mb-1'> Inc. PPN:{' '} {currencyFormat( - variant.price.priceDiscount * process.env.NEXT_PUBLIC_PPN + variant.price.priceDiscount * + process.env.NEXT_PUBLIC_PPN )} </div> </> @@ -610,7 +690,8 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { <div className=' text-caption-1 text-gray_r-11 mb-1'> Inc. PPN:{' '} {currencyFormat( - variant?.price?.price * process.env.NEXT_PUBLIC_PPN + variant?.price?.price * + process.env.NEXT_PUBLIC_PPN )} </div> </> @@ -619,7 +700,12 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { href={whatsappUrl('product', { name: variant.name, manufacture: product.manufacture?.name, - url: createSlug('/shop/product/', product.name, product.id, true) + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ), })} className='text-red_r-11' rel='noopener noreferrer' @@ -705,11 +791,14 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { )} <div className='my-12'> - <div className='text-h-lg font-semibold mb-6'>Kamu Mungkin Juga Suka</div> + <div className='text-h-lg font-semibold mb-6'> + Kamu Mungkin Juga Suka + </div> <LazyLoad> <ProductSimilar query={productSimilarQuery} /> </LazyLoad> </div> + <BottomPopup className=' !h-[75%]' title='Pakai Promo' @@ -728,6 +817,7 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { ></PromotionType> </div> </BottomPopup> + <BottomPopup className='!container' title='Berhasil Ditambahkan' @@ -742,16 +832,23 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { className='h-32 object-contain object-center w-full border border-gray_r-4' /> </div> - <div className='ml-3 flex flex-1 items-center font-normal'>{product.name}</div> + <div className='ml-3 flex flex-1 items-center font-normal'> + {product.name} + </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'> + <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> + <div className='text-h-sm font-semibold mb-6'> + Kamu Mungkin Juga Suka + </div> <LazyLoad> <ProductSimilar query={productSimilarQuery} /> </LazyLoad> @@ -759,29 +856,33 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { </BottomPopup> </div> </DesktopView> - ) -} + ); +}; const informationTabOptions = [ { value: 'description', label: 'Deskripsi' }, - { value: 'information', label: 'Info Penting' } -] + { value: 'information', label: 'Info Penting' }, +]; const TabButton = ({ children, active, ...props }) => { const activeClassName = active ? 'text-danger-500 underline underline-offset-4' - : 'text-gray_r-12/80' + : 'text-gray_r-12/80'; return ( - <button {...props} type='button' className={`font-medium ${activeClassName}`}> + <button + {...props} + type='button' + className={`font-medium ${activeClassName}`} + > {children} </button> - ) -} + ); +}; const TabContent = ({ children, active, className = '', ...props }) => ( <div {...props} className={`${active ? 'block' : 'hidden'} ${className}`}> {children} </div> -) +); -export default ProductDesktop +export default ProductDesktop; diff --git a/src/lib/product/components/Product/ProductMobile.jsx b/src/lib/product/components/Product/ProductMobile.jsx index e23e2fb9..e9e64469 100644 --- a/src/lib/product/components/Product/ProductMobile.jsx +++ b/src/lib/product/components/Product/ProductMobile.jsx @@ -1,60 +1,66 @@ -import Divider from '@/core/components/elements/Divider/Divider' -import Image from '@/core/components/elements/Image/Image' -import Link from '@/core/components/elements/Link/Link' -import currencyFormat from '@/core/utils/currencyFormat' -import { useEffect, useState } from 'react' -import Select from 'react-select' -import ProductSimilar from '../ProductSimilar' -import LazyLoad from 'react-lazy-load' -import { updateItemCart } from '@/core/utils/cart' -import { HeartIcon } from '@heroicons/react/24/outline' -import { useRouter } from 'next/router' -import MobileView from '@/core/components/views/MobileView' -import { toast } from 'react-hot-toast' -import { createSlug } from '@/core/utils/slug' -import BottomPopup from '@/core/components/elements/Popup/BottomPopup' -import whatsappUrl from '@/core/utils/whatsappUrl' -import PromotionType from '@/lib/promotinProgram/components/PromotionType' -import { gtagAddToCart } from '@/core/utils/googleTag' -import odooApi from '@/core/api/odooApi' -import ImageNext from 'next/image' -import CountDown2 from '@/core/components/elements/CountDown/CountDown2' -import Breadcrumb from './Breadcrumb' -import useAuth from '@/core/hooks/useAuth' -import { sellingProductFormat } from '@/core/utils/formatValue' +import Divider from '@/core/components/elements/Divider/Divider'; +import Image from '@/core/components/elements/Image/Image'; +import Link from '@/core/components/elements/Link/Link'; +import currencyFormat from '@/core/utils/currencyFormat'; +import { useEffect, useState } from 'react'; +import Select from 'react-select'; +import ProductSimilar from '../ProductSimilar'; +import LazyLoad from 'react-lazy-load'; +import { updateItemCart } from '@/core/utils/cart'; +import { HeartIcon } from '@heroicons/react/24/outline'; +import { useRouter } from 'next/router'; +import MobileView from '@/core/components/views/MobileView'; +import { toast } from 'react-hot-toast'; +import { createSlug } from '@/core/utils/slug'; +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; +import whatsappUrl from '@/core/utils/whatsappUrl'; +import PromotionType from '@/lib/promotinProgram/components/PromotionType'; +import { gtagAddToCart } from '@/core/utils/googleTag'; +import odooApi from '@/core/api/odooApi'; +import ImageNext from 'next/image'; +import CountDown2 from '@/core/components/elements/CountDown/CountDown2'; +import Breadcrumb from './Breadcrumb'; +import useAuth from '@/core/hooks/useAuth'; +import { sellingProductFormat } from '@/core/utils/formatValue'; +import ProductPromoSection from '~/modules/product-promo/components/Section'; const ProductMobile = ({ product, wishlist, toggleWishlist }) => { - const router = useRouter() - const auth = useAuth() - const { slug } = router.query - - const [quantity, setQuantity] = useState('1') - const [selectedVariant, setSelectedVariant] = useState(null) - const [informationTab, setInformationTab] = useState(informationTabOptions[0].value) - const [addCartAlert, setAddCartAlert] = useState(false) - - const [isLoadingSLA, setIsLoadingSLA] = useState(true) - const [promotionType, setPromotionType] = useState(false) - const [promotionActiveId, setPromotionActiveId] = useState(null) - const [backgorundFlashSale, setBackgorundFlashSale] = useState(null) + const router = useRouter(); + const auth = useAuth(); + const { slug } = router.query; + + const [quantity, setQuantity] = useState('1'); + const [selectedVariant, setSelectedVariant] = useState(null); + const [informationTab, setInformationTab] = useState( + informationTabOptions[0].value + ); + const [addCartAlert, setAddCartAlert] = useState(false); + + const [isLoadingSLA, setIsLoadingSLA] = useState(true); + const [promotionType, setPromotionType] = useState(false); + const [promotionActiveId, setPromotionActiveId] = useState(null); + const [backgorundFlashSale, setBackgorundFlashSale] = useState(null); const getLowestPrice = () => { - const prices = product.variants.map((variant) => variant.price) + const prices = product.variants.map((variant) => variant.price); const lowest = prices.reduce((lowest, price) => { - return price.priceDiscount < lowest.priceDiscount ? price : lowest - }, prices[0]) - return lowest - } + return price.priceDiscount < lowest.priceDiscount ? price : lowest; + }, prices[0]); + return lowest; + }; useEffect(() => { const getBackgound = async () => { - const get = await odooApi('GET', '/api/v1/banner?type=flash-sale-background-banner') + const get = await odooApi( + 'GET', + '/api/v1/banner?type=flash-sale-background-banner' + ); if (get.length > 0) { - setBackgorundFlashSale(get[0].image) + setBackgorundFlashSale(get[0].image); } - } - getBackgound() - }, []) + }; + getBackgound(); + }, []); const [activeVariant, setActiveVariant] = useState({ id: null, @@ -64,40 +70,44 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { stock: product.stockTotal, weight: product.weight, hasProgram: false, - qtySold: product.qtySold - }) + qtySold: product.qtySold, + }); const variantOptions = product.variants?.map((variant) => { - let label = [] + let label = []; if (variant.isFlashsale) { - label.push("<span class='blink-color-flash-sale'>🗲</span>") + label.push("<span class='blink-color-flash-sale'>🗲</span>"); } if (variant.code) { - label.push(`[${variant.code}]`) + label.push(`[${variant.code}]`); } if (variant.attributes.length > 0) { - label.push(variant.attributes.join(', ')) + label.push(variant.attributes.join(', ')); } else { - label.push(product.name) + label.push(product.name); } return { value: variant.id, - label: label.join(' ') - } - }) + label: label.join(' '), + }; + }); useEffect(() => { if (!selectedVariant && variantOptions.length == 1) { - setSelectedVariant(variantOptions[0]) + setSelectedVariant(variantOptions[0]); } - }, [selectedVariant, variantOptions]) + }, [selectedVariant, variantOptions]); useEffect(() => { if (selectedVariant) { - const variant = product.variants.find((variant) => variant.id == selectedVariant.value) + const variant = product.variants.find( + (variant) => variant.id == selectedVariant.value + ); const variantAttributes = - variant.attributes.length > 0 ? ' - ' + variant.attributes.join(', ') : '' + variant.attributes.length > 0 + ? ' - ' + variant.attributes.join(', ') + : ''; const newActiveVariant = { id: variant.id, @@ -108,60 +118,63 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { weight: variant.weight, hasProgram: variant.hasProgram, isFlashsale: variant.isFlashsale, - qtySold: variant.qtySold - } + qtySold: variant.qtySold, + }; - setActiveVariant(newActiveVariant) + setActiveVariant(newActiveVariant); const fetchSLA = async () => { - const dataSLA = await odooApi('GET', `/api/v1/product_variant/${variant.id}/stock`) - setActiveVariant({ ...newActiveVariant, sla: dataSLA }) - } - fetchSLA() + const dataSLA = await odooApi( + 'GET', + `/api/v1/product_variant/${variant.id}/stock` + ); + setActiveVariant({ ...newActiveVariant, sla: dataSLA }); + }; + fetchSLA(); } - }, [selectedVariant, product]) + }, [selectedVariant, product]); const validAction = () => { - let isValid = true + let isValid = true; if (!selectedVariant) { - toast.error('Pilih varian terlebih dahulu') - isValid = false + toast.error('Pilih varian terlebih dahulu'); + isValid = false; } if (!quantity || quantity < 1 || isNaN(parseInt(quantity))) { - toast.error('Jumlah barang minimal 1') - isValid = false + toast.error('Jumlah barang minimal 1'); + isValid = false; } - return isValid - } + return isValid; + }; const redirectToLogin = (action) => { - const nextURL = `/shop/product/${slug}?action=${action}&variantId=${activeVariant.id}&qty=${quantity}` - router.push(`/login?next=${encodeURIComponent(nextURL)}`) - return true - } + const nextURL = `/shop/product/${slug}?action=${action}&variantId=${activeVariant.id}&qty=${quantity}`; + router.push(`/login?next=${encodeURIComponent(nextURL)}`); + return true; + }; const handleClickCart = () => { - if (!validAction()) return - gtagAddToCart(activeVariant, quantity) + if (!validAction()) return; + gtagAddToCart(activeVariant, quantity); if (!auth) { - return redirectToLogin('add_to_cart') + return redirectToLogin('add_to_cart'); } updateItemCart({ productId: activeVariant.id, quantity, programLineId: promotionActiveId, - selected: true - }) - setAddCartAlert(true) - } + selected: true, + }); + setAddCartAlert(true); + }; const handleClickBuy = () => { - if (!validAction()) return + if (!validAction()) return; if (!auth) { - return redirectToLogin('buy') + return redirectToLogin('buy'); } updateItemCart({ @@ -169,58 +182,62 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { quantity, programLineId: promotionActiveId, selected: true, - source: 'buy' - }) - router.push(`/shop/checkout?source=buy`) - } + source: 'buy', + }); + router.push(`/shop/checkout?source=buy`); + }; const productSimilarQuery = [ product?.name, `fq=-product_id_i:${product.id}`, - `fq=-manufacture_id_i:${product.manufacture?.id || 0}` - ].join('&') + `fq=-manufacture_id_i:${product.manufacture?.id || 0}`, + ].join('&'); return ( <MobileView> <Breadcrumb productId={product.id} productName={product.name} /> <div className='relative'> - {product?.flashSale?.remainingTime > 0 && activeVariant?.price.discountPercentage > 0 && ( - <div className={`absolute bottom-0 w-full`}> - <div className='absolute bottom-0 w-full'> - <ImageNext - src={backgorundFlashSale || '/images/GAMBAR-BG-FLASH-SALE.jpg'} - width={1000} - height={100} - /> - </div> - <div className='relative'> - <div className='flex gap-x-2 items-center p-2'> - <div className='bg-yellow-400 rounded-full p-1 h-9 w-20 flex items-center justify-center '> - <span className='text-lg font-bold'> - {Math.floor(product.lowestPrice.discountPercentage)}% - </span> - </div> - <div - className={`bg-red-600 border border-solid border-yellow-400 rounded-full h-9 p-2 flex w-[50%] items-center justify-center gap-x-4`} - > - <ImageNext - src='/images/ICON_FLASH_SALE_WEBSITE_INDOTEKNIK.svg' - width={17} - height={10} - /> - <span className='text-white text-lg font-semibold'> - {product?.flashSale?.tag != 'false' || product?.flashSale?.tag - ? product?.flashSale?.tag - : 'FLASH SALE'} - </span> - </div> - <div> - <CountDown2 initialTime={product.flashSale.remainingTime} /> + {product?.flashSale?.remainingTime > 0 && + activeVariant?.price.discountPercentage > 0 && ( + <div className={`absolute bottom-0 w-full`}> + <div className='absolute bottom-0 w-full'> + <ImageNext + src={ + backgorundFlashSale || '/images/GAMBAR-BG-FLASH-SALE.jpg' + } + width={1000} + height={100} + /> + </div> + <div className='relative'> + <div className='flex gap-x-2 items-center p-2'> + <div className='bg-yellow-400 rounded-full p-1 h-9 w-20 flex items-center justify-center '> + <span className='text-lg font-bold'> + {Math.floor(product.lowestPrice.discountPercentage)}% + </span> + </div> + <div + className={`bg-red-600 border border-solid border-yellow-400 rounded-full h-9 p-2 flex w-[50%] items-center justify-center gap-x-4`} + > + <ImageNext + src='/images/ICON_FLASH_SALE_WEBSITE_INDOTEKNIK.svg' + width={17} + height={10} + /> + <span className='text-white text-lg font-semibold'> + {product?.flashSale?.tag != 'false' || + product?.flashSale?.tag + ? product?.flashSale?.tag + : 'FLASH SALE'} + </span> + </div> + <div> + <CountDown2 initialTime={product.flashSale.remainingTime} /> + </div> </div> </div> </div> - </div> - )} + )} <Image src={product.image} alt={product.name} @@ -232,7 +249,11 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { <div className='flex items-end mb-2'> {product.manufacture?.name ? ( <Link - href={createSlug('/shop/brands/', product.manufacture?.name, product.manufacture?.id)} + href={createSlug( + '/shop/brands/', + product.manufacture?.name, + product.manufacture?.id + )} > {product.manufacture?.name} </Link> @@ -249,18 +270,25 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { </div> <h1 className='leading-6 font-medium mb-3'>{activeVariant?.name}</h1> {product?.qtySold > 0 && ( - <div className='text-gray_r-9'>{sellingProductFormat(activeVariant?.qtySold) + ' Terjual'}</div> + <div className='text-gray_r-9'> + {sellingProductFormat(activeVariant?.qtySold) + ' Terjual'} + </div> )} {product.variants.length > 1 && activeVariant.price.priceDiscount > 0 && !selectedVariant && ( - <div className='text-gray_r-12/80 text-caption-2 mt-2 mb-1'>Harga mulai dari: </div> + <div className='text-gray_r-12/80 text-caption-2 mt-2 mb-1'> + Harga mulai dari:{' '} + </div> )} - {activeVariant.isFlashsale && activeVariant?.price?.discountPercentage > 0 ? ( + {activeVariant.isFlashsale && + activeVariant?.price?.discountPercentage > 0 ? ( <> <div className='flex gap-x-1 items-center'> - <div className='badge-solid-red'>{Math.floor(activeVariant?.price?.discountPercentage)}%</div> + <div className='badge-solid-red'> + {Math.floor(activeVariant?.price?.discountPercentage)}% + </div> <div className='text-gray_r-11 line-through text-caption-1'> {currencyFormat(activeVariant?.price?.price)} </div> @@ -270,7 +298,9 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { </div> <div className='text-gray_r-9 text-base font-normal mt-1'> Termasuk PPN:{' '} - {currencyFormat(activeVariant?.price.priceDiscount * process.env.NEXT_PUBLIC_PPN)} + {currencyFormat( + activeVariant?.price.priceDiscount * process.env.NEXT_PUBLIC_PPN + )} </div> </> ) : ( @@ -280,7 +310,9 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { {currencyFormat(activeVariant?.price?.price)} <div className='text-gray_r-9 text-base font-normal mt-1'> Termasuk PPN:{' '} - {currencyFormat(activeVariant?.price.price * process.env.NEXT_PUBLIC_PPN)} + {currencyFormat( + activeVariant?.price.price * process.env.NEXT_PUBLIC_PPN + )} </div> </> ) : ( @@ -289,7 +321,12 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { <a href={whatsappUrl('product', { name: product.name, - url: createSlug('/shop/product/', product.name, product.id, true) + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ), })} className='text-danger-500 underline' > @@ -307,13 +344,17 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { <div> <label className='flex justify-between'> Pilih Varian: - <span className='text-gray_r-11'>{product?.variantTotal} Varian</span> + <span className='text-gray_r-11'> + {product?.variantTotal} Varian + </span> </label> <Select name='variant' classNamePrefix='form-select' options={variantOptions} - formatOptionLabel={({ label }) => <div dangerouslySetInnerHTML={{ __html: label }} />} + formatOptionLabel={({ label }) => ( + <div dangerouslySetInnerHTML={{ __html: label }} /> + )} className='mt-2' value={selectedVariant} onChange={(option) => setSelectedVariant(option)} @@ -342,15 +383,27 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { onChange={(e) => setQuantity(e.target.value)} /> </div> - <button type='button' className='btn-yellow flex-1' onClick={handleClickCart}> + <button + type='button' + className='btn-yellow flex-1' + onClick={handleClickCart} + > Keranjang </button> - <button type='button' className='btn-solid-red flex-1' onClick={handleClickBuy}> + <button + type='button' + className='btn-solid-red flex-1' + onClick={handleClickBuy} + > Beli </button> </div> + + <div className='h-4' /> </div> + <ProductPromoSection productId={activeVariant.id} /> + <Divider /> <div className='p-4'> @@ -380,12 +433,16 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { type='button' title={`Masa Persiapan Barang ${activeVariant?.sla?.slaDate}`} className={`flex gap-x-1 items-center p-2 h-8 rounded-lg w-full ${ - activeVariant?.sla?.slaDate === 'indent' ? 'bg-indigo-900' : 'btn-light' + activeVariant?.sla?.slaDate === 'indent' + ? 'bg-indigo-900' + : 'btn-light' }`} > <div className={`flex-1 text-sm ${ - activeVariant?.sla?.slaDate === 'indent' ? 'text-white' : '' + activeVariant?.sla?.slaDate === 'indent' + ? 'text-white' + : '' }`} > {activeVariant?.sla?.slaDate} @@ -397,7 +454,9 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { stroke='currentColor' stroke-width='1.5' className={`w-7 h-7 text-sm ${ - activeVariant?.sla?.slaDate === 'indent' ? 'text-white' : '' + activeVariant?.sla?.slaDate === 'indent' + ? 'text-white' + : '' }`} > <path @@ -436,7 +495,12 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { <a href={whatsappUrl('product', { name: product.name, - url: createSlug('/shop/product/', product.name, product.id, true) + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ), })} className='text-danger-500 font-medium' > @@ -445,12 +509,19 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { )} </SpecificationContent> <SpecificationContent label='Berat Barang'> - {activeVariant?.weight > 0 && <span>{activeVariant?.weight} KG</span>} + {activeVariant?.weight > 0 && ( + <span>{activeVariant?.weight} KG</span> + )} {activeVariant?.weight == 0 && ( <a href={whatsappUrl('productWeight', { name: product.name, - url: createSlug('/shop/product/', product.name, product.id, true) + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ), })} className='text-danger-500 font-medium' > @@ -464,7 +535,10 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { active={informationTab == 'description'} className='leading-6 text-gray_r-11' dangerouslySetInnerHTML={{ - __html: product.description != '' ? product.description : 'Belum ada deskripsi produk.' + __html: + product.description != '' + ? product.description + : 'Belum ada deskripsi produk.', }} /> </div> @@ -491,50 +565,63 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { className='h-20 object-contain object-center w-full border border-gray_r-4' /> </div> - <div className='ml-3 flex flex-1 items-center text-sm font-normal'>{product.name}</div> + <div className='ml-3 flex flex-1 items-center text-sm font-normal'> + {product.name} + </div> <div className='ml-3 flex items-center text-sm font-normal'> - <Link href='/shop/cart' className='flex-1 py-2 text-gray_r-12 btn-yellow'> + <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> + <div className='text-h-sm font-semibold mb-6'> + Kamu Mungkin Juga Suka + </div> <LazyLoad> <ProductSimilar query={productSimilarQuery} /> </LazyLoad> </div> </BottomPopup> </MobileView> - ) -} + ); +}; const informationTabOptions = [ { value: 'specification', label: 'Spesifikasi' }, { value: 'description', label: 'Deskripsi' }, - { value: 'information', label: 'Info Penting' } -] + { value: 'information', label: 'Info Penting' }, +]; const TabButton = ({ children, active, ...props }) => { - const activeClassName = active ? 'text-danger-500 underline underline-offset-4' : 'text-gray_r-11' + const activeClassName = active + ? 'text-danger-500 underline underline-offset-4' + : 'text-gray_r-11'; return ( - <button {...props} type='button' className={`font-medium pb-1 ${activeClassName}`}> + <button + {...props} + type='button' + className={`font-medium pb-1 ${activeClassName}`} + > {children} </button> - ) -} + ); +}; const TabContent = ({ children, active, className, ...props }) => ( <div {...props} className={`${active ? 'block' : 'hidden'} ${className}`}> {children} </div> -) +); const SpecificationContent = ({ children, label }) => ( <div className='flex justify-between p-3 items-center'> <span className='text-gray_r-11'>{label}</span> {children} </div> -) +); -export default ProductMobile +export default ProductMobile; diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx index 29bb987e..e2b961f1 100644 --- a/src/lib/product/components/ProductSearch.jsx +++ b/src/lib/product/components/ProductSearch.jsx @@ -1,124 +1,141 @@ -import { useEffect, useMemo, useState } from 'react' -import useProductSearch from '../hooks/useProductSearch' -import ProductCard from './ProductCard' -import Pagination from '@/core/components/elements/Pagination/Pagination' -import { toQuery } from 'lodash-contrib' -import _ from 'lodash' -import ProductSearchSkeleton from './Skeleton/ProductSearchSkeleton' -import ProductFilter from './ProductFilter' -import useActive from '@/core/hooks/useActive' -import MobileView from '@/core/components/views/MobileView' -import DesktopView from '@/core/components/views/DesktopView' -import NextImage from 'next/image' -import ProductFilterDesktop from './ProductFilterDesktop' -import { useRouter } from 'next/router' -import searchSpellApi from '@/core/api/searchSpellApi' -import Link from '@/core/components/elements/Link/Link' -import whatsappUrl from '@/core/utils/whatsappUrl' -import { HStack, Image, Tag, TagCloseButton, TagLabel } from '@chakra-ui/react' -import odooApi from '@/core/api/odooApi' -import { formatCurrency } from '@/core/utils/formatValue' -import axios from 'axios' -import Skeleton from 'react-loading-skeleton' -import { createSlug } from '@/core/utils/slug' - -const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) => { - const router = useRouter() - const { page = 1 } = query - const [q, setQ] = useState(query?.q || '*') - const [search, setSearch] = useState(query?.q || '*') - const [limit, setLimit] = useState(query?.limit || 30) - const [orderBy, setOrderBy] = useState(router.query?.orderBy || 'popular') - if (defaultBrand) query.brand = defaultBrand.toLowerCase() +import { useEffect, useMemo, useState } from 'react'; +import useProductSearch from '../hooks/useProductSearch'; +import ProductCard from './ProductCard'; +import Pagination from '@/core/components/elements/Pagination/Pagination'; +import { toQuery } from 'lodash-contrib'; +import _ from 'lodash'; +import ProductSearchSkeleton from './Skeleton/ProductSearchSkeleton'; +import ProductFilter from './ProductFilter'; +import useActive from '@/core/hooks/useActive'; +import MobileView from '@/core/components/views/MobileView'; +import DesktopView from '@/core/components/views/DesktopView'; +import NextImage from 'next/image'; +import ProductFilterDesktop from './ProductFilterDesktop'; +import { useRouter } from 'next/router'; +import searchSpellApi from '@/core/api/searchSpellApi'; +import Link from '@/core/components/elements/Link/Link'; +import whatsappUrl from '@/core/utils/whatsappUrl'; +import { HStack, Image, Tag, TagCloseButton, TagLabel } from '@chakra-ui/react'; +import odooApi from '@/core/api/odooApi'; +import { formatCurrency } from '@/core/utils/formatValue'; +import axios from 'axios'; +import { createSlug } from '@/core/utils/slug'; + +const ProductSearch = ({ + query, + prefixUrl, + defaultBrand = null, + brand = null, +}) => { + const router = useRouter(); + const { page = 1 } = query; + const [q, setQ] = useState(query?.q || '*'); + const [search, setSearch] = useState(query?.q || '*'); + const [limit, setLimit] = useState(query?.limit || 30); + const [orderBy, setOrderBy] = useState(router.query?.orderBy || 'popular'); + if (defaultBrand) query.brand = defaultBrand.toLowerCase(); const { productSearch } = useProductSearch({ query: { ...query, q, limit, orderBy }, - operation: 'AND' - }) - const [products, setProducts] = useState(null) - const [spellings, setSpellings] = useState(null) - const [bannerPromotionHeader, setBannerPromotionHeader] = useState(null) - const [bannerPromotionFooter, setBannerPromotionFooter] = useState(null) - const [isBrand, setIsBrand] = useState(null) - const popup = useActive() - const numRows = [30, 50, 80, 100] + operation: 'AND', + }); + const [products, setProducts] = useState(null); + const [spellings, setSpellings] = useState(null); + const [bannerPromotionHeader, setBannerPromotionHeader] = useState(null); + const [bannerPromotionFooter, setBannerPromotionFooter] = useState(null); + const [isBrand, setIsBrand] = useState(null); + const popup = useActive(); + const numRows = [30, 50, 80, 100]; const [brandValues, setBrand] = useState( - !router.pathname.includes('brands') ? (query.brand ? query.brand.split(',') : []) : [] - ) - const [categoryValues, setCategory] = useState(query?.category?.split(',') || []) - const [priceFrom, setPriceFrom] = useState(query?.priceFrom || null) - const [priceTo, setPriceTo] = useState(query?.priceTo || null) - - const pageCount = Math.ceil(productSearch.data?.response.numFound / limit) - const productStart = productSearch.data?.responseHeader.params.start - const productRows = limit - const productFound = productSearch.data?.response.numFound + !router.pathname.includes('brands') + ? query.brand + ? query.brand.split(',') + : [] + : [] + ); + const [categoryValues, setCategory] = useState( + query?.category?.split(',') || [] + ); + const [priceFrom, setPriceFrom] = useState(query?.priceFrom || null); + const [priceTo, setPriceTo] = useState(query?.priceTo || null); + + const pageCount = Math.ceil(productSearch.data?.response.numFound / limit); + const productStart = productSearch.data?.responseHeader.params.start; + const productRows = limit; + const productFound = productSearch.data?.response.numFound; useEffect(() => { if (productFound == 0 && query.q && !spellings) { searchSpellApi({ query: query.q }).then((response) => { const oddIndexSuggestions = response.data.spellcheck.suggestions.filter( (_, index) => index % 2 === 1 - ) + ); const oddIndexCollations = response.data.spellcheck.collations.filter( (_, index) => index % 2 === 1 - ) + ); const dataSpellings = oddIndexSuggestions.reduce((acc, curr) => { oddIndexCollations.forEach((collation) => { - acc.push(collation.collationQuery) - }) + acc.push(collation.collationQuery); + }); curr.suggestion.forEach((s) => { - if (!acc.includes(s.word)) acc.push(s.word) - }) - return acc - }, []) + if (!acc.includes(s.word)) acc.push(s.word); + }); + return acc; + }, []); if (dataSpellings.length > 0) { - setQ(dataSpellings[0]) + setQ(dataSpellings[0]); } - setSpellings(dataSpellings) - }) + setSpellings(dataSpellings); + }); } - }, [productFound, query, spellings]) + }, [productFound, query, spellings]); useEffect(() => { const checkIfBrand = async () => { const brand = await axios( `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/brands?params=search&q=${search}` - ) - console.log('ini brand', brand) + ); + if (brand.data.length > 0) { - setIsBrand(brand?.data[0]) + setIsBrand(brand?.data[0]); } else { - setIsBrand(null) + setIsBrand(null); } - } + }; if (router.pathname.includes('search')) { - checkIfBrand() + checkIfBrand(); } - }, [q]) + }, [q]); - const brands = [] + const brands = []; for ( let i = 0; i < productSearch.data?.facetCounts?.facetFields?.manufactureNameS.length; i += 2 ) { - const brand = productSearch.data?.facetCounts?.facetFields?.manufactureNameS[i] - const qty = productSearch.data?.facetCounts?.facetFields?.manufactureNameS[i + 1] + const brand = + productSearch.data?.facetCounts?.facetFields?.manufactureNameS[i]; + const qty = + productSearch.data?.facetCounts?.facetFields?.manufactureNameS[i + 1]; if (qty > 0) { - brands.push({ brand, qty }) + brands.push({ brand, qty }); } } - const categories = [] - for (let i = 0; i < productSearch.data?.facetCounts?.facetFields?.categoryName.length; i += 2) { - const name = productSearch.data?.facetCounts?.facetFields?.categoryName[i] - const qty = productSearch.data?.facetCounts?.facetFields?.categoryName[i + 1] + const categories = []; + for ( + let i = 0; + i < productSearch.data?.facetCounts?.facetFields?.categoryName.length; + i += 2 + ) { + const name = productSearch.data?.facetCounts?.facetFields?.categoryName[i]; + const qty = + productSearch.data?.facetCounts?.facetFields?.categoryName[i + 1]; if (qty > 0) { - categories.push({ name, qty }) + categories.push({ name, qty }); } } @@ -126,46 +143,54 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) { value: 'price-asc', label: 'Harga Terendah' }, { value: 'price-desc', label: 'Harga Tertinggi' }, { value: 'popular', label: 'Populer' }, - { value: 'stock', label: 'Ready Stock' } - ] + { value: 'stock', label: 'Ready Stock' }, + ]; const handleOrderBy = (e) => { let params = { ...router.query, - orderBy: e.target.value - } - params = _.pickBy(params, _.identity) - params = toQuery(params) - router.push(`${prefixUrl}?${params}`) - } + orderBy: e.target.value, + }; + params = _.pickBy(params, _.identity); + params = toQuery(params); + router.push(`${prefixUrl}?${params}`); + }; const handleLimit = (e) => { let params = { ...router.query, - limit: e.target.value - } - params = _.pickBy(params, _.identity) - params = toQuery(params) - router.push(`${prefixUrl}?${params}`) - } + limit: e.target.value, + }; + params = _.pickBy(params, _.identity); + params = toQuery(params); + router.push(`${prefixUrl}?${params}`); + }; const getBanner = async () => { if (router.pathname.includes('search')) { - const getBannerHeader = await odooApi('GET', '/api/v1/banner?type=promotion-header') - const getBannerFooter = await odooApi('GET', '/api/v1/banner?type=promotion-footer') - var randomIndex = Math.floor(Math.random() * getBannerHeader.length) - var randomIndexFooter = Math.floor(Math.random() * getBannerFooter.length) - setBannerPromotionHeader(getBannerHeader[randomIndex]) - setBannerPromotionFooter(getBannerFooter[randomIndexFooter]) + const getBannerHeader = await odooApi( + 'GET', + '/api/v1/banner?type=promotion-header' + ); + const getBannerFooter = await odooApi( + 'GET', + '/api/v1/banner?type=promotion-footer' + ); + var randomIndex = Math.floor(Math.random() * getBannerHeader.length); + var randomIndexFooter = Math.floor( + Math.random() * getBannerFooter.length + ); + setBannerPromotionHeader(getBannerHeader[randomIndex]); + setBannerPromotionFooter(getBannerFooter[randomIndexFooter]); } - } + }; useEffect(() => { - getBanner() - }, []) + getBanner(); + }, []); useEffect(() => { - setProducts(productSearch.data?.response?.products) - }, [productSearch]) + setProducts(productSearch.data?.response?.products); + }, [productSearch]); const SpellingComponent = useMemo(() => { return ( @@ -182,8 +207,8 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) </Link> ))} </> - ) - }, [spellings]) + ); + }, [spellings]); const handleDeleteFilter = async (source, value) => { let params = { @@ -192,41 +217,41 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) brand: brandValues.join(','), category: categoryValues.join(','), priceFrom, - priceTo - } + priceTo, + }; - let brands = brandValues - let catagories = categoryValues + let brands = brandValues; + let catagories = categoryValues; switch (source) { case 'brands': - brands = brandValues.filter((item) => item !== value) - params.brand = brands.join(',') - await setBrand(brands) - break + brands = brandValues.filter((item) => item !== value); + params.brand = brands.join(','); + await setBrand(brands); + break; case 'category': - catagories = categoryValues.filter((item) => item !== value) - params.category = catagories.join(',') - await setCategory(catagories) - break + catagories = categoryValues.filter((item) => item !== value); + params.category = catagories.join(','); + await setCategory(catagories); + break; case 'price': - params.priceFrom = null - params.priceTo = null - break + params.priceFrom = null; + params.priceTo = null; + break; case 'delete': params = { q: router.query.q, - orderBy: orderBy - } - break + orderBy: orderBy, + }; + break; } - handleSubmitFilter(params) - } + handleSubmitFilter(params); + }; const handleSubmitFilter = (params) => { - params = _.pickBy(params, _.identity) - params = toQuery(params) - router.push(`${prefixUrl}?${params}`) - } + params = _.pickBy(params, _.identity); + params = toQuery(params); + router.push(`${prefixUrl}?${params}`); + }; return ( <> @@ -235,8 +260,14 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) <div className='p-4 pt-0'> {isBrand && isBrand.logo && ( <div className='mb-3'> - <h1 className='mb-2 font-semibold text-h-sm'>Brand Pencarian {q}</h1> - <Image src={isBrand?.logo} alt='' className='object-cover object-center h-[60px]' /> + <h1 className='mb-2 font-semibold text-h-sm'> + Brand Pencarian {q} + </h1> + <Image + src={isBrand?.logo} + alt='' + className='object-cover object-center h-[60px]' + /> </div> )} <h1 className='mb-2 font-semibold text-h-sm'>Produk</h1> @@ -255,7 +286,8 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) {pageCount > 1 ? ( <> {productStart + 1}- - {parseInt(productStart) + parseInt(productRows) > productFound + {parseInt(productStart) + parseInt(productRows) > + productFound ? productFound : parseInt(productStart) + parseInt(productRows)} dari @@ -267,7 +299,8 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) produk{' '} {query.q && ( <> - untuk pencarian <span className='font-semibold'>{query.q}</span> + untuk pencarian{' '} + <span className='font-semibold'>{query.q}</span> </> )} </> @@ -279,7 +312,10 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) {productFound > 0 && ( <div className='flex items-center gap-x-2 mb-5 justify-between'> <div> - <button className='btn-light py-2 px-5 h-[40px]' onClick={popup.activate}> + <button + className='btn-light py-2 px-5 h-[40px]' + onClick={popup.activate} + > Filter </button> </div> @@ -303,7 +339,9 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) <div className='grid grid-cols-2 gap-3'> {products && - products.map((product) => <ProductCard product={product} key={product.id} />)} + products.map((product) => ( + <ProductCard product={product} key={product.id} /> + ))} </div> <Pagination @@ -329,7 +367,9 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) <div className='w-3/12'> {brand && ( <div className='p-4'> - <div className='text-caption-1 text-gray_r-11 mb-2'>Produk dari brand:</div> + <div className='text-caption-1 text-gray_r-11 mb-2'> + Produk dari brand: + </div> {brand?.data?.logo && ( <Image src={brand?.data?.logo} @@ -365,12 +405,18 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) {isBrand && isBrand.logo && ( <div className='mb-3'> - <h1 className='text-2xl mb-2 font-semibold'>Brand Pencarian {q}</h1> + <h1 className='text-2xl mb-2 font-semibold'> + Brand Pencarian {q} + </h1> <Link href={createSlug('/shop/brands/', isBrand.name, isBrand.id)} className='inline' > - <Image src={isBrand?.logo} alt='' className='object-cover object-center h-24' /> + <Image + src={isBrand?.logo} + alt='' + className='object-cover object-center h-24' + /> </Link> </div> )} @@ -391,7 +437,8 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) {pageCount > 1 ? ( <> {productStart + 1}- - {parseInt(productStart) + parseInt(productRows) > productFound + {parseInt(productStart) + parseInt(productRows) > + productFound ? productFound : parseInt(productStart) + parseInt(productRows)} dari @@ -403,7 +450,8 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) produk{' '} {query.q && ( <> - untuk pencarian <span className='font-semibold'>{query.q}</span> + untuk pencarian{' '} + <span className='font-semibold'>{query.q}</span> </> )} </> @@ -447,7 +495,9 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) {productSearch.isLoading && <ProductSearchSkeleton />} <div className='grid grid-cols-5 gap-x-3 gap-y-6'> {products && - products.map((product) => <ProductCard product={product} key={product.id} />)} + products.map((product) => ( + <ProductCard product={product} key={product.id} /> + ))} </div> <div className='flex justify-between items-center mt-6 mb-2'> <div className='pt-2 pb-6 flex items-center gap-x-3'> @@ -464,7 +514,7 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) href={ query?.q ? whatsappUrl('productSearch', { - name: query.q + name: query.q, }) : whatsappUrl() } @@ -496,40 +546,61 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) </div> </DesktopView> </> - ) -} + ); +}; -export default ProductSearch +export default ProductSearch; const FilterChoicesComponent = ({ brandValues, categoryValues, priceFrom, priceTo, - handleDeleteFilter + handleDeleteFilter, }) => ( <div className='flex items-center'> <HStack spacing={2} className='flex-wrap'> {brandValues?.map((value, index) => ( - <Tag size='lg' key={index} borderRadius='lg' variant='outline' colorScheme='gray'> + <Tag + size='lg' + key={index} + borderRadius='lg' + variant='outline' + colorScheme='gray' + > <TagLabel>{value}</TagLabel> <TagCloseButton onClick={() => handleDeleteFilter('brands', value)} /> </Tag> ))} {categoryValues?.map((value, index) => ( - <Tag size='lg' key={index} borderRadius='lg' variant='outline' colorScheme='gray'> + <Tag + size='lg' + key={index} + borderRadius='lg' + variant='outline' + colorScheme='gray' + > <TagLabel>{value}</TagLabel> - <TagCloseButton onClick={() => handleDeleteFilter('category', value)} /> + <TagCloseButton + onClick={() => handleDeleteFilter('category', value)} + /> </Tag> ))} {priceFrom && priceTo && ( <Tag size='lg' borderRadius='lg' variant='outline' colorScheme='gray'> - <TagLabel>{formatCurrency(priceFrom) + '-' + formatCurrency(priceTo)}</TagLabel> - <TagCloseButton onClick={() => handleDeleteFilter('price', priceFrom)} /> + <TagLabel> + {formatCurrency(priceFrom) + '-' + formatCurrency(priceTo)} + </TagLabel> + <TagCloseButton + onClick={() => handleDeleteFilter('price', priceFrom)} + /> </Tag> )} - {brandValues?.length > 0 || categoryValues?.length > 0 || priceFrom || priceTo ? ( + {brandValues?.length > 0 || + categoryValues?.length > 0 || + priceFrom || + priceTo ? ( <span> <button className='btn-transparent py-2 px-5 h-[40px] text-red-700' @@ -543,4 +614,4 @@ const FilterChoicesComponent = ({ )} </HStack> </div> -) +); diff --git a/src/pages/api/product-variant/[id].js b/src/pages/api/product-variant/[id].js new file mode 100644 index 00000000..4186a724 --- /dev/null +++ b/src/pages/api/product-variant/[id].js @@ -0,0 +1,2 @@ +import handler from '~/pages/api/product-variant/[id]'; +export default handler; diff --git a/src/pages/api/product-variant/[id]/promotion/[category].js b/src/pages/api/product-variant/[id]/promotion/[category].js new file mode 100644 index 00000000..aef03c22 --- /dev/null +++ b/src/pages/api/product-variant/[id]/promotion/[category].js @@ -0,0 +1,2 @@ +import handler from '~/pages/api/product-variant/[id]/promotion/[category]'; +export default handler; diff --git a/src/pages/api/product-variant/[id]/promotion/highlight.js b/src/pages/api/product-variant/[id]/promotion/highlight.js new file mode 100644 index 00000000..93b1e781 --- /dev/null +++ b/src/pages/api/product-variant/[id]/promotion/highlight.js @@ -0,0 +1,2 @@ +import handler from '~/pages/api/product-variant/[id]/promotion/highlight'; +export default handler;
\ No newline at end of file diff --git a/src/pages/api/promotion-program/[id].js b/src/pages/api/promotion-program/[id].js new file mode 100644 index 00000000..f2bb550e --- /dev/null +++ b/src/pages/api/promotion-program/[id].js @@ -0,0 +1,2 @@ +import handler from '~/pages/api/promotion-program/[id]'; +export default handler; diff --git a/src/pages/shop/cart.jsx b/src/pages/shop/cart.jsx index 2da58c96..34cae86a 100644 --- a/src/pages/shop/cart.jsx +++ b/src/pages/shop/cart.jsx @@ -1,14 +1,14 @@ -import Seo from '@/core/components/Seo' -import BasicLayout from '@/core/components/layouts/BasicLayout' -import DesktopView from '@/core/components/views/DesktopView' -import MobileView from '@/core/components/views/MobileView' -import IsAuth from '@/lib/auth/components/IsAuth' -import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react' -import dynamic from 'next/dynamic' -import Link from 'next/link' +import Seo from '@/core/components/Seo'; +import BasicLayout from '@/core/components/layouts/BasicLayout'; +import DesktopView from '@/core/components/views/DesktopView'; +import MobileView from '@/core/components/views/MobileView'; +import IsAuth from '@/lib/auth/components/IsAuth'; +import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react'; +import dynamic from 'next/dynamic'; +import Link from 'next/link'; -const AppLayout = dynamic(() => import('@/core/components/layouts/AppLayout')) -const CartComponent = dynamic(() => import('@/lib/cart/components/Cart')) +const AppLayout = dynamic(() => import('@/core/components/layouts/AppLayout')); +const CartDetail = dynamic(() => import('~/modules/cart/components/Detail')); export default function Cart() { return ( @@ -18,7 +18,9 @@ export default function Cart() { <IsAuth> <MobileView> <AppLayout title='Keranjang' withFooter={false}> - <CartComponent /> + <div className='p-4'> + <CartDetail /> + </div> </AppLayout> </MobileView> @@ -27,20 +29,29 @@ export default function Cart() { <div className='container mx-auto py-4 md:py-6 pb-0'> <Breadcrumb> <BreadcrumbItem> - <BreadcrumbLink as={Link} href='/' className='!text-danger-500 whitespace-nowrap'> + <BreadcrumbLink + as={Link} + href='/' + className='!text-danger-500 whitespace-nowrap' + > Home </BreadcrumbLink> </BreadcrumbItem> <BreadcrumbItem isCurrentPage> - <BreadcrumbLink className='whitespace-nowrap'>Keranjang</BreadcrumbLink> + <BreadcrumbLink className='whitespace-nowrap'> + Keranjang + </BreadcrumbLink> </BreadcrumbItem> </Breadcrumb> + + <div className='h-10' /> + + <CartDetail /> </div> - <CartComponent /> </BasicLayout> </DesktopView> </IsAuth> </> - ) + ); } diff --git a/src/pages/shop/product/[slug].jsx b/src/pages/shop/product/[slug].jsx index d8366d3c..667373b4 100644 --- a/src/pages/shop/product/[slug].jsx +++ b/src/pages/shop/product/[slug].jsx @@ -1,79 +1,83 @@ -import Seo from '@/core/components/Seo' -import LogoSpinner from '@/core/components/elements/Spinner/LogoSpinner' -import { getIdFromSlug } from '@/core/utils/slug' -import productApi from '@/lib/product/api/productApi' -import PageNotFound from '@/pages/404' -import dynamic from 'next/dynamic' -import { useRouter } from 'next/router' -import cookie from 'cookie' -import axios from 'axios' -import { useProductContext } from '@/contexts/ProductContext' -import { useEffect } from 'react' -import { updateItemCart } from '@/core/utils/cart' +import Seo from '@/core/components/Seo'; +import LogoSpinner from '@/core/components/elements/Spinner/LogoSpinner'; +import { getIdFromSlug } from '@/core/utils/slug'; +import productApi from '@/lib/product/api/productApi'; +import PageNotFound from '@/pages/404'; +import dynamic from 'next/dynamic'; +import { useRouter } from 'next/router'; +import cookie from 'cookie'; +import axios from 'axios'; +import { useProductContext } from '@/contexts/ProductContext'; +import { useEffect } from 'react'; +import { updateItemCart } from '@/core/utils/cart'; -const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout')) -const Product = dynamic(() => import('@/lib/product/components/Product/Product')) +const BasicLayout = dynamic(() => + import('@/core/components/layouts/BasicLayout') +); +const Product = dynamic(() => + import('@/lib/product/components/Product/Product') +); export async function getServerSideProps(context) { - const { slug } = context.query - const cookies = context.req.headers.cookie - const cookieObj = cookies ? cookie.parse(cookies) : {} - const auth = cookieObj.auth ? JSON.parse(cookieObj.auth) : {} - const tier = auth.pricelist ? auth.pricelist : false - const authToken = auth?.token || '' + const { slug } = context.query; + const cookies = context.req.headers.cookie; + const cookieObj = cookies ? cookie.parse(cookies) : {}; + const auth = cookieObj.auth ? JSON.parse(cookieObj.auth) : {}; + const tier = auth.pricelist ? auth.pricelist : false; let response = await axios( `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/product-detail?id=` + getIdFromSlug(slug) + '&auth=' + tier - ) - let product = response.data + ); + let product = response.data; // let productSolr = await productApi({ id: getIdFromSlug(slug), headers: { Token: authToken } }) // let productSolr = null if (product?.length == 1) { - product = product[0] + product = product[0]; } else { - product = null + product = null; } return { - props: { product } - } + props: { product }, + }; } export default function ProductDetail({ product }) { - const router = useRouter() - const { setProduct } = useProductContext() + const router = useRouter(); + const { setProduct } = useProductContext(); useEffect(() => { if (product) { - setProduct(product) + setProduct(product); } - }, [product, setProduct]) + }, [product, setProduct]); useEffect(() => { - const { action, variantId, qty } = router.query + const { action, variantId, qty } = router.query; const addToCart = async () => { const data = { productId: variantId, quantity: qty, selected: true, programLineId: null, - source: action - } - console.log('data dr test', data) - await updateItemCart(data) - const redirectURL = action === 'buy' ? '/shop/checkout?source=buy' : '/shop/cart' - router.push(redirectURL) - } + source: action, + }; + + await updateItemCart(data); + const redirectURL = + action === 'buy' ? '/shop/checkout?source=buy' : '/shop/cart'; + router.push(redirectURL); + }; if (action && variantId && qty) { - addToCart() + addToCart(); } - }, [router]) + }, [router]); - if (!product) return <PageNotFound /> + if (!product) return <PageNotFound />; return ( <BasicLayout> @@ -87,16 +91,16 @@ export default function ProductDetail({ product }) { url: product?.image, width: 800, height: 800, - alt: product?.name - } + alt: product?.name, + }, ], - type: 'product' + type: 'product', }} additionalMetaTags={[ { name: 'keywords', - content: `${product?.name}, Harga ${product?.name}, Beli ${product?.name}, Spesifikasi ${product?.name}` - } + content: `${product?.name}, Harga ${product?.name}, Beli ${product?.name}, Spesifikasi ${product?.name}`, + }, ]} /> {!product && ( @@ -106,5 +110,5 @@ export default function ProductDetail({ product }) { )} {product && <Product product={product} />} </BasicLayout> - ) + ); } |
