summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/bannerApi.js3
-rw-r--r--src/api/promoApi.js73
-rw-r--r--src/components/ui/HeroBanner.jsx78
-rw-r--r--src/core/components/ScrollToTop.jsx24
-rw-r--r--src/core/components/elements/Footer/BasicFooter.jsx127
-rw-r--r--src/core/components/elements/Footer/SimpleFooter.jsx2
-rw-r--r--src/core/components/elements/Navbar/NavbarDesktop.jsx238
-rw-r--r--src/core/components/elements/Navbar/NavbarMobile.jsx69
-rw-r--r--src/core/components/elements/Navbar/NavbarUserDropdown.jsx5
-rw-r--r--src/core/components/elements/Navbar/TopBanner.jsx61
-rw-r--r--src/core/components/elements/Skeleton/TopBannerSkeleton.jsx24
-rw-r--r--src/core/components/layouts/AppLayout.jsx18
-rw-r--r--src/core/components/layouts/BasicLayout.jsx90
-rw-r--r--src/core/utils/auth.js2
-rw-r--r--src/core/utils/googleTag.js26
-rw-r--r--src/core/utils/slug.js34
-rw-r--r--src/core/utils/whatsappUrl.js2
-rw-r--r--src/fonts/Inter/inter.css85
-rw-r--r--src/images/logo-idul-fitri.pngbin0 -> 10642 bytes
-rw-r--r--src/lib/auth/components/LoginDesktop.jsx5
-rw-r--r--src/lib/auth/components/LoginMobile.jsx4
-rw-r--r--src/lib/auth/hooks/useLogin.js2
-rw-r--r--src/lib/brand/components/BrandCard.jsx4
-rw-r--r--src/lib/cart/components/Cartheader.jsx181
-rw-r--r--src/lib/checkout/components/Checkout.jsx1000
-rw-r--r--src/lib/checkout/components/CheckoutOld.jsx2
-rw-r--r--src/lib/checkout/components/CheckoutSection.jsx257
-rw-r--r--src/lib/checkout/email/FinishCheckoutEmail.jsx371
-rw-r--r--src/lib/flashSale/components/FlashSale.jsx62
-rw-r--r--src/lib/form/components/KunjunganSales.jsx13
-rw-r--r--src/lib/form/components/KunjunganService.jsx12
-rw-r--r--src/lib/form/components/Merchant.jsx11
-rw-r--r--src/lib/form/components/PembayaranTempo.jsx14
-rw-r--r--src/lib/form/components/RequestForQuotation.jsx12
-rw-r--r--src/lib/form/components/SuratDukungan.jsx11
-rw-r--r--src/lib/home/api/categoryHomeApi.js15
-rw-r--r--src/lib/home/components/PreferredBrand.jsx38
-rw-r--r--src/lib/home/components/PromotionProgram.jsx66
-rw-r--r--src/lib/product/components/Product/ProductDesktop.jsx764
-rw-r--r--src/lib/product/components/Product/ProductDesktopVariant.jsx324
-rw-r--r--src/lib/product/components/Product/ProductMobile.jsx409
-rw-r--r--src/lib/product/components/Product/ProductMobileVariant.jsx240
-rw-r--r--src/lib/product/components/ProductCard.jsx197
-rw-r--r--src/lib/product/components/ProductFilterDesktop.jsx10
-rw-r--r--src/lib/product/components/ProductFilterDesktopPromotion.jsx132
-rw-r--r--src/lib/product/components/ProductSearch.jsx405
-rw-r--r--src/lib/promo/components/Promocrumb.jsx40
-rw-r--r--src/lib/quotation/components/Quotation.jsx514
-rw-r--r--src/lib/transaction/api/approveApi.js13
-rw-r--r--src/lib/transaction/api/listSiteApi.js10
-rw-r--r--src/lib/transaction/api/rejectApi.js13
-rw-r--r--src/lib/transaction/components/Transaction.jsx552
-rw-r--r--src/lib/transaction/components/Transactions.jsx328
-rw-r--r--src/lib/transaction/components/stepper.jsx83
-rw-r--r--src/lib/variant/components/VariantCard.jsx35
-rw-r--r--src/pages/_app.jsx118
-rw-r--r--src/pages/_document.jsx46
-rw-r--r--src/pages/api/product-variant/[id].js2
-rw-r--r--src/pages/api/product-variant/[id]/promotion/[category].js2
-rw-r--r--src/pages/api/product-variant/[id]/promotion/highlight.js2
-rw-r--r--src/pages/api/promotion-program/[id].js2
-rw-r--r--src/pages/api/shop/brands.js49
-rw-r--r--src/pages/api/shop/generate-recomendation.js64
-rw-r--r--src/pages/api/shop/search.js118
-rw-r--r--src/pages/api/shop/variant-detail.js5
-rw-r--r--src/pages/google_merchant/products/[page].js130
-rw-r--r--src/pages/index.jsx55
-rw-r--r--src/pages/my/recomendation/api/recomendation.js17
-rw-r--r--src/pages/my/recomendation/components/products-recomendatison.jsx477
-rw-r--r--src/pages/my/recomendation/index.jsx26
-rw-r--r--src/pages/shop/cart.jsx41
-rw-r--r--src/pages/shop/category/[slug].jsx44
-rw-r--r--src/pages/shop/product/[slug].jsx114
-rw-r--r--src/pages/shop/product/variant/[slug].jsx76
-rw-r--r--src/pages/shop/promo/[slug].tsx523
-rw-r--r--src/pages/shop/promo/index.tsx186
-rw-r--r--src/pages/video.jsx2
-rw-r--r--src/styles/globals.css20
-rw-r--r--src/styles/normalize.css351
-rw-r--r--src/utils/solrMapping.js101
80 files changed, 6679 insertions, 3002 deletions
diff --git a/src/api/bannerApi.js b/src/api/bannerApi.js
index 8bae131d..431225a5 100644
--- a/src/api/bannerApi.js
+++ b/src/api/bannerApi.js
@@ -3,3 +3,6 @@ import odooApi from '@/core/api/odooApi'
export const bannerApi = ({ type }) => {
return async () => await odooApi('GET', `/api/v1/banner?type=${type}`)
}
+
+// ubah ke SOLR
+
diff --git a/src/api/promoApi.js b/src/api/promoApi.js
new file mode 100644
index 00000000..0e82c8b9
--- /dev/null
+++ b/src/api/promoApi.js
@@ -0,0 +1,73 @@
+// src/api/promoApi.js
+import odooApi from '@/core/api/odooApi';
+import { type } from 'os';
+// import { SolrResponse } from "../../../../src-migrate/types/solr.ts";
+
+export const fetchPromoItems = async (type) => {
+ try {
+ const response = await odooApi('GET', `/api/v1/program-line?type=${type}&limit=3`);
+ return response.map((item) => ({ value: item.id, label: item.name, product: item.products ,price:item.price}));
+ } catch (error) {
+ console.error('Error fetching promo items:', error);
+ return [];
+ }
+};
+
+export const fetchPromoItemsSolr = async (type) => {
+ // let query = type ? `type_value_s:${type}` : '*:*';
+ let sort ='sort=if(exists(sequence_i),0,1) asc, sequence_i asc, if(exists(total_qty_sold_f), total_qty_sold_f, -1) desc';
+ let start = 0
+ let rows = 100
+ try {
+ const queryParams = new URLSearchParams({ q: type });
+ const response = await fetch(`/solr/promotion_program_lines/select?${queryParams.toString()}&rows=${rows}&start=${start}&${sort}`);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const data = await response.json();
+ const promotions = await map(data.response.docs);
+ return promotions;
+ } catch (error) {
+ console.error("Error fetching promotion data:", error);
+ return [];
+ }
+};
+
+export const fetchVariantSolr = async(data)=>{
+ try {
+ const queryParams = new URLSearchParams({ q: data });
+ const response = await fetch(`/solr/variants/select?${queryParams.toString()}`);
+ const responseData = await response.json();
+ return responseData;
+ } catch (error) {
+ console.error("Error fetching promotion data:", error);
+ return [];
+ }
+};
+
+const map = async (promotions) => {
+ const result = [];
+ for (const promotion of promotions) {
+ const data = {
+ id: promotion.id,
+ program_id: promotion.program_id_i,
+ name: promotion.name_s,
+ type: {
+ value: promotion.type_value_s,
+ label: promotion.type_label_s,
+ },
+ limit: promotion.package_limit_i,
+ limit_user: promotion.package_limit_user_i,
+ limit_trx: promotion.package_limit_trx_i,
+ price: promotion.price_f,
+ sequence: promotion.sequence_i,
+ total_qty: promotion.total_qty_i,
+ products: JSON.parse(promotion.products_s),
+ product_id: promotion.product_ids[0],
+ qty_sold_f:promotion.total_qty_sold_f,
+ free_products: JSON.parse(promotion.free_products_s),
+ };
+ result.push(data);
+ }
+ return result;
+}; \ No newline at end of file
diff --git a/src/components/ui/HeroBanner.jsx b/src/components/ui/HeroBanner.jsx
index 6cf7902c..9a62465d 100644
--- a/src/components/ui/HeroBanner.jsx
+++ b/src/components/ui/HeroBanner.jsx
@@ -1,42 +1,43 @@
// Swiper
-import { Swiper, SwiperSlide } from 'swiper/react'
-import { Pagination, Autoplay } from 'swiper'
-import 'swiper/css'
-import 'swiper/css/pagination'
-import 'swiper/css/autoplay'
+import { Autoplay, Pagination } from 'swiper';
+import 'swiper/css';
+import 'swiper/css/autoplay';
+import 'swiper/css/pagination';
+import { Swiper, SwiperSlide } from 'swiper/react';
-import DesktopView from '@/core/components/views/DesktopView'
-import MobileView from '@/core/components/views/MobileView'
-import { useMemo } from 'react'
-import Link from '@/core/components/elements/Link/Link'
-import Image from 'next/image'
-import { useQuery } from 'react-query'
-import { bannerApi } from '@/api/bannerApi'
-import { HeroBannerSkeleton } from '../skeleton/BannerSkeleton'
+import Image from 'next/image';
+import { useMemo } from 'react';
+import { useQuery } from 'react-query';
+
+import { bannerApi } from '@/api/bannerApi';
+import Link from '@/core/components/elements/Link/Link';
+import DesktopView from '@/core/components/views/DesktopView';
+import MobileView from '@/core/components/views/MobileView';
+import SmoothRender from '~/components/ui/smooth-render';
const swiperBanner = {
autoplay: {
delay: 6000,
- disableOnInteraction: false
+ disableOnInteraction: false,
},
modules: [Pagination, Autoplay],
loop: true,
className: 'border border-gray_r-6 min-h-full',
- slidesPerView: 1
-}
+ slidesPerView: 1,
+};
const HeroBanner = () => {
- const heroBanner = useQuery('heroBanner', bannerApi({ type: 'index-a-1' }))
+ const heroBanner = useQuery('heroBanner', bannerApi({ type: 'index-a-1' }));
const swiperBannerMobile = {
...swiperBanner,
- pagination: { dynamicBullets: true, clickable: true }
- }
+ pagination: { dynamicBullets: true, clickable: true },
+ };
const swiperBannerDesktop = {
...swiperBanner,
- pagination: { dynamicBullets: false, clickable: true }
- }
+ pagination: { dynamicBullets: false, clickable: true },
+ };
const BannerComponent = useMemo(() => {
return heroBanner.data?.map((banner, index) => (
@@ -51,24 +52,29 @@ const HeroBanner = () => {
/>
</Link>
</SwiperSlide>
- ))
- }, [heroBanner.data])
-
- if (heroBanner.isLoading) return <HeroBannerSkeleton />
+ ));
+ }, [heroBanner.data]);
return (
- heroBanner.data && (
- <>
- <MobileView>
+ <>
+ <MobileView>
+ <SmoothRender
+ isLoaded={heroBanner.data?.length > 0}
+ height='68vw'
+ duration='750ms'
+ delay='100ms'
+ >
<Swiper {...swiperBannerMobile}>{BannerComponent}</Swiper>
- </MobileView>
+ </SmoothRender>
+ </MobileView>
- <DesktopView>
+ <DesktopView>
+ {heroBanner.data?.length > 0 && (
<Swiper {...swiperBannerDesktop}>{BannerComponent}</Swiper>
- </DesktopView>
- </>
- )
- )
-}
+ )}
+ </DesktopView>
+ </>
+ );
+};
-export default HeroBanner
+export default HeroBanner;
diff --git a/src/core/components/ScrollToTop.jsx b/src/core/components/ScrollToTop.jsx
new file mode 100644
index 00000000..f8e85167
--- /dev/null
+++ b/src/core/components/ScrollToTop.jsx
@@ -0,0 +1,24 @@
+import { useEffect } from 'react';
+import { useRouter } from 'next/router';
+
+const ScrollToTop = () => {
+ const router = useRouter();
+
+ useEffect(() => {
+ const handleRouteChange = (url, { shallow }) => {
+ if (!shallow) {
+ window.scrollTo(0, 0);
+ }
+ };
+
+ router.events.on('routeChangeComplete', handleRouteChange);
+
+ return () => {
+ router.events.off('routeChangeComplete', handleRouteChange);
+ };
+ }, [router.events]);
+
+ return null;
+};
+
+export default ScrollToTop;
diff --git a/src/core/components/elements/Footer/BasicFooter.jsx b/src/core/components/elements/Footer/BasicFooter.jsx
index 28a3764c..6129143d 100644
--- a/src/core/components/elements/Footer/BasicFooter.jsx
+++ b/src/core/components/elements/Footer/BasicFooter.jsx
@@ -41,6 +41,8 @@ const BasicFooter = () => {
<Form />
<CustomerGuide />
<Payments />
+ <Shippings />
+ <Secures />
</div>
<div className='w-full mt-8 leading-5 text-caption-2 text-gray_r-12/80'>
@@ -53,7 +55,7 @@ const BasicFooter = () => {
<DesktopView>
<footer className='bg-gray_r-3 py-6'>
<div className='container mx-auto flex flex-wrap justify-between'>
- <div className='w-3/12'>
+ <div className='w-4/12'>
<NextImage
src={IndoteknikLogo}
alt='Logo Indoteknik'
@@ -64,27 +66,40 @@ const BasicFooter = () => {
PT. Indoteknik Dotcom Gemilang
</div>
<InformationCenter />
+ <div className='h-4' />
+ <OfficeLocation />
+ <div className='h-4' />
+ <OpenHours />
</div>
- <CustomerGuide />
- <Form />
- <AboutUs />
+
+ <div className='w-2/12'>
+ <CustomerGuide />
+ <div className='h-6' />
+ <Form />
+ </div>
+
+ <div className='w-2/12'>
+ <AboutUs />
+ </div>
+
<div className='w-3/12'>
- <div className='grid grid-cols-1 gap-y-4'>
- <OfficeLocation />
- {/* <WarehouseLocation /> */}
- <OpenHours />
+ <div className='grid grid-cols-1 gap-y-6'>
<Payments />
+ <Shippings />
+ <Secures />
</div>
</div>
+
<hr className='w-full my-4 border-gray_r-7' />
+
<div className='w-full flex justify-between items-center'>
<div className='text-caption-1'>
Copyright © 2007 - {new Date().getFullYear()}, PT. Indoteknik
Dotcom Gemilang
</div>
- <div>
+ {/* <div>
<SocialMedias />
- </div>
+ </div> */}
</div>
</div>
</footer>
@@ -256,7 +271,7 @@ const InformationCenter = () => (
<li className='text-gray_r-12/80 flex items-center'>
<DevicePhoneMobileIcon className='w-[18px] mr-2' />
<a href={whatsappUrl()} target='_blank' rel='noreferrer'>
- 0812-8080-622
+ 0817-1718-1922
</a>
</li>
</ul>
@@ -286,7 +301,7 @@ const SocialMedias = () => (
<a
target='_blank'
rel='noreferrer'
- href='https://www.youtube.com/@indoteknikb2bindustriale-c778'
+ href='https://www.youtube.com/@indoteknikcom'
>
<NextImage
src='/images/socials/youtube.webp'
@@ -369,57 +384,43 @@ const SocialMedias = () => (
const Payments = () => (
<div>
- <div className={headerClassName}>Pembayaran</div>
- <div className='flex flex-wrap gap-2'>
- <NextImage
- src='/images/payments/bca.png'
- alt='Bank BCA Logo'
- width={48}
- height={24}
- />
- <NextImage
- src='/images/payments/bni.png'
- alt='Bank BNI Logo'
- width={48}
- height={24}
- />
- <NextImage
- src='/images/payments/bri.png'
- alt='Bank BRI Logo'
- width={48}
- height={24}
- />
- <NextImage
- src='/images/payments/gopay.png'
- alt='Gopay Logo'
- width={48}
- height={24}
- />
- <NextImage
- src='/images/payments/mandiri.png'
- alt='Bank Mandiri Logo'
- width={48}
- height={24}
- />
- <NextImage
- src='/images/payments/mastercard.png'
- alt='Mastercard Logo'
- width={48}
- height={24}
- />
- <NextImage
- src='/images/payments/permata.png'
- alt='Bank Permata Logo'
- width={48}
- height={24}
- />
- <NextImage
- src='/images/payments/visa.png'
- alt='Visa Logo'
- width={48}
- height={24}
- />
- </div>
+ <div className={headerClassName}>Metode Pembayaran</div>
+ <NextImage
+ src='/images/footer/payment-method-new.png'
+ alt='Metode Pembayaran - Indoteknik'
+ width={512}
+ height={512}
+ quality={100}
+ className='w-full'
+ />
+ </div>
+);
+
+const Shippings = () => (
+ <div>
+ <div className={headerClassName}>Jasa Pengiriman</div>
+ <NextImage
+ src='/images/footer/shippings.png'
+ alt='Jasa Pengiriman - Indoteknik'
+ width={512}
+ height={512}
+ quality={100}
+ className='w-full'
+ />
+ </div>
+);
+
+const Secures = () => (
+ <div>
+ <div className={headerClassName}>Keamanan Belanja</div>
+ <NextImage
+ src='/images/footer/secures.png'
+ alt='Keamanan Belanja - Indoteknik'
+ width={512}
+ height={512}
+ quality={100}
+ className='w-full'
+ />
</div>
);
diff --git a/src/core/components/elements/Footer/SimpleFooter.jsx b/src/core/components/elements/Footer/SimpleFooter.jsx
index 26f7f786..371b1652 100644
--- a/src/core/components/elements/Footer/SimpleFooter.jsx
+++ b/src/core/components/elements/Footer/SimpleFooter.jsx
@@ -22,7 +22,7 @@ const SimpleFooter = () => (
<li className='text-gray_r-12/80 flex items-center'>
<DevicePhoneMobileIcon className='w-[18px] mr-2' />
<a href={whatsappUrl()} target='_blank' rel='noreferrer'>
- 0812-8080-622
+ 081717181922
</a>
</li>
</ul>
diff --git a/src/core/components/elements/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx
index d9f5658e..308f2623 100644
--- a/src/core/components/elements/Navbar/NavbarDesktop.jsx
+++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx
@@ -1,85 +1,86 @@
+import { useProductContext } from '@/contexts/ProductContext';
+import useAuth from '@/core/hooks/useAuth';
+import { getCountCart } from '@/core/utils/cart';
+import { createSlug } from '@/core/utils/slug';
+import whatsappUrl from '@/core/utils/whatsappUrl';
+import IndoteknikLogo from '@/images/logo.png';
+import Cardheader from '@/lib/cart/components/Cartheader';
+import Category from '@/lib/category/components/Category';
import {
ChevronDownIcon,
+ DocumentCheckIcon,
HeartIcon,
- ShoppingCartIcon,
- DocumentCheckIcon
-} from '@heroicons/react/24/outline'
-import Link from '../Link/Link'
-import Image from 'next/image'
-import DesktopView from '../../views/DesktopView'
-import dynamic from 'next/dynamic'
-import IndoteknikLogo from '@/images/logo.png'
-import Category from '@/lib/category/components/Category'
-import { useCallback, useContext, useEffect, useState } from 'react'
-import useAuth from '@/core/hooks/useAuth'
-import NavbarUserDropdown from './NavbarUserDropdown'
-import { getCartApi, getCountCart } from '@/core/utils/cart'
-import whatsappUrl from '@/core/utils/whatsappUrl'
-import { useRouter } from 'next/router'
-import { getAuth, setAuth } from '@/core/utils/auth'
-import { createSlug, getIdFromSlug } from '@/core/utils/slug'
-import { TopBannerSkeleton } from '../Skeleton/TopBannerSkeleton'
-import { useProductContext } from '@/contexts/ProductContext'
-import Cardheader from '@/lib/cart/components/Cartheader'
-
-const Search = dynamic(() => import('./Search'))
-const TopBanner = dynamic(() => import('./TopBanner'), {
- loading: () => <TopBannerSkeleton />
-})
+} from '@heroicons/react/24/outline';
+import dynamic from 'next/dynamic';
+import Image from 'next/image';
+import { useRouter } from 'next/router';
+import { useEffect, useState } from 'react';
+import DesktopView from '../../views/DesktopView';
+import Link from '../Link/Link';
+import NavbarUserDropdown from './NavbarUserDropdown';
+import NextImage from 'next/image';
+
+const Search = dynamic(() => import('./Search'), { ssr: false });
+const TopBanner = dynamic(() => import('./TopBanner'), { ssr: false });
const NavbarDesktop = () => {
- const [isOpenCategory, setIsOpenCategory] = useState(false)
- const auth = useAuth()
+ const [isOpenCategory, setIsOpenCategory] = useState(false);
+ const auth = useAuth();
- const [cartCount, setCartCount] = useState(0)
+ const [cartCount, setCartCount] = useState(0);
- const [templateWA, setTemplateWA] = useState(null)
- const [payloadWA, setPayloadWa] = useState(null)
- const [urlPath, setUrlPath] = useState(null)
+ const [templateWA, setTemplateWA] = useState(null);
+ const [payloadWA, setPayloadWa] = useState(null);
+ const [urlPath, setUrlPath] = useState(null);
- const router = useRouter()
+ const router = useRouter();
- const { product } = useProductContext()
+ const { product } = useProductContext();
useEffect(() => {
if (router.pathname === '/shop/product/[slug]') {
setPayloadWa({
name: product?.name,
manufacture: product?.manufacture.name,
- url: createSlug('/shop/product/', product?.name, product?.id, true)
- })
- setTemplateWA('product')
+ url: createSlug('/shop/product/', product?.name, product?.id, true),
+ });
+ setTemplateWA('product');
- setUrlPath(router.asPath)
+ setUrlPath(router.asPath);
}
- }, [product, router])
+ }, [product, router]);
useEffect(() => {
const handleCartChange = () => {
const cart = async () => {
- const listCart = await getCountCart()
- setCartCount(listCart)
- }
- cart()
- }
- handleCartChange()
+ const listCart = await getCountCart();
+ setCartCount(listCart);
+ };
+ cart();
+ };
+ handleCartChange();
- window.addEventListener('localStorageChange', handleCartChange)
+ window.addEventListener('localStorageChange', handleCartChange);
return () => {
- window.removeEventListener('localStorageChange', handleCartChange)
- }
- }, [])
+ window.removeEventListener('localStorageChange', handleCartChange);
+ };
+ }, []);
return (
<DesktopView>
<TopBanner />
- <div className='py-3 bg-warning-400' id='desktop-nav-top'>
+ <div className='py-2 bg-warning-400' id='desktop-nav-top'>
<div className='container mx-auto flex justify-between'>
- <Link href='/tentang-kami' className='!text-gray_r-12'>
- Tentang Indoteknik.com
- </Link>
+ <div className='flex items-start gap-5'>
+ <div>
+ <SocialMedias />
+ </div>
+ </div>
<div className='flex gap-x-6'>
+ <Link href='/tentang-kami' className='!text-gray_r-12'>
+ Tentang Indoteknik.com
+ </Link>
<Link href='/my/pembayaran-tempo' className='!text-gray_r-12'>
Pembayaran Tempo
</Link>
@@ -93,7 +94,12 @@ const NavbarDesktop = () => {
<nav className='pt-6 sticky top-0 z-50 bg-white border-b-2 border-danger-500'>
<div className='container mx-auto flex gap-x-6'>
<Link href='/'>
- <Image src={IndoteknikLogo} alt='Indoteknik Logo' width={210} height={210 / 3} />
+ <Image
+ src={IndoteknikLogo}
+ alt='Indoteknik Logo'
+ width={210}
+ height={210 / 3}
+ />
</Link>
<div className='flex-1 flex items-center'>
<Search />
@@ -128,10 +134,15 @@ const NavbarDesktop = () => {
rel='noreferrer'
className='flex items-center gap-x-1 !text-gray_r-12/80'
>
- <Image src='/images/socials/Whatsapp-2.png' alt='Whatsapp' width={48} height={48} />
+ <Image
+ src='/images/socials/Whatsapp-2.png'
+ alt='Whatsapp'
+ width={48}
+ height={48}
+ />
<div>
<div className='font-semibold'>Whatsapp</div>
- 0812 8080 622 (Chat)
+ 0817 1718 1922 (Chat)
</div>
</a>
</div>
@@ -146,16 +157,24 @@ const NavbarDesktop = () => {
className='w-3/12 p-4 font-semibold border border-gray_r-6 rounded-t-xl flex items-center relative'
>
<div>Kategori Produk</div>
- <ChevronDownIcon className={`ml-auto w-6 ${isOpenCategory ? 'rotate-180' : ''}`} />
+ <ChevronDownIcon
+ className={`ml-auto w-6 ${isOpenCategory ? 'rotate-180' : ''}`}
+ />
- <div className={`category-mega-box-wrapper ${isOpenCategory ? 'show' : ''}`}>
+ <div
+ className={`category-mega-box-wrapper ${
+ isOpenCategory ? 'show' : ''
+ }`}
+ >
<Category />
</div>
</button>
<div className='w-6/12 flex px-1 divide-x divide-gray_r-6'>
<Link
href='/shop/brands'
- className='p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition'
+ className={`${
+ router.asPath === '/shop/brands' && 'bg-gray_r-3'
+ } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition`}
target='_blank'
rel='noreferrer'
>
@@ -163,7 +182,10 @@ const NavbarDesktop = () => {
</Link>
<Link
href='/shop/search?orderBy=stock'
- className='p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition'
+ className={`${
+ router.asPath === '/shop/search?orderBy=stock' &&
+ 'bg-gray_r-3'
+ } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition`}
target='_blank'
rel='noreferrer'
>
@@ -179,7 +201,9 @@ const NavbarDesktop = () => {
</Link>
<Link
href='/video'
- className='p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition'
+ className={`${
+ router.asPath === '/video' && 'bg-gray_r-3'
+ } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition`}
target='_blank'
rel='noreferrer'
>
@@ -220,7 +244,95 @@ const NavbarDesktop = () => {
</div>
</nav>
</DesktopView>
- )
-}
+ );
+};
+
+const SocialMedias = () => (
+ <div>
+ {/* <div className={headerClassName + 'block md:hidden'}>Temukan Kami</div> */}
+ <div className='flex flex-wrap gap-3 items-start'>
+ <a
+ target='_blank'
+ rel='noreferrer'
+ href='https://www.youtube.com/@indoteknikcom'
+ >
+ <NextImage
+ src='/images/socials/youtube.webp'
+ alt='Youtube - Indoteknik.com'
+ width={24}
+ height={24}
+ />
+ </a>
+ <a
+ target='_blank'
+ rel='noreferrer'
+ href='https://www.tiktok.com/@indoteknikcom'
+ >
+ <NextImage
+ src='/images/socials/tiktok.png'
+ alt='TikTok - Indoteknik.com'
+ width={24}
+ height={24}
+ />
+ </a>
+ {/* <a target='_blank' rel='noreferrer' href={whatsappUrl(null)}>
+ <NextImage
+ src='/images/socials/Whatsapp.png'
+ alt='Whatsapp - Indoteknik.com'
+ width={24}
+ height={24}
+ />
+ </a> */}
+ <a
+ target='_blank'
+ rel='noreferrer'
+ href='https://www.facebook.com/indoteknikcom'
+ >
+ <NextImage
+ src='/images/socials/Facebook.png'
+ alt='Facebook - Indoteknik.com'
+ width={24}
+ height={24}
+ />
+ </a>
+ <a
+ target='_blank'
+ rel='noreferrer'
+ href='https://www.instagram.com/indoteknikcom/'
+ >
+ <NextImage
+ src='/images/socials/Instagram.png'
+ alt='Instagram - Indoteknik.com'
+ width={24}
+ height={24}
+ />
+ </a>
+ <a
+ target='_blank'
+ rel='noreferrer'
+ href='https://www.linkedin.com/company/pt-indoteknik-dotcom-gemilang/'
+ >
+ <NextImage
+ src='/images/socials/Linkedin.png'
+ alt='Linkedin - Indoteknik.com'
+ width={24}
+ height={24}
+ />
+ </a>
+ <a
+ target='_blank'
+ rel='noreferrer'
+ href='https://goo.gl/maps/GF8EmDjpQTHZPsJ1A'
+ >
+ <NextImage
+ src='/images/socials/g_maps.png'
+ alt='Maps - Indoteknik.com'
+ width={24}
+ height={24}
+ />
+ </a>
+ </div>
+ </div>
+);
-export default NavbarDesktop
+export default NavbarDesktop;
diff --git a/src/core/components/elements/Navbar/NavbarMobile.jsx b/src/core/components/elements/Navbar/NavbarMobile.jsx
index 704e91b6..bcf45e0a 100644
--- a/src/core/components/elements/Navbar/NavbarMobile.jsx
+++ b/src/core/components/elements/Navbar/NavbarMobile.jsx
@@ -1,51 +1,60 @@
-import Image from 'next/image'
-import MobileView from '../../views/MobileView'
-import Link from '../Link/Link'
-import { Bars3Icon, HeartIcon, ShoppingCartIcon } from '@heroicons/react/24/outline'
-import useSidebar from '@/core/hooks/useSidebar'
-import dynamic from 'next/dynamic'
-import IndoteknikLogo from '@/images/logo.png'
-import { useEffect, useState } from 'react'
-import { getCart, getCountCart } from '@/core/utils/cart'
-import TopBanner from './TopBanner'
+import useSidebar from '@/core/hooks/useSidebar';
+import { getCountCart } from '@/core/utils/cart';
+import IndoteknikLogo from '@/images/logo.png';
+import {
+ Bars3Icon,
+ HeartIcon,
+ ShoppingCartIcon,
+} from '@heroicons/react/24/outline';
+import dynamic from 'next/dynamic';
+import Image from 'next/image';
+import { useEffect, useState } from 'react';
+import MobileView from '../../views/MobileView';
+import Link from '../Link/Link';
+// import TopBanner from './TopBanner';
-const Search = dynamic(() => import('./Search'))
+const Search = dynamic(() => import('./Search'));
const NavbarMobile = () => {
- const { Sidebar, open } = useSidebar()
+ const { Sidebar, open } = useSidebar();
- const [cartCount, setCartCount] = useState(0)
+ const [cartCount, setCartCount] = useState(0);
useEffect(() => {
const handleCartChange = () => {
const cart = async () => {
- const listCart = await getCountCart()
- setCartCount(listCart)
- }
- cart()
- }
- handleCartChange()
+ const listCart = await getCountCart();
+ setCartCount(listCart);
+ };
+ cart();
+ };
+ handleCartChange();
- window.addEventListener('localStorageChange', handleCartChange)
+ window.addEventListener('localStorageChange', handleCartChange);
return () => {
- window.removeEventListener('localStorageChange', handleCartChange)
- }
- }, [])
+ window.removeEventListener('localStorageChange', handleCartChange);
+ };
+ }, []);
return (
<MobileView>
- <TopBanner />
+ {/* <TopBanner /> */}
<nav className='px-4 py-2 pb-3 sticky top-0 z-50 bg-white shadow'>
<div className='flex justify-between items-center mb-2'>
<Link href='/'>
- <Image src={IndoteknikLogo} alt='Indoteknik Logo' width={120} height={40} />
+ <Image
+ src={IndoteknikLogo}
+ alt='Indoteknik Logo'
+ width={120}
+ height={40}
+ />
</Link>
<div className='flex gap-x-3'>
- <Link href='/my/wishlist'>
+ <Link href='/my/wishlist' aria-label='Wishlist'>
<HeartIcon className='w-6 text-gray_r-12' />
</Link>
- <Link href='/shop/cart' className='relative'>
+ <Link href='/shop/cart' className='relative' aria-label='Cart'>
<ShoppingCartIcon className='w-6 text-gray_r-12' />
{cartCount > 0 && (
<span className='absolute -top-2 -right-2 badge-solid-red rounded-full w-5 h-5 flex items-center justify-center'>
@@ -62,7 +71,7 @@ const NavbarMobile = () => {
</nav>
{Sidebar}
</MobileView>
- )
-}
+ );
+};
-export default NavbarMobile
+export default NavbarMobile;
diff --git a/src/core/components/elements/Navbar/NavbarUserDropdown.jsx b/src/core/components/elements/Navbar/NavbarUserDropdown.jsx
index 1851ce84..42bdc12a 100644
--- a/src/core/components/elements/Navbar/NavbarUserDropdown.jsx
+++ b/src/core/components/elements/Navbar/NavbarUserDropdown.jsx
@@ -2,9 +2,11 @@ import { deleteAuth } from '@/core/utils/auth'
import Link from '../Link/Link'
import { useRouter } from 'next/router'
import { signOut, useSession } from 'next-auth/react'
+import useAuth from '@/core/hooks/useAuth'
const NavbarUserDropdown = () => {
const router = useRouter()
+ const atuh = useAuth()
const logout = async () => {
deleteAuth().then(() => {
@@ -21,6 +23,9 @@ const NavbarUserDropdown = () => {
<Link href='/my/invoices'>Invoice & Faktur Pajak</Link>
<Link href='/my/wishlist'>Wishlist</Link>
<Link href='/my/address'>Daftar Alamat</Link>
+ {!atuh?.external &&
+ <Link href='/my/recomendation'>Dashboard Recomendation</Link>
+ }
<button type='button' onClick={logout}>
Keluar Akun
</button>
diff --git a/src/core/components/elements/Navbar/TopBanner.jsx b/src/core/components/elements/Navbar/TopBanner.jsx
index a757c260..722a7501 100644
--- a/src/core/components/elements/Navbar/TopBanner.jsx
+++ b/src/core/components/elements/Navbar/TopBanner.jsx
@@ -1,33 +1,40 @@
-import odooApi from '@/core/api/odooApi'
-import { useQuery } from 'react-query'
-import Image from 'next/image'
-import Link from '../Link/Link'
-import { TopBannerSkeleton } from '../Skeleton/TopBannerSkeleton'
+import Image from 'next/image';
+import { useQuery } from 'react-query';
+
+import odooApi from '@/core/api/odooApi';
+import SmoothRender from '~/components/ui/smooth-render';
+import Link from '../Link/Link';
const TopBanner = () => {
- const fetchTopBanner = async () => await odooApi('GET', '/api/v1/banner?type=top-banner')
- const topBanner = useQuery('topBanner', fetchTopBanner, { refetchOnWindowFocus: false })
+ const topBanner = useQuery({
+ queryKey: 'topBanner',
+ queryFn: async () => await odooApi('GET', '/api/v1/banner?type=top-banner'),
+ refetchOnWindowFocus: false,
+ });
- if (topBanner.isLoading) {
- return <TopBannerSkeleton />
- }
+ const backgroundColor = topBanner.data?.[0]?.backgroundColor || 'transparent';
+ const hasData = topBanner.data?.length > 0;
+ const data = topBanner.data?.[0] || null;
return (
- topBanner.isFetched &&
- topBanner.data?.length > 0 && (
- <div style={{ backgroundColor: topBanner.data[0]?.backgroundColor || 'transparent' }}>
- <Link href={topBanner.data[0]?.url}>
- <Image
- src={topBanner.data[0].image}
- alt={topBanner.data[0].name}
- width={1440}
- height={40}
- className='object-cover object-center h-full mx-auto'
- />
- </Link>
- </div>
- )
- )
-}
+ <SmoothRender
+ isLoaded={hasData}
+ height='36px'
+ duration='700ms'
+ delay='300ms'
+ style={{ backgroundColor }}
+ >
+ <Link href={data?.url}>
+ <Image
+ src={data?.image}
+ alt={data?.name}
+ width={1440}
+ height={40}
+ className='object-cover object-center h-full mx-auto'
+ />
+ </Link>
+ </SmoothRender>
+ );
+};
-export default TopBanner
+export default TopBanner;
diff --git a/src/core/components/elements/Skeleton/TopBannerSkeleton.jsx b/src/core/components/elements/Skeleton/TopBannerSkeleton.jsx
index f7d2e748..8d1a51d2 100644
--- a/src/core/components/elements/Skeleton/TopBannerSkeleton.jsx
+++ b/src/core/components/elements/Skeleton/TopBannerSkeleton.jsx
@@ -1,19 +1,17 @@
-import useDevice from '@/core/hooks/useDevice'
-import classNames from 'classnames'
-import Skeleton from 'react-loading-skeleton'
+import useDevice from '@/core/hooks/useDevice';
+import classNames from 'classnames';
+import { Skeleton } from '@chakra-ui/react';
const TopBannerSkeleton = () => {
- const { isDesktop, isMobile } = useDevice()
+ const { isDesktop, isMobile } = useDevice();
const deviceClassName = {
- 'h-10': isDesktop,
- 'h-2.5': isMobile
- }
- const combinedClassName = classNames(deviceClassName)
+ '!h-[36px]': isDesktop,
+ 'h-2.5': isMobile,
+ };
+ const combinedClassName = classNames(deviceClassName);
- return (
- <Skeleton className={combinedClassName} count={1} containerClassName='w-full h-full block' />
- )
-}
+ return <Skeleton className={combinedClassName} />;
+};
-export { TopBannerSkeleton }
+export { TopBannerSkeleton };
diff --git a/src/core/components/layouts/AppLayout.jsx b/src/core/components/layouts/AppLayout.jsx
index d74d61e3..ebbc1ad5 100644
--- a/src/core/components/layouts/AppLayout.jsx
+++ b/src/core/components/layouts/AppLayout.jsx
@@ -1,6 +1,12 @@
-import AppBar from '../elements/Appbar/Appbar'
-import BasicFooter from '../elements/Footer/BasicFooter'
-import AnimationLayout from './AnimationLayout'
+import dynamic from 'next/dynamic';
+import AnimationLayout from './AnimationLayout';
+
+const AppBar = dynamic(() => import('../elements/Appbar/Appbar'), {
+ ssr: false,
+});
+const BasicFooter = dynamic(() => import('../elements/Footer/BasicFooter'), {
+ ssr: false,
+});
const AppLayout = ({ children, title, withFooter = true }) => {
return (
@@ -11,7 +17,7 @@ const AppLayout = ({ children, title, withFooter = true }) => {
</AnimationLayout>
{withFooter && <BasicFooter />}
</>
- )
-}
+ );
+};
-export default AppLayout
+export default AppLayout;
diff --git a/src/core/components/layouts/BasicLayout.jsx b/src/core/components/layouts/BasicLayout.jsx
index 9441dbd7..a4f3a856 100644
--- a/src/core/components/layouts/BasicLayout.jsx
+++ b/src/core/components/layouts/BasicLayout.jsx
@@ -1,55 +1,63 @@
-import dynamic from 'next/dynamic'
-import BasicFooter from '../elements/Footer/BasicFooter'
-import Image from 'next/image'
-import whatsappUrl from '@/core/utils/whatsappUrl'
-import { useEffect, useState } from 'react'
-import axios from 'axios'
-import odooApi from '@/core/api/odooApi'
-import { useRouter } from 'next/router'
-import productApi from '@/lib/product/api/productApi'
-import { getAuth, setAuth } from '@/core/utils/auth'
-import { createSlug, getIdFromSlug } from '@/core/utils/slug'
-import { useSession } from 'next-auth/react'
-import { setCookie } from 'cookies-next'
-import { useProductContext } from '@/contexts/ProductContext'
+import dynamic from 'next/dynamic';
+import Image from 'next/image';
+import { useRouter } from 'next/router';
+import { useEffect, useState } from 'react';
-const Navbar = dynamic(() => import('../elements/Navbar/Navbar'))
-const AnimationLayout = dynamic(() => import('./AnimationLayout'))
+import { useProductContext } from '@/contexts/ProductContext';
+import odooApi from '@/core/api/odooApi';
+import whatsappUrl from '@/core/utils/whatsappUrl';
+import Navbar from '../elements/Navbar/Navbar';
+
+const AnimationLayout = dynamic(() => import('./AnimationLayout'), {
+ ssr: false,
+});
+const BasicFooter = dynamic(() => import('../elements/Footer/BasicFooter'), {
+ ssr: false,
+});
const BasicLayout = ({ children }) => {
- const [templateWA, setTemplateWA] = useState(null)
- const [payloadWA, setPayloadWa] = useState(null)
- const [urlPath, setUrlPath] = useState(null)
+ const [templateWA, setTemplateWA] = useState(null);
+ const [payloadWA, setPayloadWa] = useState(null);
+ const [urlPath, setUrlPath] = useState(null);
- const router = useRouter()
+ const router = useRouter();
- const { product } = useProductContext()
+ const { product } = useProductContext();
useEffect(() => {
- if (router.pathname === '/shop/product/[slug]' || router.pathname === '/shop/product/variant/[slug]') {
+ if (
+ router.pathname === '/shop/product/[slug]' ||
+ router.pathname === '/shop/product/variant/[slug]'
+ ) {
setPayloadWa({
name: product?.name,
manufacture: product?.manufacture.name,
- url: process.env.NEXT_PUBLIC_SELF_HOST + router.asPath
- })
- setTemplateWA('product')
+ url: process.env.NEXT_PUBLIC_SELF_HOST + router.asPath,
+ });
+ setTemplateWA('product');
- setUrlPath(router.asPath)
+ setUrlPath(router.asPath);
}
- }, [product, router])
+ }, [product, router]);
+
+ const recordActivity = async (pathname) => {
+ const ONLY_ON_PATH = false;
+ const recordedPath = [];
+ if (ONLY_ON_PATH && !recordedPath.includes(pathname)) return;
+
+ const ip = await odooApi('GET', '/api/ip-address');
+ const data = new URLSearchParams({
+ page_title: document.title,
+ url: window.location.href,
+ ip,
+ });
+ fetch(`/api/user-activity?${data.toString()}`);
+ };
useEffect(() => {
- const getIP = async () => {
- const ip = await odooApi('GET', '/api/ip-address')
- const data = {
- page_title: document.title,
- url: window.location.href,
- ip: ip
- }
- axios.get(`/api/user-activity?page_title=${data.page_title}&url=${data.url}&ip=${data.ip}`)
- }
- getIP()
- }, [])
+ recordActivity(router.pathname);
+ }, [router.pathname]);
+
return (
<>
<Navbar />
@@ -82,7 +90,7 @@ const BasicLayout = ({ children }) => {
</AnimationLayout>
<BasicFooter />
</>
- )
-}
+ );
+};
-export default BasicLayout
+export default BasicLayout;
diff --git a/src/core/utils/auth.js b/src/core/utils/auth.js
index a7244747..03b20ae2 100644
--- a/src/core/utils/auth.js
+++ b/src/core/utils/auth.js
@@ -29,7 +29,7 @@ const setAuth = (user) => {
* @returns {boolean} - Returns `true`.
*/
const deleteAuth = async() => {
- // await signOut()
+ await signOut()
deleteCookie('auth')
return true
}
diff --git a/src/core/utils/googleTag.js b/src/core/utils/googleTag.js
index 6d7476bd..96a6bd2e 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,
@@ -33,6 +33,20 @@ const sumTotal = (variants) => {
}
}
+const mapProducts = (product) => {
+ const res = {
+ item_id: product.id,
+ item_name: product.name,
+ discount: product.lowest_price.price_discount || 0,
+ // index: 0,
+ item_brand: product.manufacture.name,
+ item_category: product.categories,
+ item_variant: product.variants,
+ price: product.lowest_price.price,
+ quantity: product.stock_total
+ }
+ return res
+}
export const gtagAddToCart = (variant, quantity) => {
const param = {
currency: 'IDR',
@@ -77,3 +91,13 @@ export const gtagPurchase = (variants, shipping, transactionId) => {
}
gtag('event', 'purchase', param)
}
+
+export const gtagProductDetail = (product) => {
+ const items = mapProducts(product)
+ const param = {
+ currency: 'IDR',
+ value: product.id,
+ items
+ }
+ gtag('event', 'view_item', param)
+} \ No newline at end of file
diff --git a/src/core/utils/slug.js b/src/core/utils/slug.js
index e91bcf83..19c7b115 100644
--- a/src/core/utils/slug.js
+++ b/src/core/utils/slug.js
@@ -1,4 +1,4 @@
-import toTitleCase from './toTitleCase'
+import toTitleCase from './toTitleCase';
/**
* Creates a slug from input parameters by converting the name and appending it with an ID.
@@ -10,19 +10,20 @@ import toTitleCase from './toTitleCase'
* @returns {string} - The generated slug with the prefix, name, and ID.
*/
const createSlug = (prefix, name, id, withHost = false) => {
+ name ||= '';
let slug =
name
?.trim()
.replace(new RegExp(/[^A-Za-z0-9]/, 'g'), '-')
.toLowerCase() +
'-' +
- id
- let splitSlug = slug.split('-')
- let filterSlugFromEmptyChar = splitSlug.filter((x) => x != '')
- slug = prefix + filterSlugFromEmptyChar.join('-')
- if (withHost) slug = process.env.NEXT_PUBLIC_SELF_HOST + slug
- return slug
-}
+ id;
+ let splitSlug = slug.split('-');
+ let filterSlugFromEmptyChar = splitSlug.filter((x) => x != '');
+ slug = prefix + filterSlugFromEmptyChar.join('-');
+ if (withHost) slug = process.env.NEXT_PUBLIC_SELF_HOST + slug;
+ return slug;
+};
/**
* Extracts the ID from a slug.
@@ -32,9 +33,9 @@ const createSlug = (prefix, name, id, withHost = false) => {
* @returns {string} - The extracted ID from the slug.
*/
const getIdFromSlug = (slug) => {
- let id = slug.split('-')
- return id[id.length - 1]
-}
+ let id = slug.split('-');
+ return id[id.length - 1];
+};
/**
* Extracts the name from a slug.
@@ -45,9 +46,10 @@ const getIdFromSlug = (slug) => {
* @returns {string} - The extracted name from the slug in title case.
*/
const getNameFromSlug = (slug) => {
- let name = slug.split('-')
- name.pop()
- return toTitleCase(name.join(' '))
-}
+ let name = slug.split('-');
+ name.pop();
+ return toTitleCase(name.join(' '));
+};
+
+export { createSlug, getIdFromSlug, getNameFromSlug };
-export { createSlug, getIdFromSlug, getNameFromSlug }
diff --git a/src/core/utils/whatsappUrl.js b/src/core/utils/whatsappUrl.js
index 9a92f424..7a129aa6 100644
--- a/src/core/utils/whatsappUrl.js
+++ b/src/core/utils/whatsappUrl.js
@@ -7,7 +7,7 @@ const whatsappUrl = (template = 'default', payload, urlPath = null) => {
if(!urlPath) return '/login'
}
let parentName = user.parentName || '-'
- let url = 'https://wa.me/628128080622'
+ let url = 'https://wa.me/6281717181922'
let text = 'Hallo Indoteknik.com,'
switch (template) {
case 'product':
diff --git a/src/fonts/Inter/inter.css b/src/fonts/Inter/inter.css
index de6ce273..3a1de02a 100644
--- a/src/fonts/Inter/inter.css
+++ b/src/fonts/Inter/inter.css
@@ -1,57 +1,6 @@
@font-face {
font-family: 'Inter';
font-style: normal;
- font-weight: 100;
- font-display: swap;
- src: url('Inter-Thin.woff2?v=3.19') format('woff2'),
- url('Inter-Thin.woff?v=3.19') format('woff');
-}
-@font-face {
- font-family: 'Inter';
- font-style: italic;
- font-weight: 100;
- font-display: swap;
- src: url('Inter-ThinItalic.woff2?v=3.19') format('woff2'),
- url('Inter-ThinItalic.woff?v=3.19') format('woff');
-}
-
-@font-face {
- font-family: 'Inter';
- font-style: normal;
- font-weight: 200;
- font-display: swap;
- src: url('Inter-ExtraLight.woff2?v=3.19') format('woff2'),
- url('Inter-ExtraLight.woff?v=3.19') format('woff');
-}
-@font-face {
- font-family: 'Inter';
- font-style: italic;
- font-weight: 200;
- font-display: swap;
- src: url('Inter-ExtraLightItalic.woff2?v=3.19') format('woff2'),
- url('Inter-ExtraLightItalic.woff?v=3.19') format('woff');
-}
-
-@font-face {
- font-family: 'Inter';
- font-style: normal;
- font-weight: 300;
- font-display: swap;
- src: url('Inter-Light.woff2?v=3.19') format('woff2'),
- url('Inter-Light.woff?v=3.19') format('woff');
-}
-@font-face {
- font-family: 'Inter';
- font-style: italic;
- font-weight: 300;
- font-display: swap;
- src: url('Inter-LightItalic.woff2?v=3.19') format('woff2'),
- url('Inter-LightItalic.woff?v=3.19') format('woff');
-}
-
-@font-face {
- font-family: 'Inter';
- font-style: normal;
font-weight: 400;
font-display: swap;
src: url('Inter-Regular.woff2?v=3.19') format('woff2'),
@@ -117,40 +66,6 @@
url('Inter-BoldItalic.woff?v=3.19') format('woff');
}
-@font-face {
- font-family: 'Inter';
- font-style: normal;
- font-weight: 800;
- font-display: swap;
- src: url('Inter-ExtraBold.woff2?v=3.19') format('woff2'),
- url('Inter-ExtraBold.woff?v=3.19') format('woff');
-}
-@font-face {
- font-family: 'Inter';
- font-style: italic;
- font-weight: 800;
- font-display: swap;
- src: url('Inter-ExtraBoldItalic.woff2?v=3.19') format('woff2'),
- url('Inter-ExtraBoldItalic.woff?v=3.19') format('woff');
-}
-
-@font-face {
- font-family: 'Inter';
- font-style: normal;
- font-weight: 900;
- font-display: swap;
- src: url('Inter-Black.woff2?v=3.19') format('woff2'),
- url('Inter-Black.woff?v=3.19') format('woff');
-}
-@font-face {
- font-family: 'Inter';
- font-style: italic;
- font-weight: 900;
- font-display: swap;
- src: url('Inter-BlackItalic.woff2?v=3.19') format('woff2'),
- url('Inter-BlackItalic.woff?v=3.19') format('woff');
-}
-
/* -------------------------------------------------------
Variable font.
Usage:
diff --git a/src/images/logo-idul-fitri.png b/src/images/logo-idul-fitri.png
new file mode 100644
index 00000000..db04faa5
--- /dev/null
+++ b/src/images/logo-idul-fitri.png
Binary files differ
diff --git a/src/lib/auth/components/LoginDesktop.jsx b/src/lib/auth/components/LoginDesktop.jsx
index 1333db14..9a68dc53 100644
--- a/src/lib/auth/components/LoginDesktop.jsx
+++ b/src/lib/auth/components/LoginDesktop.jsx
@@ -8,6 +8,7 @@ import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
import LogoSpinner from '@/core/components/elements/Spinner/LogoSpinner';
+import Image from 'next/image';
const LoginDesktop = () => {
const {
@@ -108,7 +109,7 @@ const LoginDesktop = () => {
{!isLoading ? 'Masuk' : 'Loading...'}
</button>
</form>
- {/* <div className='flex items-center mt-3 mb-3'>
+ <div className='flex items-center mt-3 mb-3'>
<hr className='flex-1' />
<p className='text-gray-400'>ATAU</p>
<hr className='flex-1' />
@@ -127,7 +128,7 @@ const LoginDesktop = () => {
height={10}
/>
<p>Masuk dengan Google</p>
- </button> */}
+ </button>
<div className='text-gray_r-11 mt-10'>
Belum punya akun Indoteknik?{' '}
diff --git a/src/lib/auth/components/LoginMobile.jsx b/src/lib/auth/components/LoginMobile.jsx
index 40924fbe..d2bf704f 100644
--- a/src/lib/auth/components/LoginMobile.jsx
+++ b/src/lib/auth/components/LoginMobile.jsx
@@ -117,7 +117,7 @@ const LoginMobile = () => {
{!isLoading ? 'Masuk' : 'Loading...'}
</button>
</form>
- {/* <div className='flex items-center mt-3 mb-3'>
+ <div className='flex items-center mt-3 mb-3'>
<hr className='flex-1' />
<p className='text-gray-400'>ATAU</p>
<hr className='flex-1' />
@@ -136,7 +136,7 @@ const LoginMobile = () => {
height={10}
/>
<p>Masuk dengan Google</p>
- </button> */}
+ </button>
<div className='text-gray_r-11 mt-4'>
Belum punya akun Indoteknik?{' '}
diff --git a/src/lib/auth/hooks/useLogin.js b/src/lib/auth/hooks/useLogin.js
index dc9580ea..dd5a4b03 100644
--- a/src/lib/auth/hooks/useLogin.js
+++ b/src/lib/auth/hooks/useLogin.js
@@ -74,7 +74,7 @@ const useLogin = () => {
if (data.isAuth) {
session.odooUser = data.user;
setCookie('auth', JSON.stringify(session?.odooUser));
- router.push(decodeURIComponent(router?.query?.next) ?? '/');
+ router.push(router?.query?.next || '/');
return;
}
};
diff --git a/src/lib/brand/components/BrandCard.jsx b/src/lib/brand/components/BrandCard.jsx
index bb1a17f7..731214ff 100644
--- a/src/lib/brand/components/BrandCard.jsx
+++ b/src/lib/brand/components/BrandCard.jsx
@@ -1,4 +1,4 @@
-import Image from '@/core/components/elements/Image/Image'
+import Image from '~/components/ui/image'
import Link from '@/core/components/elements/Link/Link'
import useDevice from '@/core/hooks/useDevice'
import { createSlug } from '@/core/utils/slug'
@@ -16,6 +16,8 @@ const BrandCard = ({ brand }) => {
<Image
src={brand.logo}
alt={brand.name}
+ width={128}
+ height={128}
className='h-full w-full object-contain object-center'
/>
)}
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..4aafdece 100644
--- a/src/lib/checkout/components/Checkout.jsx
+++ b/src/lib/checkout/components/Checkout.jsx
@@ -1,191 +1,201 @@
-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 { Skeleton, Spinner } from '@chakra-ui/react';
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 axios from 'axios';
+import { AnimatePresence, motion } from 'framer-motion';
+import { useRouter } from 'next/router';
+import React, { useEffect, useMemo, useRef, useState } from 'react';
+import { toast } from 'react-hot-toast';
+import { useQuery } from 'react-query';
+import snakecaseKeys from 'snakecase-keys';
+
+import Alert from '@/core/components/elements/Alert/Alert';
+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 BottomPopup from '@/core/components/elements/Popup/BottomPopup';
+import DesktopView from '@/core/components/views/DesktopView';
+import MobileView from '@/core/components/views/MobileView';
+import useAuth from '@/core/hooks/useAuth';
+import { getItemAddress } from '@/core/utils/address';
+import { deleteItemCart } from '@/core/utils/cart';
+import currencyFormat from '@/core/utils/currencyFormat';
+import getFileBase64 from '@/core/utils/getFileBase64';
+import { gtagPurchase } from '@/core/utils/googleTag';
+import whatsappUrl from '@/core/utils/whatsappUrl';
+import addressesApi from '@/lib/address/api/addressesApi';
+import CartItem from '~/modules/cart/components/Item.tsx';
+import ExpedisiList from '../api/ExpedisiList';
+import { findVoucher, getVoucher } from '../api/getVoucher';
+
+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 [grandTotal, setGrandTotal] = useState(0);
+ const [hasFlashSale, setHasFlashSale] = 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,215 +209,257 @@ 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);
+ setHasFlashSale(cartCheckout?.products[0]?.hasFlashsale ? cartCheckout.products[0].hasFlashsale : false);
+ }, [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)
+ useEffect(() => {
+ const GT =
+ cartCheckout?.grandTotal +
+ Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000;
+ const finalGT = GT < 0 ? 0 : GT;
+ setGrandTotal(finalGT);
+ }, [biayaKirim, cartCheckout?.grandTotal, activeVoucher]);
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,
+ // partner_shipping_id: auth.partnerId,
+ // partner_invoice_id: auth.partnerId,
+ partner_shipping_id: selectedAddress?.shipping?.id || auth.partnerId,
+ partner_invoice_id: selectedAddress?.invoicing?.id || auth.partnerId,
user_id: auth.id,
order_line: JSON.stringify(productOrder),
delivery_amount: biayaKirim,
carrier_id: selectedCarrierId,
estimated_arrival_days: splitDuration(etd),
delivery_service_type: selectedExpedisiService,
+ flash_sale : hasFlashSale, // dibuat negasi untuk ngetest kebalikan nilai false
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 })
- 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
- }
+ for (const product of products) deleteItemCart({ productId: product.id });
+ if (grandTotal > 0) {
+ 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;
+ } else {
+ window.location.href = `${
+ process.env.NEXT_PUBLIC_SELF_HOST
+ }/shop/checkout/success?order_id=${isCheckouted.name.replace(
+ /\//g,
+ '-'
+ )}`;
+ }
+ };
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
+ const hasNoPrice = useMemo(() => {
+ if (!products) return false;
+ for (const item of products) {
+ if (item.price.priceDiscount == 0) return true;
+ }
+ return false;
+ }, [products]);
+
return (
<>
<BottomPopup
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 +472,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 +497,7 @@ const Checkout = () => {
</div>
</div>
</BottomPopup>
+
<BottomPopup
className='w-full md:!w-[40%] !min-h-[350px]'
active={bottomPopup}
@@ -448,8 +505,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 +538,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 +558,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 +593,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 +645,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 +657,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 +679,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 +692,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 +722,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 +753,7 @@ const Checkout = () => {
</div>
</div>
</BottomPopup>
+
<MobileView>
<div className='p-4'>
<Alert type='info' className='text-caption-2 flex gap-x-3'>
@@ -677,8 +761,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,16 +785,22 @@ 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 && (
- <>
+ <Skeleton
+ isLoaded={!!selectedAddress.invoicing && !!selectedAddress.shipping}
+ minHeight={320}
+ >
<SectionAddress
address={selectedAddress.shipping}
label='Alamat Pengiriman'
@@ -722,7 +812,7 @@ const Checkout = () => {
label='Alamat Penagihan'
url='/my/address?select=invoice'
/>
- </>
+ </Skeleton>
)}
<Divider />
<SectionValidation address={selectedAddress.invoicing} />
@@ -742,7 +832,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 +843,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 +896,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 +913,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>
)}
@@ -839,9 +937,7 @@ const Checkout = () => {
<div className='flex gap-x-2 justify-between mb-4'>
<div>Grand Total</div>
<div className='font-semibold text-gray_r-12'>
- {currencyFormat(
- cartCheckout?.grandTotal + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000
- )}
+ {currencyFormat(grandTotal)}
</div>
</div>
)}
@@ -852,8 +948,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 +982,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 +1008,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 +1026,13 @@ const Checkout = () => {
<button
className='flex-1 btn-yellow'
onClick={checkout}
- disabled={isLoading || !products || products?.length == 0 || priceCheck}
+ disabled={
+ isLoading ||
+ !products ||
+ products?.length == 0 ||
+ priceCheck ||
+ hasNoPrice
+ }
>
{isLoading ? 'Loading...' : 'Lanjut Pembayaran'}
</button>
@@ -957,8 +1066,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,9 +1076,16 @@ 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 && (
- <>
+ <Skeleton
+ isLoaded={
+ !!selectedAddress.invoicing && !!selectedAddress.shipping
+ }
+ minHeight={290}
+ >
<SectionAddress
address={selectedAddress.shipping}
label='Alamat Pengiriman'
@@ -980,7 +1097,7 @@ const Checkout = () => {
label='Alamat Penagihan'
url='/my/address?select=invoice'
/>
- </>
+ </Skeleton>
)}
<Divider />
<SectionValidation address={selectedAddress.invoicing} />
@@ -1000,170 +1117,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 +1131,7 @@ 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 +1187,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 +1206,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>
@@ -1265,10 +1229,7 @@ const Checkout = () => {
<div className='flex gap-x-2 justify-between mb-4'>
<div>Grand Total</div>
<div className='font-semibold text-gray_r-12'>
- {currencyFormat(
- cartCheckout?.grandTotal +
- Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000
- )}
+ {currencyFormat(grandTotal)}
</div>
</div>
)}
@@ -1279,8 +1240,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 +1273,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 +1284,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 +1300,29 @@ 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 ||
+ hasNoPrice
+ }
>
{isLoading ? 'Loading...' : 'Lanjut Pembayaran'}
</button>
@@ -1367,8 +1342,8 @@ const Checkout = () => {
</div>
</DesktopView>
</>
- )
-}
+ );
+};
const SectionAddress = ({ address, label, url }) => (
<div className='p-4'>
@@ -1382,7 +1357,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 +1369,7 @@ const SectionAddress = ({ address, label, url }) => (
</div>
)}
</div>
-)
+);
const SectionValidation = ({ address }) =>
address?.rajaongkirCityId == 0 && (
@@ -1409,7 +1386,7 @@ const SectionValidation = ({ address }) =>
</Link>
</div>
</BottomPopup>
- )
+ );
const SectionExpedisi = ({
address,
@@ -1418,7 +1395,7 @@ const SectionExpedisi = ({
checkWeigth,
checkoutValidation,
expedisiValidation,
- loadingRajaOngkir
+ loadingRajaOngkir,
}) =>
address?.rajaongkirCityId > 0 && (
<div className='p-4' ref={expedisiValidation}>
@@ -1427,7 +1404,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 +1432,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 +1442,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,18 +1455,19 @@ 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'
+ href='https://api.whatsapp.com/send?phone=6281717181922'
>
tautan ini
</a>
</p>
)}
</div>
- )
+ );
const SectionListService = ({ listserviceExpedisi, setSelectedServiceType }) =>
listserviceExpedisi?.length > 0 && (
@@ -1494,7 +1476,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 +1496,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 +1507,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 +1583,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/checkout/components/CheckoutOld.jsx b/src/lib/checkout/components/CheckoutOld.jsx
index d57fbd66..e2c45ce6 100644
--- a/src/lib/checkout/components/CheckoutOld.jsx
+++ b/src/lib/checkout/components/CheckoutOld.jsx
@@ -696,7 +696,7 @@ const SectionExpedisi = ({ address, listExpedisi, setSelectedExpedisi, checkWeig
diatur beratnya. Mohon atur berat barang dengan menghubungi admin melalui{' '}
<a
className='text-danger-500 inline'
- href='https://api.whatsapp.com/send?phone=628128080622'
+ href='https://api.whatsapp.com/send?phone=6281717181922'
>
tautan ini
</a>
diff --git a/src/lib/checkout/components/CheckoutSection.jsx b/src/lib/checkout/components/CheckoutSection.jsx
new file mode 100644
index 00000000..affe6138
--- /dev/null
+++ b/src/lib/checkout/components/CheckoutSection.jsx
@@ -0,0 +1,257 @@
+import Link from 'next/link';
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
+import { AnimatePresence, motion } from 'framer-motion';
+import { Divider, Spinner } from '@chakra-ui/react';
+
+export const SectionAddress = ({ address, label, url }) => {
+ return (
+ <div className='p-4'>
+ <div className='flex justify-between items-center'>
+ <div className='font-medium'>{label}</div>
+ <Link className='text-caption-1' href={url}>
+ Pilih Alamat Lain
+ </Link>
+ </div>
+
+ {address && (
+ <div className='mt-4 text-caption-1'>
+ <div className='badge-red mb-2'>
+ {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>
+ <p className='mt-1 text-gray_r-11'>
+ {address.street}, {address?.city?.name}
+ </p>
+ </div>
+ )}
+ </div>
+ );
+};
+
+export const SectionValidation = ({ address }) =>
+ address?.rajaongkirCityId == 0 && (
+ <BottomPopup active={true} title='Update Alamat'>
+ <div className='leading-7 text-gray_r-12/80'>
+ Mohon untuk memperbarui alamat Anda dengan mengklik tombol di bawah ini.{' '}
+ </div>
+ <div className='flex justify-center mt-6 gap-x-4'>
+ <Link
+ className='btn-solid-red w-full md:w-fit text-white'
+ href={`/my/address/${address?.id}/edit`}
+ >
+ Update Alamat
+ </Link>
+ </div>
+ </BottomPopup>
+ );
+
+export const SectionExpedisi = ({
+ address,
+ listExpedisi,
+ setSelectedExpedisi,
+ checkWeigth,
+ checkoutValidation,
+ expedisiValidation,
+ loadingRajaOngkir,
+}) =>
+ address?.rajaongkirCityId > 0 && (
+ <div className='p-4' ref={expedisiValidation}>
+ <div className='flex justify-between items-center'>
+ <div className='font-medium'>Pilih Ekspedisi: </div>
+ <div className='w-[250px]'>
+ <div className='flex items-center gap-x-4'>
+ <select
+ className={`form-input ${
+ checkoutValidation ? 'border-red-500 shake' : ''
+ }`}
+ onChange={(e) => setSelectedExpedisi(e.target.value)}
+ required
+ >
+ <option value='0,0'>Pilih Pengiriman</option>
+ <option value='1,32'>SELF PICKUP</option>
+ {checkWeigth != true &&
+ listExpedisi.map((expedisi) => (
+ <option
+ disabled={checkWeigth}
+ value={expedisi.label + ',' + expedisi.carrierId}
+ key={expedisi.value}
+ >
+ {' '}
+ {expedisi.label.toUpperCase()}{' '}
+ </option>
+ ))}
+ </select>
+
+ <AnimatePresence>
+ {loadingRajaOngkir && (
+ <motion.div
+ initial={{ opacity: 0, width: 0 }}
+ animate={{ opacity: 1, width: '28px' }}
+ exit={{ opacity: 0, width: 0 }}
+ transition={{
+ duration: 0.25,
+ }}
+ className='overflow-hidden'
+ >
+ <Spinner thickness='3px' speed='0.5s' color='red.500' />
+ </motion.div>
+ )}
+ </AnimatePresence>
+ </div>
+ {checkoutValidation && (
+ <span className='text-sm text-red-500'>
+ *silahkan pilih expedisi
+ </span>
+ )}
+ </div>
+ <style jsx>{`
+ .shake {
+ animation: shake 0.4s ease-in-out;
+ }
+ `}</style>
+ </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{' '}
+ <a
+ className='text-danger-500 inline'
+ href='https://api.whatsapp.com/send?phone=6281717181922'
+ >
+ tautan ini
+ </a>
+ </p>
+ )}
+ </div>
+ );
+
+export const SectionListService = ({
+ listserviceExpedisi,
+ setSelectedServiceType,
+}) =>
+ listserviceExpedisi?.length > 0 && (
+ <>
+ <div className='p-4'>
+ <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)}
+ >
+ {listserviceExpedisi.map((service) => (
+ <option
+ value={
+ service.cost[0].value +
+ ',' +
+ service.description +
+ '-' +
+ service.service +
+ ',' +
+ extractDuration(service.cost[0].etd)
+ }
+ key={service.service}
+ >
+ {' '}
+ {service.description} - {service.service.toUpperCase()}
+ {extractDuration(service.cost[0].etd) &&
+ ` (Estimasi Tiba ${extractDuration(
+ service.cost[0].etd
+ )} Hari)`}
+ </option>
+ ))}
+ </select>
+ </div>
+ </div>
+ </div>
+ <Divider />
+ </>
+ );
+
+export const PickupAddress = ({ label }) => (
+ <div className='p-4'>
+ <div className='flex justify-between items-center'>
+ <div className='font-medium'>{label}</div>
+ </div>
+ <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
+ </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>
+);
+
+const extractDuration = (text) => {
+ 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]);
+
+ if (min === max) {
+ return min.toString();
+ }
+
+ return matches[0];
+ }
+
+ return '';
+};
+
+export 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 etdText = `*Estimasi tiba ${formatDate(from)}`;
+
+ if (estimationDate[1] > estimationDate[0]) {
+ etdText += ` - ${formatDate(to)}`;
+ }
+
+ return etdText;
+ }
+
+ return '';
+}
+
+function addDays(date, days) {
+ 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}`;
+}
+
+export function splitDuration(duration) {
+ if (duration) {
+ let estimationDate = null;
+ if (duration.includes('-')) {
+ estimationDate = duration.split('-');
+ estimationDate = parseInt(estimationDate[1]);
+ } else {
+ estimationDate = parseInt(duration);
+ }
+
+ return estimationDate;
+ }
+
+ return '';
+} \ No newline at end of file
diff --git a/src/lib/checkout/email/FinishCheckoutEmail.jsx b/src/lib/checkout/email/FinishCheckoutEmail.jsx
index d40ce7d4..d19ba1ca 100644
--- a/src/lib/checkout/email/FinishCheckoutEmail.jsx
+++ b/src/lib/checkout/email/FinishCheckoutEmail.jsx
@@ -14,8 +14,10 @@ import {
Section,
Text
} from '@react-email/components'
+import FinishCheckout from '../components/FinishCheckout'
const FinishCheckoutEmail = ({ transaction, payment, statusPayment }) => {
+
return (
<Html>
<Head />
@@ -38,7 +40,10 @@ const FinishCheckoutEmail = ({ transaction, payment, statusPayment }) => {
</Heading>
<Text style={style.text}>Hai {transaction.address.customer.name},</Text>
- <Text style={style.text}>
+
+ {transaction.amountTotal > 0 ?
+ <div>
+ <Text style={style.text}>
{statusPayment == 'success' && (
<>
Terima kasih atas kepercayaan anda berbelanja di Indoteknik. Dengan ini kami
@@ -71,202 +76,204 @@ const FinishCheckoutEmail = ({ transaction, payment, statusPayment }) => {
& Solution
</>
)}
- </Text>
- <Text style={style.text}>
- {['pending', 'failed'].includes(statusPayment) && (
- <>
- Jika anda mengalami kesulitan, dapat menghubungi Customer Service kami untuk
- menanyakan transaksi anda lakukan melalui Whatsapp kami.
- </>
- )}
- {statusPayment == 'success' && (
- <>
- Anda dapat menghubungi Customer Service kami untuk menanyakan status pesanan yang
- sudah berhasil anda lakukan melalui Whatsapp kami.
- </>
- )}
- {statusPayment == 'manual' && (
+ </Text>
+ <Text style={style.text}>
+ {['pending', 'failed'].includes(statusPayment) && (
+ <>
+ Jika anda mengalami kesulitan, dapat menghubungi Customer Service kami untuk
+ menanyakan transaksi anda lakukan melalui Whatsapp kami.
+ </>
+ )}
+ {statusPayment == 'success' && (
+ <>
+ Anda dapat menghubungi Customer Service kami untuk menanyakan status pesanan yang
+ sudah berhasil anda lakukan melalui Whatsapp kami.
+ </>
+ )}
+ {statusPayment == 'manual' && (
+ <>
+ Kami mohon kepada {transaction.address.customer.name} untuk dapat segera
+ menyelesaikan transaksi dengan detail dibawah ini:
+ <ul>
+ <li>Nomor Pembelian: {transaction.name}</li>
+ <li>Nominal: {currencyFormat(transaction.amountTotal)}</li>
+ <li>Tanggal: {transaction.dateOrder}</li>
+ </ul>
+ </>
+ )}
+ </Text>
+
+ {['pending', 'failed', 'success'].includes(statusPayment) && (
<>
- Kami mohon kepada {transaction.address.customer.name} untuk dapat segera
- menyelesaikan transaksi dengan detail dibawah ini:
- <ul>
- <li>Nomor Pembelian: {transaction.name}</li>
- <li>Nominal: {currencyFormat(transaction.amountTotal)}</li>
- <li>Tanggal: {transaction.dateOrder}</li>
- </ul>
- </>
- )}
- </Text>
+ <Text style={{ ...style.text, lineHeight: '100%', marginTop: '24px' }}>
+ <strong>Detail Transaksi</strong>
+ </Text>
+
+ <Hr style={style.hr} />
- {['pending', 'failed', 'success'].includes(statusPayment) && (
- <>
- <Text style={{ ...style.text, lineHeight: '100%', marginTop: '24px' }}>
- <strong>Detail Transaksi</strong>
- </Text>
+ <Section style={style.alert}>
+ {statusPayment == 'success' &&
+ 'Struk ini dapat anda simpan sebagai bukti tambahan dalam transaksi yang telah dilakukan.'}
+ {statusPayment == 'pending' &&
+ 'Kami akan menginformasikan melalui email setelah anda berhasil melakukan pembayaran.'}
+ {statusPayment == 'failed' &&
+ 'Dimohon untuk tidak melakukan pembayaran. Karena transaksi anda tidak berhasil dibuat.'}
+ </Section>
- <Hr style={style.hr} />
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>No Transaksi (SO)</Column>
+ <Column style={style.descriptionRCol}>{transaction.name}</Column>
+ </Row>
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>Tanggal Transaksi</Column>
+ <Column style={style.descriptionRCol}>{payment.transactionTime}</Column>
+ </Row>
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>Status Pembayaran</Column>
+ <Column style={{ ...style.descriptionRCol }}>
+ {statusPayment == 'success' && (
+ <div style={{ ...style.badge, ...style.badgeGreen }}>Berhasil</div>
+ )}
+ {statusPayment == 'pending' && (
+ <div style={{ ...style.badge, ...style.badgeRed }}>Pending</div>
+ )}
+ {statusPayment == 'failed' && (
+ <div style={{ ...style.badge, ...style.badgeRed }}>Tidak Berhasil</div>
+ )}
+ </Column>
+ </Row>
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>Metode Pembayaran</Column>
+ <Column style={style.descriptionRCol}>
+ {toTitleCase(payment.paymentType.replaceAll('_', ' '))}
+ </Column>
+ </Row>
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>Batas Akhir Pembayaran</Column>
+ <Column style={style.descriptionRCol}>{payment.expiryTime}</Column>
+ </Row>
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>Nominal Transfer</Column>
+ <Column style={style.descriptionRCol}>
+ <span style={{ fontWeight: '600' }}>{currencyFormat(payment.grossAmount)}</span>
+ </Column>
+ </Row>
- <Section style={style.alert}>
- {statusPayment == 'success' &&
- 'Struk ini dapat anda simpan sebagai bukti tambahan dalam transaksi yang telah dilakukan.'}
- {statusPayment == 'pending' &&
- 'Kami akan menginformasikan melalui email setelah anda berhasil melakukan pembayaran.'}
- {statusPayment == 'failed' &&
- 'Dimohon untuk tidak melakukan pembayaran. Karena transaksi anda tidak berhasil dibuat.'}
- </Section>
+ <Text style={{ ...style.text, lineHeight: '100%', marginTop: '24px' }}>
+ <strong>Detail Produk</strong>
+ </Text>
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>No Transaksi (SO)</Column>
- <Column style={style.descriptionRCol}>{transaction.name}</Column>
- </Row>
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>Tanggal Transaksi</Column>
- <Column style={style.descriptionRCol}>{payment.transactionTime}</Column>
- </Row>
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>Status Pembayaran</Column>
- <Column style={{ ...style.descriptionRCol }}>
- {statusPayment == 'success' && (
- <div style={{ ...style.badge, ...style.badgeGreen }}>Berhasil</div>
- )}
- {statusPayment == 'pending' && (
- <div style={{ ...style.badge, ...style.badgeRed }}>Pending</div>
- )}
- {statusPayment == 'failed' && (
- <div style={{ ...style.badge, ...style.badgeRed }}>Tidak Berhasil</div>
- )}
- </Column>
- </Row>
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>Metode Pembayaran</Column>
- <Column style={style.descriptionRCol}>
- {toTitleCase(payment.paymentType.replaceAll('_', ' '))}
- </Column>
- </Row>
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>Batas Akhir Pembayaran</Column>
- <Column style={style.descriptionRCol}>{payment.expiryTime}</Column>
- </Row>
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>Nominal Transfer</Column>
- <Column style={style.descriptionRCol}>
- <span style={{ fontWeight: '600' }}>{currencyFormat(payment.grossAmount)}</span>
- </Column>
- </Row>
+ <Hr style={style.hr} />
- <Text style={{ ...style.text, lineHeight: '100%', marginTop: '24px' }}>
- <strong>Detail Produk</strong>
- </Text>
+ {transaction.products.map((product) => (
+ <Row style={style.productRow} key={product.id}>
+ <Column style={style.productLCol}>
+ <Img src={product.parent.image} width='100%' />
+ </Column>
+ <Column style={style.productRCol}>
+ <Text style={style.productName}>{product.name}</Text>
+ <Text style={style.productCode}>{product.code}</Text>
+ <div style={{ dislay: 'flex' }}>
+ <span style={style.productPriceA}>
+ {currencyFormat(product.price.priceDiscount)}
+ </span>
+ {product.price.discountPercentage > 0 && (
+ <>
+ &nbsp;
+ <span style={style.productPriceB}>
+ {currencyFormat(product.price.price)}
+ </span>
+ </>
+ )}
+ &nbsp; x {product.quantity} barang
+ </div>
+ </Column>
+ </Row>
+ ))}
- <Hr style={style.hr} />
+ <Hr style={style.hr} />
- {transaction.products.map((product) => (
- <Row style={style.productRow} key={product.id}>
- <Column style={style.productLCol}>
- <Img src={product.parent.image} width='100%' />
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>Subtotal</Column>
+ <Column style={style.descriptionRCol}>
+ {currencyFormat(transaction.subtotal)}
</Column>
- <Column style={style.productRCol}>
- <Text style={style.productName}>{product.name}</Text>
- <Text style={style.productCode}>{product.code}</Text>
- <div style={{ dislay: 'flex' }}>
- <span style={style.productPriceA}>
- {currencyFormat(product.price.priceDiscount)}
- </span>
- {product.price.discountPercentage > 0 && (
- <>
- &nbsp;
- <span style={style.productPriceB}>
- {currencyFormat(product.price.price)}
- </span>
- </>
- )}
- &nbsp; x {product.quantity} barang
- </div>
+ </Row>
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>Total Diskon</Column>
+ <Column style={{ ...style.descriptionRCol, color: '#E20613' }}>
+ {currencyFormat(transaction.discountTotal)}
+ </Column>
+ </Row>
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>PPN 11% (Incl.)</Column>
+ <Column style={style.descriptionRCol}>
+ {currencyFormat(transaction.subtotal * 0.11)}
</Column>
</Row>
- ))}
-
- <Hr style={style.hr} />
-
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>Subtotal</Column>
- <Column style={style.descriptionRCol}>
- {currencyFormat(transaction.subtotal)}
- </Column>
- </Row>
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>Total Diskon</Column>
- <Column style={{ ...style.descriptionRCol, color: '#E20613' }}>
- {currencyFormat(transaction.discountTotal)}
- </Column>
- </Row>
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>PPN 11% (Incl.)</Column>
- <Column style={style.descriptionRCol}>
- {currencyFormat(transaction.subtotal * 0.11)}
- </Column>
- </Row>
-
- <Hr style={style.hr} />
- <Row style={style.descriptionRow}>
- <Column style={style.descriptionLCol}>Grand Total</Column>
- <Column style={style.descriptionRCol}>
- <span style={{ fontWeight: '600' }}>
- {currencyFormat(transaction.amountTotal)}
- </span>
- </Column>
- </Row>
+ <Hr style={style.hr} />
- <Hr style={style.hr} />
- </>
- )}
+ <Row style={style.descriptionRow}>
+ <Column style={style.descriptionLCol}>Grand Total</Column>
+ <Column style={style.descriptionRCol}>
+ <span style={{ fontWeight: '600' }}>
+ {transaction.amountTotal > 0 ? currencyFormat(transaction.amountTotal) : '0'}
+ </span>
+ </Column>
+ </Row>
- {statusPayment == 'manual' && (
- <>
- <Text style={style.text}>
- Dengan cara dibawah ini:
- <ul>
- <li>
- Lakukan pembayaran manual via mobile app perbankan{' '}
- {transaction.address.customer.name}
- <br />
- Nama Bank: Bank Central Asia (BCA)
- <br />
- No. Rek: 8870400081
- <br />
- A/N: INDOTEKNIK DOTCOM GEMILANG PT
- </li>
- <li>
- Setelah berhasil melakukan pembayaran, mohon agar melakukan Screen Capture bukti
- bayar sebagai bukti untuk kami bahwa {transaction.address.customer.name} telah
- melakukan transaksi pembayaran
- </li>
- <li>
- Kirimkan bukti transaksi pembayaran anda dengan melakukan reply / balas email
- ini dengan melampirkan bukti di attachment / lampiran
- </li>
- <li>
- Transaksi {transaction.address.customer.name} akan segera diproses oleh salah
- satu Account Representative Indoteknik
- </li>
- </ul>
- </Text>
- <Text style={style.text}>
- Jika ada pertanyaan seputar teknis pembayaran {transaction.address.customer.name}{' '}
- dapat hubungi kami melalui Email{' '}
- <a href='mailto:sales@indoteknik.com'>(sales@indoteknik.com)</a> atau Whatsapp{' '}
- <a href={whatsappUrl()} target='_blank' rel='noreferrer'>
- (+62 812-8080-622)
- </a>
- .
- </Text>
- <Text style={style.text}>
- Terima kasih atas perhatiannya, selamat kembali beraktifitas
- </Text>
- </>
- )}
+ <Hr style={style.hr} />
+ </>
+ )}
+ {statusPayment == 'manual' && (
+ <>
+ <Text style={style.text}>
+ Dengan cara dibawah ini:
+ <ul>
+ <li>
+ Lakukan pembayaran manual via mobile app perbankan{' '}
+ {transaction.address.customer.name}
+ <br />
+ Nama Bank: Bank Central Asia (BCA)
+ <br />
+ No. Rek: 8870400081
+ <br />
+ A/N: INDOTEKNIK DOTCOM GEMILANG PT
+ </li>
+ <li>
+ Setelah berhasil melakukan pembayaran, mohon agar melakukan Screen Capture bukti
+ bayar sebagai bukti untuk kami bahwa {transaction.address.customer.name} telah
+ melakukan transaksi pembayaran
+ </li>
+ <li>
+ Kirimkan bukti transaksi pembayaran anda dengan melakukan reply / balas email
+ ini dengan melampirkan bukti di attachment / lampiran
+ </li>
+ <li>
+ Transaksi {transaction.address.customer.name} akan segera diproses oleh salah
+ satu Account Representative Indoteknik
+ </li>
+ </ul>
+ </Text>
+ <Text style={style.text}>
+ Jika ada pertanyaan seputar teknis pembayaran {transaction.address.customer.name}{' '}
+ dapat hubungi kami melalui Email{' '}
+ <a href='mailto:sales@indoteknik.com'>(sales@indoteknik.com)</a> atau Whatsapp{' '}
+ <a href={whatsappUrl()} target='_blank' rel='noreferrer'>
+ (+62 812-8080-622)
+ </a>
+ .
+ </Text>
+ <Text style={style.text}>
+ Terima kasih atas perhatiannya, selamat kembali beraktifitas
+ </Text>
+ </>
+ )}
+ </div>
+ : <FinishCheckout query={{order_id:transaction.name}}/>
+ }
<Text style={{ ...style.text, margin: '12px 0 3px' }}>Best regards,</Text>
<Text style={{ ...style.text, margin: '3px 0 0' }}>
diff --git a/src/lib/flashSale/components/FlashSale.jsx b/src/lib/flashSale/components/FlashSale.jsx
index 3d5c4e0e..5be6d4e3 100644
--- a/src/lib/flashSale/components/FlashSale.jsx
+++ b/src/lib/flashSale/components/FlashSale.jsx
@@ -1,26 +1,28 @@
-import { useEffect, useState } from 'react'
-import flashSaleApi from '../api/flashSaleApi'
-import Image from 'next/image'
-import CountDown from '@/core/components/elements/CountDown/CountDown'
-import productSearchApi from '@/lib/product/api/productSearchApi'
-import ProductSlider from '@/lib/product/components/ProductSlider'
-import { FlashSaleSkeleton } from '../skeleton/FlashSaleSkeleton'
+import Image from 'next/image';
+import { useEffect, useState } from 'react';
+
+import CountDown from '@/core/components/elements/CountDown/CountDown';
+import productSearchApi from '@/lib/product/api/productSearchApi';
+import ProductSlider from '@/lib/product/components/ProductSlider';
+
+import flashSaleApi from '../api/flashSaleApi';
+import { FlashSaleSkeleton } from '../skeleton/FlashSaleSkeleton';
const FlashSale = () => {
- const [flashSales, setFlashSales] = useState(null)
- const [isLoading, setIsLoading] = useState(true)
+ const [flashSales, setFlashSales] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const loadFlashSales = async () => {
- const dataFlashSales = await flashSaleApi()
- setFlashSales(dataFlashSales)
- setIsLoading(false)
- }
- loadFlashSales()
- }, [])
+ const dataFlashSales = await flashSaleApi();
+ setFlashSales(dataFlashSales);
+ setIsLoading(false);
+ };
+ loadFlashSales();
+ }, []);
if (isLoading) {
- return <FlashSaleSkeleton />
+ return <FlashSaleSkeleton />;
}
return (
@@ -29,7 +31,9 @@ const FlashSale = () => {
{flashSales.map((flashSale, index) => (
<div key={index}>
<div className='flex gap-x-3 mb-4 justify-between sm:justify-start'>
- <div className='font-medium sm:text-h-lg mt-1.5'>{flashSale.name}</div>
+ <div className='font-medium sm:text-h-lg mt-1.5'>
+ {flashSale.name}
+ </div>
<CountDown initialTime={flashSale.duration} />
</div>
@@ -54,24 +58,24 @@ const FlashSale = () => {
))}
</div>
)
- )
-}
+ );
+};
const FlashSaleProduct = ({ flashSaleId }) => {
- const [products, setProducts] = useState(null)
+ const [products, setProducts] = useState(null);
useEffect(() => {
const loadProducts = async () => {
const dataProducts = await productSearchApi({
query: `fq=flashsale_id_i:${flashSaleId}&fq=flashsale_price_f:[1 TO *]&limit=500&orderBy=flashsale-price-asc`,
- operation: 'AND'
- })
- setProducts(dataProducts.response)
- }
- loadProducts()
- }, [flashSaleId])
+ operation: 'AND',
+ });
+ setProducts(dataProducts.response);
+ };
+ loadProducts();
+ }, [flashSaleId]);
- return <ProductSlider products={products} />
-}
+ return <ProductSlider products={products} />;
+};
-export default FlashSale
+export default FlashSale;
diff --git a/src/lib/form/components/KunjunganSales.jsx b/src/lib/form/components/KunjunganSales.jsx
index 7470395a..ffa8f135 100644
--- a/src/lib/form/components/KunjunganSales.jsx
+++ b/src/lib/form/components/KunjunganSales.jsx
@@ -10,6 +10,9 @@ import * as Yup from 'yup'
import createLeadApi from '../api/createLeadApi'
import PageContent from '@/lib/content/components/PageContent'
+import useAuth from '@/core/hooks/useAuth'
+import { useRouter } from 'next/router'
+
const KunjunganSales = () => {
const {
register,
@@ -23,10 +26,18 @@ const KunjunganSales = () => {
})
const [cities, setCities] = useState([])
const [companyTypes, setCompanyTypes] = useState([])
+ const router = useRouter()
+
+ const auth = useAuth()
+
+
const recaptchaRef = useRef(null)
useEffect(() => {
+ if(auth == false) {
+ router.push('/login')
+ }
const loadCities = async () => {
let dataCities = await cityApi()
dataCities = dataCities.map((obj) => ({ value: obj.name, label: obj.name }))
@@ -39,7 +50,7 @@ const KunjunganSales = () => {
loadCompanyTypes()
loadCities()
- }, [])
+ }, [auth])
const onSubmitHandler = async (values) => {
const recaptchaValue = recaptchaRef.current.getValue()
diff --git a/src/lib/form/components/KunjunganService.jsx b/src/lib/form/components/KunjunganService.jsx
index 1cb0b446..5720d14e 100644
--- a/src/lib/form/components/KunjunganService.jsx
+++ b/src/lib/form/components/KunjunganService.jsx
@@ -8,6 +8,9 @@ import { toast } from 'react-hot-toast'
import * as Yup from 'yup'
import createLeadApi from '../api/createLeadApi'
import PageContent from '@/lib/content/components/PageContent'
+import { useRouter } from 'next/router'
+
+import useAuth from '@/core/hooks/useAuth'
const CreateKunjunganService = () => {
const {
@@ -22,17 +25,24 @@ const CreateKunjunganService = () => {
})
const [cities, setCities] = useState([])
const [company_unit, setCompany_unit] = useState([])
+
+ const router = useRouter()
+
+ const auth = useAuth()
const recaptchaRef = useRef(null)
useEffect(() => {
+ if(auth == false) {
+ router.push('/login')
+ }
const loadCities = async () => {
let dataCities = await cityApi()
dataCities = dataCities.map((city) => ({ value: city.id, label: city.name }))
setCities(dataCities)
}
loadCities()
- }, [])
+ }, [auth])
const onSubmitHandler = async (values) => {
const recaptchaValue = recaptchaRef.current.getValue()
diff --git a/src/lib/form/components/Merchant.jsx b/src/lib/form/components/Merchant.jsx
index 6c1af231..85f72bf8 100644
--- a/src/lib/form/components/Merchant.jsx
+++ b/src/lib/form/components/Merchant.jsx
@@ -8,6 +8,9 @@ import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
import createLeadApi from '../api/createLeadApi';
import PageContent from '@/lib/content/components/PageContent';
+import { useRouter } from 'next/router';
+import useAuth from '@/core/hooks/useAuth'
+
const CreateMerchant = () => {
const {
@@ -50,8 +53,14 @@ const CreateMerchant = () => {
const [company_unit, setCompany_unit] = useState(list_unit);
const recaptchaRef = useRef(null);
+ const router = useRouter()
+
+ const auth = useAuth()
useEffect(() => {
+ if(auth == false) {
+ router.push('/login')
+ }
const loadCities = async () => {
let dataCities = await cityApi();
dataCities = dataCities.map((city) => ({
@@ -61,7 +70,7 @@ const CreateMerchant = () => {
setCities(dataCities);
};
loadCities();
- }, []);
+ }, [auth]);
const onSubmitHandler = async (values) => {
const recaptchaValue = recaptchaRef.current.getValue();
diff --git a/src/lib/form/components/PembayaranTempo.jsx b/src/lib/form/components/PembayaranTempo.jsx
index 8c624fe2..fc4d248a 100644
--- a/src/lib/form/components/PembayaranTempo.jsx
+++ b/src/lib/form/components/PembayaranTempo.jsx
@@ -1,12 +1,15 @@
import getFileBase64 from '@/core/utils/getFileBase64'
import { yupResolver } from '@hookform/resolvers/yup'
-import React, { useRef } from 'react'
+import React, { useEffect, useRef } from 'react'
import ReCAPTCHA from 'react-google-recaptcha'
import { useForm } from 'react-hook-form'
import { toast } from 'react-hot-toast'
import * as Yup from 'yup'
import createLeadApi from '../api/createLeadApi'
import PageContent from '@/lib/content/components/PageContent'
+import { useRouter } from 'next/router'
+
+import useAuth from '@/core/hooks/useAuth'
const PembayaranTempo = () => {
@@ -21,6 +24,15 @@ const PembayaranTempo = () => {
})
const recaptchaRef = useRef(null)
+ const router = useRouter()
+
+ const auth = useAuth()
+
+ useEffect(() => {
+ if(auth == false) {
+ router.push('/login')
+ }
+ },[auth])
const onSubmitHandler = async (values) => {
const recaptchaValue = recaptchaRef.current.getValue()
diff --git a/src/lib/form/components/RequestForQuotation.jsx b/src/lib/form/components/RequestForQuotation.jsx
index fa526d5f..68b7fa17 100644
--- a/src/lib/form/components/RequestForQuotation.jsx
+++ b/src/lib/form/components/RequestForQuotation.jsx
@@ -10,6 +10,9 @@ import * as Yup from 'yup'
import createLeadApi from '../api/createLeadApi'
import getFileBase64 from '@/core/utils/getFileBase64'
import PageContent from '@/lib/content/components/PageContent'
+import { useRouter } from 'next/router'
+
+import useAuth from '@/core/hooks/useAuth'
const RequestForQuotation = () => {
const {
@@ -26,15 +29,22 @@ const RequestForQuotation = () => {
const quotationFileRef = useRef(null)
const recaptchaRef = useRef(null)
+ const router = useRouter()
+
+ const auth = useAuth()
+
useEffect(() => {
+ if(auth == false) {
+ router.push('/login')
+ }
const loadCities = async () => {
let dataCities = await cityApi()
dataCities = dataCities.map((obj) => ({ value: obj.name, label: obj.name }))
setCities(dataCities)
}
loadCities()
- }, [])
+ }, [auth])
const onSubmitHandler = async (values) => {
const recaptchaValue = recaptchaRef.current.getValue()
diff --git a/src/lib/form/components/SuratDukungan.jsx b/src/lib/form/components/SuratDukungan.jsx
index d73c3fab..31e7ee83 100644
--- a/src/lib/form/components/SuratDukungan.jsx
+++ b/src/lib/form/components/SuratDukungan.jsx
@@ -10,6 +10,9 @@ import createLeadsApi from '../api/createLeadApi';
import PageContent from '@/lib/content/components/PageContent';
+import useAuth from '@/core/hooks/useAuth'
+import { useRouter } from 'next/router';
+
const CreateSuratDukungan = () => {
const {
register,
@@ -25,8 +28,14 @@ const CreateSuratDukungan = () => {
const [company_unit, setCompany_unit] = useState([]);
const recaptchaRef = useRef(null);
+ const router = useRouter()
+
+ const auth = useAuth()
useEffect(() => {
+ if(auth == false) {
+ router.push('/login')
+ }
const loadCities = async () => {
let dataCities = await cityApi();
dataCities = dataCities.map((city) => ({
@@ -36,7 +45,7 @@ const CreateSuratDukungan = () => {
setCities(dataCities);
};
loadCities();
- }, []);
+ }, [auth]);
const onSubmitHandler = async (values) => {
const recaptchaValue = recaptchaRef.current.getValue();
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/home/components/PreferredBrand.jsx b/src/lib/home/components/PreferredBrand.jsx
index 571c4745..ec09aa4e 100644
--- a/src/lib/home/components/PreferredBrand.jsx
+++ b/src/lib/home/components/PreferredBrand.jsx
@@ -1,13 +1,41 @@
import { Swiper, SwiperSlide } from 'swiper/react'
+import { useCallback, useEffect, useState } from 'react'
import usePreferredBrand from '../hooks/usePreferredBrand'
import PreferredBrandSkeleton from './Skeleton/PreferredBrandSkeleton'
import BrandCard from '@/lib/brand/components/BrandCard'
import useDevice from '@/core/hooks/useDevice'
import Link from '@/core/components/elements/Link/Link'
+import axios from 'axios'
const PreferredBrand = () => {
let query = 'level_s'
let params = 'prioritas'
+ const [isLoading, setIsLoading] = useState(true)
+ const [startWith, setStartWith] = useState(null)
+ const [manufactures, setManufactures] = useState([])
+
+ const loadBrand = useCallback(async () => {
+ setIsLoading(true)
+ const name = startWith ? `${startWith}*` : ''
+ const result = await axios(`${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/brands?params=${name}`)
+
+ setIsLoading(false)
+ setManufactures((manufactures) => [...result.data])
+ }, [startWith])
+
+ const toggleStartWith = (alphabet) => {
+ setManufactures([])
+ if (alphabet == startWith) {
+ setStartWith(null)
+ return
+ }
+ setStartWith(alphabet)
+ }
+
+ useEffect(() => {
+ loadBrand()
+ }, [loadBrand])
+
const { preferredBrands } = usePreferredBrand(query)
const { isMobile, isDesktop } = useDevice()
@@ -21,12 +49,12 @@ const PreferredBrand = () => {
</Link>
)}
</div>
- {preferredBrands.isLoading && <PreferredBrandSkeleton />}
- {!preferredBrands.isLoading && (
+ {manufactures.isLoading && <PreferredBrandSkeleton />}
+ {!manufactures.isLoading && (
<Swiper slidesPerView={isMobile ? 3.5 : 7.5} spaceBetween={isMobile ? 12 : 24} freeMode>
- {preferredBrands.data?.data.map((brand) => (
- <SwiperSlide key={brand.id}>
- <BrandCard brand={brand} />
+ {manufactures.map((manufacture) => (
+ <SwiperSlide key={manufacture.id}>
+ <BrandCard brand={manufacture} />
</SwiperSlide>
))}
</Swiper>
diff --git a/src/lib/home/components/PromotionProgram.jsx b/src/lib/home/components/PromotionProgram.jsx
new file mode 100644
index 00000000..b204df8e
--- /dev/null
+++ b/src/lib/home/components/PromotionProgram.jsx
@@ -0,0 +1,66 @@
+import Link from '@/core/components/elements/Link/Link'
+import Image from 'next/image'
+import { bannerApi } from '@/api/bannerApi';
+import useDevice from '@/core/hooks/useDevice'
+import { Swiper, SwiperSlide } from 'swiper/react';
+const { useQuery } = require('react-query')
+const BannerSection = () => {
+ const promotionProgram = useQuery('promotionProgram', bannerApi({ type: 'banner-promotion' }));
+ const { isMobile, isDesktop } = useDevice()
+
+ return (
+ <div className='px-4 sm:px-0'>
+ <div className='flex justify-between items-center mb-4 '>
+ <div className='font-semibold sm:text-h-lg'>Promo Tersedia</div>
+ {isDesktop && (
+ <div></div>
+ // <Link href='/shop/promo' className='!text-red-500 font-semibold'>
+ // Lihat Semua
+ // </Link>
+ )}
+ </div>
+ {isDesktop && (promotionProgram.data &&
+ promotionProgram.data?.length > 0 && (
+ <div className='grid grid-cols-3 sm:grid-cols-3 gap-4 rounded-md'>
+ {promotionProgram.data?.map((banner) => (
+ <Link key={banner.id} href={banner.url}>
+ <Image
+ width={439}
+ height={150}
+ quality={100}
+ src={banner.image}
+ alt={banner.name}
+ className='h-auto w-full rounded hover:scale-105 transition duration-500 ease-in-out'
+ />
+ </Link>
+ ))}
+ </div>
+
+ ))}
+
+{isMobile && (
+
+ <Swiper slidesPerView={1.1} spaceBetween={8} freeMode>
+ {promotionProgram.data?.map((banner) => (
+ <SwiperSlide key={banner.id}>
+ <Link key={banner.id} href={banner.url}>
+ <Image
+ width={439}
+ height={150}
+ quality={100}
+ src={banner.image}
+ alt={banner.name}
+ className='h-auto w-full rounded '
+ />
+ </Link>
+ </SwiperSlide>
+ ))}
+ </Swiper>
+
+ )}
+ </div>
+
+ )
+}
+
+export default BannerSection
diff --git a/src/lib/product/components/Product/ProductDesktop.jsx b/src/lib/product/components/Product/ProductDesktop.jsx
index 5f034c09..444ddd8e 100644
--- a/src/lib/product/components/Product/ProductDesktop.jsx
+++ b/src/lib/product/components/Product/ProductDesktop.jsx
@@ -1,416 +1,442 @@
-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/BG-FLASH-SALE.jpg'
+ }
+ width={1000}
+ height={100}
+ className='h-full'
+ />
+ </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-1`}
+ >
+ <ImageNext
+ src='/images/ICON_FLASH_SALE_WEBSITE_INDOTEKNIK.svg'
+ width={17}
+ height={10}
+ />
+ <span className='text-white text-sm 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 +467,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 +483,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 +496,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 +508,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 +561,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 +578,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 +590,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 +647,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'>&#128498;</span>
+ <span className='blink-color-flash-sale'>
+ &#128498;
+ </span>
)}
{variant.code}
</td>
@@ -580,11 +658,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 +676,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 +691,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 +701,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 +792,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 +818,7 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
></PromotionType>
</div>
</BottomPopup>
+
<BottomPopup
className='!container'
title='Berhasil Ditambahkan'
@@ -742,16 +833,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 +857,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/ProductDesktopVariant.jsx b/src/lib/product/components/Product/ProductDesktopVariant.jsx
index ef61bafd..09b30a44 100644
--- a/src/lib/product/components/Product/ProductDesktopVariant.jsx
+++ b/src/lib/product/components/Product/ProductDesktopVariant.jsx
@@ -1,137 +1,155 @@
-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 useAuth from '@/core/hooks/useAuth'
-import odooApi from '@/core/api/odooApi'
-import { useProductCartContext } from '@/contexts/ProductCartContext'
-import { Box, Skeleton, Tooltip } from '@chakra-ui/react'
-import { Info } from 'lucide-react'
-
-const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant }) => {
- const router = useRouter()
- const auth = useAuth()
- const { slug } = router.query
-
- const [lowestPrice, setLowestPrice] = useState(null)
-
- const [addCartAlert, setAddCartAlert] = useState(false)
- const [isLoadingSLA, setIsLoadingSLA] = useState(true)
-
- const { setRefreshCart } = useProductCartContext()
+
+import { Box, Skeleton, Tooltip } from '@chakra-ui/react';
+import { HeartIcon } from '@heroicons/react/24/outline';
+import { Info } from 'lucide-react';
+import { useRouter } from 'next/router';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { toast } from 'react-hot-toast';
+import LazyLoad from 'react-lazy-load';
+
+import { useProductCartContext } from '@/contexts/ProductCartContext';
+import odooApi from '@/core/api/odooApi';
+import Image from '@/core/components/elements/Image/Image';
+import Link from '@/core/components/elements/Link/Link';
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
+import DesktopView from '@/core/components/views/DesktopView';
+import useAuth from '@/core/hooks/useAuth';
+import { updateItemCart } from '@/core/utils/cart';
+import currencyFormat from '@/core/utils/currencyFormat';
+import { createSlug } from '@/core/utils/slug';
+import whatsappUrl from '@/core/utils/whatsappUrl';
+
+import productSimilarApi from '../../api/productSimilarApi';
+import ProductCard from '../ProductCard';
+import ProductSimilar from '../ProductSimilar';
+
+const ProductDesktopVariant = ({
+ product,
+ wishlist,
+ toggleWishlist,
+ isVariant,
+}) => {
+ const router = useRouter();
+ const auth = useAuth();
+ const { slug } = router.query;
+
+ const [lowestPrice, setLowestPrice] = useState(null);
+
+ const [addCartAlert, setAddCartAlert] = useState(false);
+ const [isLoadingSLA, setIsLoadingSLA] = useState(true);
+
+ const { setRefreshCart } = useProductCartContext();
const getLowestPrice = useCallback(() => {
- const lowest = product.price
+ const lowest = product.price;
/* const lowest = prices.reduce((lowest, price) => {
return price.priceDiscount < lowest.priceDiscount ? price : lowest
}, prices[0])*/
- return lowest
- }, [product])
+ return lowest;
+ }, [product]);
useEffect(() => {
- const lowest = getLowestPrice()
- setLowestPrice(lowest)
- }, [getLowestPrice])
+ const lowest = getLowestPrice();
+ setLowestPrice(lowest);
+ }, [getLowestPrice]);
- const [informationTab, setInformationTab] = useState(informationTabOptions[0].value)
+ const [informationTab, setInformationTab] = useState(
+ informationTabOptions[0].value
+ );
- const variantQuantityRefs = useRef([])
+ const variantQuantityRefs = useRef([]);
const setVariantQuantityRef = (variantId) => (element) => {
- 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 handleAddToCart = (variant) => {
if (!auth) {
- router.push(`/login?next=/shop/product/${slug}`)
- return
+ router.push(`/login?next=/shop/product/${slug}`);
+ return;
}
- const quantity = variantQuantityRefs.current[product.id].value
- if (!validQuantity(quantity)) return
+ const quantity = variantQuantityRefs.current[product.id].value;
+ if (!validQuantity(quantity)) return;
updateItemCart({
productId: product.id,
quantity,
programLineId: null,
selected: true,
- source: null
+ source: null,
}).then(() => {
- setRefreshCart(true)
- })
- setAddCartAlert(true)
- }
+ setRefreshCart(true);
+ });
+ setAddCartAlert(true);
+ };
const handleBuy = (variant) => {
- const quantity = variantQuantityRefs.current[product.id].value
- if (!validQuantity(quantity)) return
+ const quantity = variantQuantityRefs.current[product.id].value;
+ if (!validQuantity(quantity)) return;
updateItemCart({
productId: variant,
quantity,
programLineId: null,
selected: true,
- source: 'buy'
- })
- router.push(`/shop/checkout?source=buy`)
- }
+ source: 'buy',
+ });
+ 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 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 dataProductSimilar = await productSimilarApi({ query: productSimilarQuery })
- setProductSimilarInBrand(dataProductSimilar.products)
- }
- if (!productSimilarInBrand) loadProductSimilarInBrand()
- }, [product, productSimilarInBrand])
+ const productSimilarQuery = [
+ product?.name,
+ `fq=-product_id_i:${product.id}`,
+ ].join('&');
+ const dataProductSimilar = await productSimilarApi({
+ query: productSimilarQuery,
+ });
+ setProductSimilarInBrand(dataProductSimilar.products);
+ };
+ if (!productSimilarInBrand) loadProductSimilarInBrand();
+ }, [product, productSimilarInBrand]);
useEffect(() => {
const fetchData = async () => {
- const dataSLA = await odooApi('GET', `/api/v1/product_variant/${product.id}/stock`)
- product.sla = dataSLA
+ const dataSLA = await odooApi(
+ 'GET',
+ `/api/v1/product_variant/${product.id}/stock`
+ );
+ product.sla = dataSLA;
- setIsLoadingSLA(false)
- }
- fetchData()
- }, [product])
+ setIsLoadingSLA(false);
+ };
+ fetchData();
+ }, [product]);
return (
<DesktopView>
@@ -140,14 +158,16 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
<div className='w-full flex flex-wrap'>
<div className='w-5/12'>
<Image
- src={product.image}
+ src={product.image + '?variant=True'}
alt={product.name}
className='h-[430px] object-contain object-center w-full border border-gray_r-4'
/>
</div>
<div className='w-7/12 px-4'>
- <h1 className='text-title-md leading-10 font-medium'>{product?.name}</h1>
+ <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>
@@ -177,7 +197,9 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
</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-4/12 text-gray_r-12/70'>
+ Persiapan Barang
+ </div>
<div className='w-8/12'>
{!product?.sla && <Skeleton width='20%' height='16px' />}
{product?.sla && (
@@ -203,8 +225,13 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
<a
href={whatsappUrl('product', {
name: product.name,
- manufacture: product?.manufacture?.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'
>
@@ -221,7 +248,12 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
<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'
>
@@ -233,44 +265,23 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
</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-7 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> */}
+ <div className='p-4 md:p-6 md:bg-gray-50 rounded-xl'>
+ <h2 className='text-h-md md:text-h-lg font-medium'>Informasi Produk</h2>
+ <div className='h-4' />
+ <div
+ className='leading-relaxed text-gray-700'
+ dangerouslySetInnerHTML={{
+ __html:
+ !product.parent.description || product.parent.description == '<p><br></p>'
+ ? 'Belum ada deskripsi'
+ : product.parent.description,
+ }}
+ />
+ </div>
</div>
<div className='w-[25%]'>
- {product?.isFlashsale > 0 && product?.price?.discountPercentage > 0? (
+ {product?.isFlashsale > 0 &&
+ product?.price?.discountPercentage > 0 ? (
<>
<div className='flex gap-x-1 items-center mt-2'>
<div className='badge-solid-red text-caption-1'>
@@ -285,7 +296,9 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
</div>
<div className='text-gray_r-9 text-base font-normal mt-1'>
Termasuk PPN:{' '}
- {currencyFormat(product?.price?.priceDiscount * process.env.NEXT_PUBLIC_PPN)}
+ {currencyFormat(
+ product?.price?.priceDiscount * process.env.NEXT_PUBLIC_PPN
+ )}
</div>
</>
) : (
@@ -295,7 +308,9 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
{currencyFormat(product?.price?.price)}
<div className='text-gray_r-9 text-base font-normal mt-1'>
Termasuk PPN:{' '}
- {currencyFormat(product?.price?.price * process.env.NEXT_PUBLIC_PPN)}
+ {currencyFormat(
+ product?.price?.price * process.env.NEXT_PUBLIC_PPN
+ )}
</div>
</>
) : (
@@ -305,7 +320,12 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
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'
@@ -340,7 +360,10 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
</button>
</div>
<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' />
) : (
@@ -366,7 +389,9 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
</div>
<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>
@@ -381,21 +406,28 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
<div className='flex mt-4'>
<div className='w-[10%]'>
<Image
- src={product.image}
+ src={product.image + '?variant=True'}
alt={product.name}
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>
@@ -403,29 +435,33 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant })
</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 ProductDesktopVariant
+export default ProductDesktopVariant;
diff --git a/src/lib/product/components/Product/ProductMobile.jsx b/src/lib/product/components/Product/ProductMobile.jsx
index e23e2fb9..113a1e42 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'>&#128498;</span>")
+ label.push("<span class='blink-color-flash-sale'>&#128498;</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,60 @@ 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/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 +247,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 +268,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 +296,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 +308,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 +319,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 +342,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 +381,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 +431,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 +452,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 +493,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 +507,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 +533,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 +563,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/Product/ProductMobileVariant.jsx b/src/lib/product/components/Product/ProductMobileVariant.jsx
index 9888e482..af9e52bb 100644
--- a/src/lib/product/components/Product/ProductMobileVariant.jsx
+++ b/src/lib/product/components/Product/ProductMobileVariant.jsx
@@ -1,37 +1,40 @@
-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 { gtagAddToCart } from '@/core/utils/googleTag'
-import odooApi from '@/core/api/odooApi'
-import { Skeleton } from '@chakra-ui/react'
+import { Skeleton } from '@chakra-ui/react';
+import { HeartIcon } from '@heroicons/react/24/outline';
+import { useRouter } from 'next/router';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-hot-toast';
+import LazyLoad from 'react-lazy-load';
+
+import odooApi from '@/core/api/odooApi';
+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 BottomPopup from '@/core/components/elements/Popup/BottomPopup';
+import MobileView from '@/core/components/views/MobileView';
+import { updateItemCart } from '@/core/utils/cart';
+import currencyFormat from '@/core/utils/currencyFormat';
+import { gtagAddToCart } from '@/core/utils/googleTag';
+import { createSlug } from '@/core/utils/slug';
+import whatsappUrl from '@/core/utils/whatsappUrl';
+
+import ProductSimilar from '../ProductSimilar';
const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
- const router = useRouter()
+ const router = useRouter();
- const [quantity, setQuantity] = useState('1')
- const [selectedVariant, setSelectedVariant] = useState(product.id)
- const [informationTab, setInformationTab] = useState(informationTabOptions[0].value)
- const [addCartAlert, setAddCartAlert] = useState(false)
+ const [quantity, setQuantity] = useState('1');
+ const [selectedVariant, setSelectedVariant] = useState(product.id);
+ const [informationTab, setInformationTab] = useState(
+ informationTabOptions[0].value
+ );
+ const [addCartAlert, setAddCartAlert] = useState(false);
- const [isLoadingSLA, setIsLoadingSLA] = useState(true)
+ const [isLoadingSLA, setIsLoadingSLA] = useState(true);
const getLowestPrice = () => {
- const lowest = product.lowestPrice
- return lowest
- }
+ const lowest = product.lowestPrice;
+ return lowest;
+ };
const [activeVariant, setActiveVariant] = useState({
id: null,
@@ -40,8 +43,8 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
price: getLowestPrice(),
stock: product.stockTotal,
weight: product.weight,
- isFlashSale: product.isFlashSale
- })
+ isFlashSale: product.isFlashSale,
+ });
useEffect(() => {
if (selectedVariant) {
@@ -52,70 +55,73 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
price: product.price,
stock: product.stockTotal,
weight: product.weight,
- isFlashSale: product.isFlashSale
- })
+ isFlashSale: product.isFlashSale,
+ });
}
- }, [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 handleClickCart = () => {
- if (!validAction()) return
- gtagAddToCart(activeVariant, quantity)
+ if (!validAction()) return;
+ gtagAddToCart(activeVariant, quantity);
updateItemCart({
productId: variant,
quantity,
programLineId: null,
selected: true,
- source: null
- })
- setAddCartAlert(true)
- }
+ source: null,
+ });
+ setAddCartAlert(true);
+ };
const handleClickBuy = () => {
- if (!validAction()) return
+ if (!validAction()) return;
updateItemCart({
productId: product.id,
quantity,
programLineId: null,
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('&');
useEffect(() => {
const fetchData = async () => {
- const dataSLA = await odooApi('GET', `/api/v1/product_variant/${product.id}/stock`)
- product.sla = dataSLA
+ const dataSLA = await odooApi(
+ 'GET',
+ `/api/v1/product_variant/${product.id}/stock`
+ );
+ product.sla = dataSLA;
- setIsLoadingSLA(false)
- }
- fetchData()
- }, [product])
+ setIsLoadingSLA(false);
+ };
+ fetchData();
+ }, [product]);
return (
<MobileView>
<Image
- src={product.image}
+ src={product.image + '?variant=True'}
alt={product.name}
className='h-72 object-contain object-center w-full border-b border-gray_r-4'
/>
@@ -124,7 +130,11 @@ const ProductMobileVariant = ({ 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>
@@ -141,10 +151,13 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
</div>
<h1 className='leading-6 font-medium mb-3'>{activeVariant?.name}</h1>
- {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'>{activeVariant?.price?.discountPercentage}%</div>
+ <div className='badge-solid-red'>
+ {activeVariant?.price?.discountPercentage}%
+ </div>
<div className='text-gray_r-11 line-through text-caption-1'>
{currencyFormat(activeVariant?.price?.price)}
</div>
@@ -154,7 +167,9 @@ const ProductMobileVariant = ({ 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>
</>
) : (
@@ -164,7 +179,9 @@ const ProductMobileVariant = ({ 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>
</>
) : (
@@ -173,7 +190,12 @@ const ProductMobileVariant = ({ 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'
>
@@ -199,10 +221,18 @@ const ProductMobileVariant = ({ 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>
@@ -238,7 +268,9 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
type='button'
title={`Masa Persiapan Barang ${product?.sla?.slaDate}`}
className={`flex gap-x-1 items-center p-2 h-8 rounded-lg w-full ${
- product?.sla?.slaDate === 'indent' ? 'bg-indigo-900' : 'btn-light'
+ product?.sla?.slaDate === 'indent'
+ ? 'bg-indigo-900'
+ : 'btn-light'
}`}
>
<div
@@ -281,14 +313,21 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
{activeVariant?.stock > 0 && (
<span className='flex gap-x-1.5'>
<div className='badge-solid-red'>Ready Stock</div>
- <div className='badge-gray'>{activeVariant?.stock > 5 ? '> 5' : '< 5'}</div>
+ <div className='badge-gray'>
+ {activeVariant?.stock > 5 ? '> 5' : '< 5'}
+ </div>
</span>
)}
{activeVariant?.stock == 0 && (
<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'
>
@@ -297,12 +336,19 @@ const ProductMobileVariant = ({ 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'
>
@@ -316,7 +362,10 @@ const ProductMobileVariant = ({ 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>
@@ -338,55 +387,68 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
<div className='flex mt-4'>
<div className='w-[15%]'>
<Image
- src={product.image}
+ src={product.image + '?variant=True'}
alt={product.name}
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: 'specification', label: 'Spesifikasi' },
// { value: 'description', label: 'Deskripsi' },
// { 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'>
<span className='text-gray_r-11'>{label}</span>
{children}
</div>
-)
+);
-export default ProductMobileVariant
+export default ProductMobileVariant;
diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx
index fa555bcf..98732407 100644
--- a/src/lib/product/components/ProductCard.jsx
+++ b/src/lib/product/components/ProductCard.jsx
@@ -1,38 +1,85 @@
-import Image from '@/core/components/elements/Image/Image'
-import Link from '@/core/components/elements/Link/Link'
-import currencyFormat from '@/core/utils/currencyFormat'
-import { sellingProductFormat } from '@/core/utils/formatValue'
-import { createSlug } from '@/core/utils/slug'
-import whatsappUrl from '@/core/utils/whatsappUrl'
-import ImageNext from 'next/image'
-import { useRouter } from 'next/router'
+import clsx from 'clsx';
+import ImageNext from 'next/image';
+import { useRouter } from 'next/router';
+import { useMemo, useEffect, useState } from 'react';
+
+import Image from '@/core/components/elements/Image/Image';
+import Link from '@/core/components/elements/Link/Link';
+import currencyFormat from '@/core/utils/currencyFormat';
+import { sellingProductFormat } from '@/core/utils/formatValue';
+import { createSlug } from '@/core/utils/slug';
+import whatsappUrl from '@/core/utils/whatsappUrl';
+import useUtmSource from '~/hooks/useUtmSource';
const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
- const router = useRouter()
+ const router = useRouter();
+ const utmSource = useUtmSource();
+
const callForPriceWhatsapp = 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),
+ });
+
+ const image = useMemo(() => {
+ if (product.image) return product.image + '?ratio=square';
+ return '/images/noimage.jpeg';
+ }, [product.image]);
+
+ const URL = {
+ product:
+ createSlug('/shop/product/', product?.name, product?.id) +
+ `?utm_source=${utmSource}`,
+ manufacture: createSlug(
+ '/shop/brands/',
+ product?.manufacture?.name,
+ product?.manufacture.id
+ ),
+ };
if (variant == 'vertical') {
return (
<div className='rounded shadow-sm border border-gray_r-4 bg-white h-[300px] md:h-[350px]'>
- <Link
- href={createSlug('/shop/product/', product?.name, product?.id)}
- className='border-b border-gray_r-4 relative'
- >
+ <Link href={URL.product} className='border-b border-gray_r-4 relative'>
+ <div className="relative">
<Image
- src={product?.image}
+ src={image}
alt={product?.name}
- className='w-full object-contain object-center h-36 sm:h-48'
+ className="gambarA w-full object-contain object-center h-36 sm:h-48"
/>
+ <div className="absolute top-0 right-0 flex mt-3">
+ <div className="gambarB ">
+ {product?.isSni && (
+ <ImageNext
+ src="/images/sni-logo.png"
+ alt="SNI Logo"
+ className="w-4 h-5 object-contain object-top sm:h-6"
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ <div className="gambarC ">
+ {product?.isTkdn && (
+ <ImageNext
+ src="/images/TKDN.png"
+ alt="TKDN"
+ className="w-11 h-6 object-contain object-top ml-1 mr-1 sm:h-6"
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ </div>
+ </div>
+
+
{router.pathname != '/' && product?.flashSale?.id > 0 && (
<div className='absolute bottom-0 w-full grid'>
<div className='absolute bottom-0 w-full h-full'>
<ImageNext
- src='/images/GAMBAR-BG-FLASH-SALE.jpg'
+ src='/images/BG-FLASH-SALE.jpg'
className='h-full'
width={1000}
height={100}
@@ -52,7 +99,8 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
height={5}
/>
<span className='text-white text-[9px] md:text-[10px] font-semibold'>
- {product?.flashSale?.tag != 'false' || product?.flashSale?.tag
+ {product?.flashSale?.tag != 'false' ||
+ product?.flashSale?.tag
? product?.flashSale?.tag
: 'FLASH SALE'}
</span>
@@ -69,27 +117,21 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
</Link>
<div className='p-2 sm:p-3 pb-3 text-caption-2 sm:text-body-2 leading-5'>
{product?.manufacture?.name ? (
- <Link
- href={createSlug(
- '/shop/brands/',
- product?.manufacture?.name,
- product?.manufacture.id
- )}
- className='mb-1'
- >
+ <Link href={URL.manufacture} className='mb-1'>
{product.manufacture.name}
</Link>
) : (
<div>-</div>
)}
<Link
- href={createSlug('/shop/product/', product?.name, product?.id)}
+ href={URL.product}
className={`mb-2 !text-gray_r-12 leading-6 block line-clamp-3 h-[64px]`}
title={product?.name}
>
{product?.name}
</Link>
- {product?.flashSale?.id > 0 && product?.lowestPrice.discountPercentage > 0 ? (
+ {product?.flashSale?.id > 0 &&
+ product?.lowestPrice.discountPercentage > 0 ? (
<>
<div className='flex gap-x-1 mb-1 items-center'>
<div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'>
@@ -103,7 +145,11 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
{product?.lowestPrice.priceDiscount > 0 ? (
currencyFormat(product?.lowestPrice.priceDiscount)
) : (
- <a rel='noopener noreferrer' target='_blank' href={callForPriceWhatsapp}>
+ <a
+ rel='noopener noreferrer'
+ target='_blank'
+ href={callForPriceWhatsapp}
+ >
Call for Inquiry
</a>
)}
@@ -116,11 +162,17 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
{currencyFormat(product?.lowestPrice.price)}
<div className='text-gray_r-9 text-[10px] font-normal mt-2'>
Inc. PPN:{' '}
- {currencyFormat(product.lowestPrice.price * process.env.NEXT_PUBLIC_PPN)}
+ {currencyFormat(
+ product.lowestPrice.price * process.env.NEXT_PUBLIC_PPN
+ )}
</div>
</>
) : (
- <a rel='noopener noreferrer' target='_blank' href={callForPriceWhatsapp}>
+ <a
+ rel='noopener noreferrer'
+ target='_blank'
+ href={callForPriceWhatsapp}
+ >
Call for Inquiry
</a>
)}
@@ -128,7 +180,9 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
)}
<div className='flex w-full items-center gap-x-1 '>
- {product?.stockTotal > 0 && <div className='badge-solid-red'>Ready Stock</div>}
+ {product?.stockTotal > 0 && (
+ <div className='badge-solid-red'>Ready Stock</div>
+ )}
{/* <div className='badge-gray'>{product?.stockTotal > 5 ? '> 5' : '< 5'}</div> */}
{product?.qtySold > 0 && (
<div className='text-gray_r-9 text-[11px]'>
@@ -138,22 +192,45 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
</div>
</div>
</div>
- )
+ );
}
if (variant == 'horizontal') {
return (
<div className='flex bg-white'>
<div className='w-4/12'>
- <Link
- href={createSlug('/shop/product/', product?.name, product?.id)}
- className='relative'
- >
+ <Link href={URL.product} className='relative'>
+ <div className="relative">
<Image
- src={product?.image}
+ src={image}
alt={product?.name}
- className='w-full object-contain object-center h-36'
+ className="gambarA w-full object-contain object-center h-36 sm:h-48"
/>
+ <div className="absolute top-0 right-0 flex mt-3">
+ <div className="gambarB ">
+ {product?.isSni && (
+ <ImageNext
+ src="/images/sni-logo.png"
+ alt="SNI Logo"
+ className="w-4 h-5 object-contain object-top sm:h-6"
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ <div className="gambarC ">
+ {product?.isTkdn && (
+ <ImageNext
+ src="/images/TKDN.png"
+ alt="TKDN"
+ className="w-11 h-6 object-contain object-top ml-1 sm:h-6"
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ </div>
+ </div>
{product.variantTotal > 1 && (
<div className='absolute badge-gray bottom-1.5 left-1.5'>
{product.variantTotal} Varian
@@ -178,26 +255,20 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
</div>
)}
{product?.manufacture?.name ? (
- <Link
- href={createSlug(
- '/shop/brands/',
- product?.manufacture?.name,
- product?.manufacture.id
- )}
- className='mb-1'
- >
+ <Link href={URL.manufacture} className='mb-1'>
{product.manufacture.name}
</Link>
) : (
<div>-</div>
)}
<Link
- href={createSlug('/shop/product/', product?.name, product?.id)}
+ href={URL.product}
className={`mb-3 !text-gray_r-12 leading-6 line-clamp-3`}
>
{product?.name}
</Link>
- {product?.flashSale?.id > 0 && product?.lowestPrice?.discountPercentage > 0 ? (
+ {product?.flashSale?.id > 0 &&
+ product?.lowestPrice?.discountPercentage > 0 ? (
<>
{product?.lowestPrice.discountPercentage > 0 && (
<div className='flex gap-x-1 mb-1 items-center'>
@@ -214,7 +285,11 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
{product?.lowestPrice?.priceDiscount > 0 ? (
currencyFormat(product?.lowestPrice?.priceDiscount)
) : (
- <a rel='noopener noreferrer' target='_blank' href={callForPriceWhatsapp}>
+ <a
+ rel='noopener noreferrer'
+ target='_blank'
+ href={callForPriceWhatsapp}
+ >
Call for Inquiry
</a>
)}
@@ -227,11 +302,17 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
{currencyFormat(product?.lowestPrice.price)}
<div className='text-gray_r-9 text-[11px] sm:text-caption-2 font-normal mt-2'>
Inc. PPN:{' '}
- {currencyFormat(product.lowestPrice.price * process.env.NEXT_PUBLIC_PPN)}
+ {currencyFormat(
+ product.lowestPrice.price * process.env.NEXT_PUBLIC_PPN
+ )}
</div>
</>
) : (
- <a rel='noopener noreferrer' target='_blank' href={callForPriceWhatsapp}>
+ <a
+ rel='noopener noreferrer'
+ target='_blank'
+ href={callForPriceWhatsapp}
+ >
Call for Inquiry
</a>
)}
@@ -239,7 +320,9 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
)}
<div className='flex w-full items-center gap-x-1 '>
- {product?.stockTotal > 0 && <div className='badge-solid-red'>Ready Stock</div>}
+ {product?.stockTotal > 0 && (
+ <div className='badge-solid-red'>Ready Stock</div>
+ )}
{/* <div className='badge-gray'>{product?.stockTotal > 5 ? '> 5' : '< 5'}</div> */}
{product?.qtySold > 0 && (
<div className='text-gray_r-9 text-[11px]'>
@@ -249,8 +332,8 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
</div>
</div>
</div>
- )
+ );
}
-}
+};
-export default ProductCard
+export default ProductCard;
diff --git a/src/lib/product/components/ProductFilterDesktop.jsx b/src/lib/product/components/ProductFilterDesktop.jsx
index e4a62abb..a8073036 100644
--- a/src/lib/product/components/ProductFilterDesktop.jsx
+++ b/src/lib/product/components/ProductFilterDesktop.jsx
@@ -21,6 +21,7 @@ import Image from '@/core/components/elements/Image/Image'
import { formatCurrency } from '@/core/utils/formatValue'
const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = null }) => {
+
const router = useRouter()
const { query } = router
const [order, setOrder] = useState(query?.orderBy)
@@ -102,7 +103,14 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu
}
params = _.pickBy(params, _.identity)
params = toQuery(params)
- router.push(`${prefixUrl}?${params}`)
+
+ const slug = Array.isArray(router.query.slug) ? router.query.slug[0] : router.query.slug;
+
+ if (slug) {
+ router.push(`${prefixUrl}/${slug}?${params}`)
+ } else {
+ router.push(`${prefixUrl}?${params}`)
+ }
}
diff --git a/src/lib/product/components/ProductFilterDesktopPromotion.jsx b/src/lib/product/components/ProductFilterDesktopPromotion.jsx
new file mode 100644
index 00000000..0815b881
--- /dev/null
+++ b/src/lib/product/components/ProductFilterDesktopPromotion.jsx
@@ -0,0 +1,132 @@
+import { useRouter } from 'next/router';
+import { useEffect, useState } from 'react';
+import _ from 'lodash';
+import { toQuery } from 'lodash-contrib';
+import { Button } from '@chakra-ui/react';
+import { MultiSelect } from 'react-multi-select-component';
+
+const ProductFilterDesktop = ({ brands, categories, prefixUrl }) => {
+ const router = useRouter();
+ const { query } = router;
+ const [order, setOrder] = useState(query?.orderBy);
+ const [brandValues, setBrand] = useState([]);
+ const [categoryValues, setCategory] = useState([]);
+ const [priceFrom, setPriceFrom] = useState(query?.priceFrom);
+ const [priceTo, setPriceTo] = useState(query?.priceTo);
+ const [stock, setStock] = useState(query?.stock);
+ const [activeRange, setActiveRange] = useState(null);
+ const [isBrandDropdownClicked, setIsBrandDropdownClicked] = useState(false);
+ const [isCategoryDropdownClicked, setIsCategoryDropdownClicked] = useState(false);
+
+ // Effect to set brandValues from query parameter 'brand'
+ useEffect(() => {
+ const brandParam = query?.brand;
+ if (brandParam) {
+ const brandsArray = brandParam.split(',').map((b) => ({
+ label: b,
+ value: b,
+ }));
+ setBrand(brandsArray);
+ }
+
+ }, [query.brand]); // Trigger effect whenever query.brand changes
+
+ useEffect(() => {
+ const categoryParam = query?.category;
+ if (categoryParam) {
+ const categoriesArray = categoryParam.split(',').map((c) => ({
+ label: c,
+ value: c,
+ }));
+ setCategory(categoriesArray);
+ }
+ }, [query.category]); // Trigger effect whenever query.category changes
+
+ const handleSubmit = () => {
+ let params = {
+ q: router.query.q,
+ orderBy: order,
+ brand: brandValues.map((b) => b.value).join(','),
+ category: categoryValues.map((c) => c.value).join(','),
+ priceFrom,
+ priceTo,
+ stock: stock,
+ };
+ params = _.pickBy(params, _.identity);
+ params = toQuery(params);
+
+ const slug = Array.isArray(router.query.slug)
+ ? router.query.slug[0]
+ : router.query.slug;
+
+ if (slug) {
+ router.push(`${prefixUrl}/${slug}?${params}`);
+ } else {
+ router.push(`${prefixUrl}?${params}`);
+ }
+ };
+
+
+ const brandOptions = brands.map((brand) => ({
+ label: `${brand.brand} (${brand.qty})`,
+ value: brand.brand,
+ }));
+
+ const categoryOptions = categories.map((category) => ({
+ label: `${category.name} (${category.qty})`,
+ value: category.name,
+ }));
+
+ return (
+ <>
+ <div className='flex h-full w-[100%] justify-end '>
+ {/* Brand MultiSelect */}
+ <div className='mb-[20px] mr-4 w-64 h-full flex justify-start '>
+ <div className='relative'>
+ <label>Brand</label>
+ <div className='h-auto z-50 w-64 '>
+ <MultiSelect
+ options={brandOptions}
+ value={brandValues}
+ onChange={setBrand}
+ labelledBy='Select Brand'
+ onMenuToggle={(isOpen) => setIsBrandDropdownClicked(isOpen)}
+ hasSelectAll={false}
+ />
+ </div>
+ </div>
+ </div>
+
+ {/* Category MultiSelect */}
+ <div className='mb-[20px] mr-4 w-64 h-full flex justify-start '>
+ <div className='relative'>
+ <label>Kategori</label>
+ <div className=' h-auto w-64'>
+ <MultiSelect
+ options={categoryOptions}
+ value={categoryValues}
+ onChange={setCategory}
+ labelledBy='Select Kategori'
+ onMenuToggle={() =>
+ setIsCategoryDropdownClicked(!isCategoryDropdownClicked)
+ }
+ hasSelectAll={false}
+ />
+ </div>
+ </div>
+ </div>
+
+ {/* Apply Button */}
+ <div className='TOMBOL mb-1 h-24 flex justify-center items-center w-24'>
+ <div className=' bottom-1 pb-1 left-0 right-0 flex justify-center rounded' >
+ <Button colorScheme='red' width={"full"} onClick={handleSubmit}>
+ Terapkan
+ </Button>
+ </div>
+ </div>
+ </div>
+ </>
+ );
+};
+
+export default ProductFilterDesktop;
diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx
index 29bb987e..b1a5d409 100644
--- a/src/lib/product/components/ProductSearch.jsx
+++ b/src/lib/product/components/ProductSearch.jsx
@@ -1,171 +1,204 @@
-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 NextImage from 'next/image';
+import { useRouter } from 'next/router';
+import { useEffect, useMemo, useState } from 'react';
+
+import { HStack, Image, Tag, TagCloseButton, TagLabel } from '@chakra-ui/react';
+import axios from 'axios';
+import _ from 'lodash';
+import { toQuery } from 'lodash-contrib';
+
+import odooApi from '@/core/api/odooApi';
+import searchSpellApi from '@/core/api/searchSpellApi';
+import Link from '@/core/components/elements/Link/Link';
+import Pagination from '@/core/components/elements/Pagination/Pagination';
+import DesktopView from '@/core/components/views/DesktopView';
+import MobileView from '@/core/components/views/MobileView';
+import useActive from '@/core/hooks/useActive';
+import { formatCurrency } from '@/core/utils/formatValue';
+import { createSlug } from '@/core/utils/slug';
+import whatsappUrl from '@/core/utils/whatsappUrl';
+
+import useProductSearch from '../hooks/useProductSearch';
+import ProductCard from './ProductCard';
+import ProductFilter from './ProductFilter';
+import ProductFilterDesktop from './ProductFilterDesktop';
+import ProductSearchSkeleton from './Skeleton/ProductSearchSkeleton';
+
+import SideBanner from '~/modules/side-banner';
+import FooterBanner from '~/modules/footer-banner';
+
+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') && q !== '*') {
+ checkIfBrand();
}
- if (router.pathname.includes('search')) {
- 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 });
}
}
+
const orderOptions = [
{ 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 +215,8 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null })
</Link>
))}
</>
- )
- }, [spellings])
+ );
+ }, [spellings]);
const handleDeleteFilter = async (source, value) => {
let params = {
@@ -192,51 +225,64 @@ 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}`);
+ };
+
+ const isNotReadyStockPage = router.asPath !== '/shop/search?orderBy=stock';
return (
<>
<MobileView>
{productSearch.isLoading && <ProductSearchSkeleton />}
<div className='p-4 pt-0'>
- {isBrand && isBrand.logo && (
+ {isNotReadyStockPage && 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>
+ <Link
+ href={createSlug('/shop/brands/', isBrand.name, isBrand.id)}
+ className='inline'
+ >
+ <Image
+ src={isBrand?.logo}
+ alt=''
+ className='object-cover object-center h-[60px]'
+ />
+ </Link>
</div>
)}
<h1 className='mb-2 font-semibold text-h-sm'>Produk</h1>
@@ -255,7 +301,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)}
&nbsp;dari&nbsp;
@@ -267,7 +314,8 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null })
&nbsp;produk{' '}
{query.q && (
<>
- untuk pencarian <span className='font-semibold'>{query.q}</span>
+ untuk pencarian{' '}
+ <span className='font-semibold'>{query.q}</span>
</>
)}
</>
@@ -279,7 +327,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 +354,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 +382,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}
@@ -351,6 +406,10 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null })
prefixUrl={prefixUrl}
defaultBrand={defaultBrand}
/>
+
+ <div className='h-6' />
+
+ <SideBanner />
</div>
<div className='w-9/12 pl-6'>
{bannerPromotionHeader && bannerPromotionHeader?.image && (
@@ -363,14 +422,20 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null })
</div>
)}
- {isBrand && isBrand.logo && (
+ {isNotReadyStockPage && 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 +456,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)}
&nbsp;dari&nbsp;
@@ -403,7 +469,8 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null })
&nbsp;produk{' '}
{query.q && (
<>
- untuk pencarian <span className='font-semibold'>{query.q}</span>
+ untuk pencarian{' '}
+ <span className='font-semibold'>{query.q}</span>
</>
)}
</>
@@ -447,7 +514,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 +533,7 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null })
href={
query?.q
? whatsappUrl('productSearch', {
- name: query.q
+ name: query.q,
})
: whatsappUrl()
}
@@ -492,44 +561,66 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null })
/>
</div>
)}
+ <FooterBanner />
</div>
</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 +634,4 @@ const FilterChoicesComponent = ({
)}
</HStack>
</div>
-)
+);
diff --git a/src/lib/promo/components/Promocrumb.jsx b/src/lib/promo/components/Promocrumb.jsx
new file mode 100644
index 00000000..4f5cf346
--- /dev/null
+++ b/src/lib/promo/components/Promocrumb.jsx
@@ -0,0 +1,40 @@
+import { Breadcrumb as ChakraBreadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react'
+import Link from 'next/link'
+import React from 'react'
+
+/**
+ * Renders a breadcrumb component with links to navigate through different pages.
+ *
+ * @param {Object} props - The props object containing the brand name.
+ * @param {string} props.brandName - The name of the brand to display in the breadcrumb.
+ * @return {JSX.Element} The rendered breadcrumb component.
+ */
+const Breadcrumb = ({ brandName }) => {
+ return (
+ <div className='container mx-auto py-4 md:py-6'>
+ <ChakraBreadcrumb>
+ <BreadcrumbItem>
+ <BreadcrumbLink as={Link} href='/' className='!text-danger-500 whitespace-nowrap'>
+ Shop
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+
+ {/* <BreadcrumbItem>
+ <BreadcrumbLink
+ as={Link}
+ href='/shop/promo'
+ className='!text-danger-500 whitespace-nowrap'
+ >
+ Promo
+ </BreadcrumbLink>
+ </BreadcrumbItem> */}
+
+ <BreadcrumbItem isCurrentPage>
+ <BreadcrumbLink className='whitespace-nowrap'>{brandName}</BreadcrumbLink>
+ </BreadcrumbItem>
+ </ChakraBreadcrumb>
+ </div>
+ )
+}
+
+export default Breadcrumb
diff --git a/src/lib/quotation/components/Quotation.jsx b/src/lib/quotation/components/Quotation.jsx
index 8c379ead..8855c6c4 100644
--- a/src/lib/quotation/components/Quotation.jsx
+++ b/src/lib/quotation/components/Quotation.jsx
@@ -1,102 +1,295 @@
-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 CartApi from '@/lib/cart/api/CartApi'
-import { ExclamationCircleIcon } from '@heroicons/react/24/outline'
-import { useEffect, useState } from 'react'
-import _ from 'lodash'
-import { deleteItemCart, getCart, getItemCart } from '@/core/utils/cart'
-import currencyFormat from '@/core/utils/currencyFormat'
-import { toast } from 'react-hot-toast'
+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 CartApi from '@/lib/cart/api/CartApi';
+import { ExclamationCircleIcon } from '@heroicons/react/24/outline';
+import { useEffect, useRef, useState } from 'react';
+import _ from 'lodash';
+import { deleteItemCart, getCart, getItemCart } from '@/core/utils/cart';
+import currencyFormat from '@/core/utils/currencyFormat';
+import { toast } from 'react-hot-toast';
// import checkoutApi from '@/lib/checkout/api/checkoutApi'
-import { useRouter } from 'next/router'
-import VariantGroupCard from '@/lib/variant/components/VariantGroupCard'
-import MobileView from '@/core/components/views/MobileView'
-import DesktopView from '@/core/components/views/DesktopView'
-import Image from '@/core/components/elements/Image/Image'
-import { useQuery } from 'react-query'
-import CardProdcuctsList from '@/core/components/elements/Product/cartProductsList'
+import { useRouter } from 'next/router';
+import VariantGroupCard from '@/lib/variant/components/VariantGroupCard';
+import MobileView from '@/core/components/views/MobileView';
+import DesktopView from '@/core/components/views/DesktopView';
+import Image from '@/core/components/elements/Image/Image';
+import { useQuery } from 'react-query';
+import CardProdcuctsList from '@/core/components/elements/Product/cartProductsList';
+import { Skeleton } from '@chakra-ui/react';
+import {
+ PickupAddress,
+ SectionAddress,
+ SectionExpedisi,
+ SectionListService,
+ SectionValidation,
+ calculateEstimatedArrival,
+ splitDuration,
+} from '../../checkout/components/CheckoutSection';
+import addressesApi from '@/lib/address/api/addressesApi';
+import { getItemAddress } from '@/core/utils/address';
+import ExpedisiList from '../../checkout/api/ExpedisiList';
+import axios from 'axios';
-const { checkoutApi } = require('@/lib/checkout/api/checkoutApi')
-const { getProductsCheckout } = require('@/lib/checkout/api/checkoutApi')
+const { checkoutApi } = require('@/lib/checkout/api/checkoutApi');
+const { getProductsCheckout } = require('@/lib/checkout/api/checkoutApi');
const Quotation = () => {
- const router = useRouter()
- const auth = useAuth()
+ const router = useRouter();
+ const auth = useAuth();
- const { data: cartCheckout } = useQuery('cartCheckout', () => getProductsCheckout())
+ const { data: cartCheckout } = useQuery('cartCheckout', () =>
+ getProductsCheckout()
+ );
- const [products, setProducts] = useState(null)
- const [totalAmount, setTotalAmount] = useState(0)
- const [totalDiscountAmount, setTotalDiscountAmount] = useState(0)
+ const SELF_PICKUP_ID = 32;
+
+ const [products, setProducts] = useState(null);
+ const [totalAmount, setTotalAmount] = useState(0);
+ const [totalDiscountAmount, setTotalDiscountAmount] = useState(0);
+
+ //start set up address and carrier
+ const [selectedCarrierId, setselectedCarrierId] = useState(0);
+ const [listExpedisi, setExpedisi] = useState([]);
+ const [selectedExpedisi, setSelectedExpedisi] = useState(0);
+ const [checkWeigth, setCheckWeight] = useState(false);
+ const [checkoutValidation, setCheckoutValidation] = useState(false);
+ const [loadingRajaOngkir, setLoadingRajaOngkir] = useState(false);
+
+ const [listserviceExpedisi, setListServiceExpedisi] = useState([]);
+ const [selectedServiceType, setSelectedServiceType] = useState(null);
+
+ const [selectedCarrier, setselectedCarrier] = useState(0);
+ const [totalWeight, setTotalWeight] = useState(0);
+
+ const [biayaKirim, setBiayaKirim] = useState(0);
+ const [selectedExpedisiService, setselectedExpedisiService] = useState(null);
+ const [etd, setEtd] = useState(null);
+ const [etdFix, setEtdFix] = useState(null);
+
+ const [isApproval, setIsApproval] = useState(false);
+
+ const expedisiValidation = useRef(null);
+
+ const [selectedAddress, setSelectedAddress] = useState({
+ shipping: null,
+ invoicing: null,
+ });
+
+ const [addresses, setAddresses] = useState(null);
+
+ useEffect(() => {
+ if (!auth) return;
+
+ const getAddresses = async () => {
+ const dataAddresses = await addressesApi();
+ setAddresses(dataAddresses);
+ };
+
+ getAddresses();
+ setIsApproval(auth?.feature?.soApproval);
+ }, [auth]);
+
+ useEffect(() => {
+ if (!addresses) return;
+
+ const matchAddress = (key) => {
+ const addressToMatch = getItemAddress(key);
+ const foundAddress = addresses.filter(
+ (address) => address.id == addressToMatch
+ );
+ if (foundAddress.length > 0) {
+ return foundAddress[0];
+ }
+ return addresses[0];
+ };
+
+ setSelectedAddress({
+ shipping: matchAddress('shipping'),
+ invoicing: matchAddress('invoicing'),
+ });
+ }, [addresses]);
+
+ const loadExpedisi = async () => {
+ let dataExpedisi = await ExpedisiList();
+ dataExpedisi = dataExpedisi.map((expedisi) => ({
+ value: expedisi.id,
+ label: expedisi.name,
+ carrierId: expedisi.deliveryCarrierId,
+ }));
+ setExpedisi(dataExpedisi);
+ };
+
+ const loadServiceRajaOngkir = async () => {
+ 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);
+ if (dataService.data[0].costs[0]) {
+ 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');
+ } else {
+ toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.');
+ }
+ };
+
+ useEffect(() => {
+ setCheckoutValidation(false);
+
+ if (selectedCarrier != 0 && selectedCarrier != 1 && totalWeight > 0) {
+ loadServiceRajaOngkir();
+ } else {
+ setListServiceExpedisi();
+ setBiayaKirim(0);
+ setselectedExpedisiService();
+ setEtd();
+ }
+ }, [selectedCarrier, selectedAddress, totalWeight]);
+
+ useEffect(() => {
+ if (selectedExpedisi) {
+ let serviceType = selectedExpedisi.split(',');
+ if (serviceType[0] === 0) return;
+
+ setselectedCarrier(serviceType[0]);
+ setselectedCarrierId(serviceType[1]);
+ setListServiceExpedisi([]);
+ }
+ }, [selectedExpedisi]);
+
+ useEffect(() => {
+ if (selectedServiceType) {
+ let serviceType = selectedServiceType.split(',');
+ setBiayaKirim(serviceType[0]);
+ setselectedExpedisiService(serviceType[1]);
+ setEtd(serviceType[2]);
+ }
+ }, [selectedServiceType]);
+
+ useEffect(() => {
+ if (etd) setEtdFix(calculateEstimatedArrival(etd));
+ }, [etd]);
+
+ useEffect(() => {
+ if (isApproval) {
+ setselectedCarrierId(1);
+ setselectedExpedisiService('indoteknik');
+ }
+ }, [isApproval]);
+
+ // end set up address and carrier
useEffect(() => {
const loadProducts = async () => {
- const cart = getCart()
+ const cart = getCart();
const variantIds = _.filter(cart, (o) => o.selected == true)
.map((o) => o.productId)
- .join(',')
- const dataProducts = await CartApi({ variantIds })
+ .join(',');
+ const dataProducts = await CartApi({ variantIds });
const productsWithQuantity = dataProducts?.map((product) => {
return {
...product,
- quantity: getItemCart({ productId: product.id }).quantity
- }
- })
+ quantity: getItemCart({ productId: product.id }).quantity,
+ };
+ });
if (productsWithQuantity) {
Promise.all(productsWithQuantity).then((resolvedProducts) => {
- setProducts(resolvedProducts)
- })
+ setProducts(resolvedProducts);
+ });
}
- }
+ };
+ loadExpedisi();
// loadProducts()
- }, [])
+ }, []);
useEffect(() => {
- setProducts(cartCheckout?.products)
- }, [cartCheckout])
+ setProducts(cartCheckout?.products);
+ setCheckWeight(cartCheckout?.hasProductWithoutWeight);
+ setTotalWeight(cartCheckout?.totalWeight.g);
+ }, [cartCheckout]);
useEffect(() => {
if (products) {
- let calculateTotalAmount = 0
- let calculateTotalDiscountAmount = 0
+ let calculateTotalAmount = 0;
+ let calculateTotalDiscountAmount = 0;
products.forEach((product) => {
- calculateTotalAmount += product.price.price * product.quantity
+ calculateTotalAmount += product.price.price * product.quantity;
calculateTotalDiscountAmount +=
- (product.price.price - product.price.priceDiscount) * product.quantity
- })
- setTotalAmount(calculateTotalAmount)
- setTotalDiscountAmount(calculateTotalDiscountAmount)
+ (product.price.price - product.price.priceDiscount) *
+ product.quantity;
+ });
+ setTotalAmount(calculateTotalAmount);
+ setTotalDiscountAmount(calculateTotalDiscountAmount);
}
- }, [products])
+ }, [products]);
- const [isLoading, setIsLoading] = useState(false)
+ const [isLoading, setIsLoading] = useState(false);
const checkout = async () => {
- if (!products || products.length == 0) return
- setIsLoading(true)
+ // validation checkout
+ if (selectedExpedisi === 0 && !isApproval) {
+ setCheckoutValidation(true);
+ if (expedisiValidation.current) {
+ const position = expedisiValidation.current.getBoundingClientRect();
+ window.scrollTo({
+ top: position.top - 300 + window.pageYOffset,
+ behavior: 'smooth',
+ });
+ }
+ return;
+ }
+ if (selectedCarrier != 1 && biayaKirim == 0 && !isApproval) {
+ toast.error('Maaf, layanan tidak tersedia. Mohon pilih expedisi lain.');
+ return;
+ }
+
+ if (!products || products.length == 0) return;
+ 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,
+ partner_shipping_id: selectedAddress.shipping.id,
+ partner_invoice_id: selectedAddress.invoicing.id,
user_id: auth.id,
- order_line: JSON.stringify(productOrder)
- }
- const isSuccess = await checkoutApi({ data })
- setIsLoading(false)
+ order_line: JSON.stringify(productOrder),
+ delivery_amount: biayaKirim,
+ carrier_id: selectedCarrierId,
+ estimated_arrival_days: splitDuration(etd),
+ delivery_service_type: selectedExpedisiService,
+ };
+ console.log('data checkout', data);
+ const isSuccess = await checkoutApi({ data });
+ console.log('isSuccess', isSuccess);
+ setIsLoading(false);
if (isSuccess?.id) {
- for (const product of products) deleteItemCart({ productId: product.id })
- router.push(`/shop/quotation/finish?id=${isSuccess.id}`)
- return
+ for (const product of products) deleteItemCart({ productId: product.id });
+ router.push(`/shop/quotation/finish?id=${isSuccess.id}`);
+ return;
}
- toast.error('Gagal melakukan transaksi, terjadi kesalahan internal')
- }
+ toast.error('Gagal melakukan transaksi, terjadi kesalahan internal');
+ };
- const taxTotal = (totalAmount - totalDiscountAmount) * 0.11
+ const taxTotal = (totalAmount - totalDiscountAmount) * 0.11;
return (
<>
@@ -107,16 +300,85 @@ const Quotation = () => {
<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>
<Divider />
+ {selectedCarrierId == SELF_PICKUP_ID && (
+ <div className='p-4'>
+ <div
+ class='flex items-center p-4 mb-4 text-sm border border-yellow-500 text-yellow-800 rounded-lg bg-yellow-50'
+ role='alert'
+ >
+ <svg
+ class='flex-shrink-0 inline w-4 h-4 mr-3'
+ aria-hidden='true'
+ fill='currentColor'
+ viewBox='0 0 20 20'
+ >
+ <path d='M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z' />
+ </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.
+ </div>
+ </div>
+ </div>
+ )}
+
+ {selectedCarrierId == SELF_PICKUP_ID && (
+ <PickupAddress label='Alamat Pickup' />
+ )}
+ {selectedCarrierId != SELF_PICKUP_ID && (
+ <Skeleton
+ isLoaded={!!selectedAddress.invoicing && !!selectedAddress.shipping}
+ minHeight={320}
+ >
+ <SectionAddress
+ address={selectedAddress.shipping}
+ label='Alamat Pengiriman'
+ url='/my/address?select=shipping'
+ />
+ <Divider />
+ <SectionAddress
+ address={selectedAddress.invoicing}
+ label='Alamat Penagihan'
+ url='/my/address?select=invoice'
+ />
+ </Skeleton>
+ )}
+ <Divider />
+ <SectionValidation address={selectedAddress.invoicing} />
+ {!isApproval && (
+ <>
+ <SectionExpedisi
+ address={selectedAddress.shipping}
+ listExpedisi={listExpedisi}
+ setSelectedExpedisi={setSelectedExpedisi}
+ checkWeigth={checkWeigth}
+ checkoutValidation={checkoutValidation}
+ expedisiValidation={expedisiValidation}
+ loadingRajaOngkir={loadingRajaOngkir}
+ />
+ <Divider />
+ </>
+ )}
+
+ <SectionListService
+ listserviceExpedisi={listserviceExpedisi}
+ setSelectedServiceType={setSelectedServiceType}
+ />
+
<div className='p-4 flex flex-col gap-y-4'>
- {products && <VariantGroupCard openOnClick={false} variants={products} />}
+ {products && (
+ <VariantGroupCard openOnClick={false} variants={products} />
+ )}
</div>
<Divider />
@@ -124,7 +386,9 @@ const Quotation = () => {
<div className='p-4'>
<div className='flex justify-between items-center'>
<div className='font-medium'>Ringkasan Penawaran</div>
- <div className='text-gray_r-11 text-caption-1'>{products?.length} Barang</div>
+ <div className='text-gray_r-11 text-caption-1'>
+ {products?.length} Barang
+ </div>
</div>
<hr className='my-4 border-gray_r-6' />
<div className='flex flex-col gap-y-4'>
@@ -134,7 +398,9 @@ const Quotation = () => {
</div>
<div className='flex gap-x-2 justify-between'>
<div className='text-gray_r-11'>Diskon Produk</div>
- <div className='text-danger-500'>- {currencyFormat(cartCheckout?.totalDiscount)}</div>
+ <div className='text-danger-500'>
+ - {currencyFormat(cartCheckout?.totalDiscount)}
+ </div>
</div>
<div className='flex gap-x-2 justify-between'>
<div className='text-gray_r-11'>Subtotal</div>
@@ -144,17 +410,33 @@ const Quotation = () => {
<div className='text-gray_r-11'>PPN 11%</div>
<div>{currencyFormat(cartCheckout?.tax)}</div>
</div>
+ <div className='flex gap-x-2 justify-between'>
+ <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>
</div>
<hr className='my-4 border-gray_r-6' />
<div className='flex gap-x-2 justify-between mb-4'>
<div>Grand Total</div>
<div className='font-semibold text-gray_r-12'>
- {currencyFormat(cartCheckout?.grandTotal)}
+ {currencyFormat(
+ cartCheckout?.grandTotal +
+ Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000
+ )}
</div>
</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 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>{' '}
@@ -165,7 +447,11 @@ const Quotation = () => {
<Divider />
<div className='flex gap-x-3 p-4'>
- <button className='flex-1 btn-yellow' onClick={checkout} disabled={isLoading}>
+ <button
+ className='flex-1 btn-yellow'
+ onClick={checkout}
+ disabled={isLoading}
+ >
{isLoading ? 'Loading...' : 'Quotation'}
</button>
</div>
@@ -174,15 +460,65 @@ const Quotation = () => {
<DesktopView>
<div className='container mx-auto py-10 flex'>
<div className='w-3/4 border border-gray_r-6 rounded bg-white p-4'>
- <div className='font-medium'>Detail Barang</div>
- <CardProdcuctsList isLoading={isLoading} products={products} source='checkout' />
+ {selectedCarrierId == SELF_PICKUP_ID && (
+ <PickupAddress label='Alamat Pickup' />
+ )}
+ {selectedCarrierId != SELF_PICKUP_ID && (
+ <Skeleton
+ isLoaded={
+ !!selectedAddress.invoicing && !!selectedAddress.shipping
+ }
+ minHeight={290}
+ >
+ <SectionAddress
+ address={selectedAddress.shipping}
+ label='Alamat Pengiriman'
+ url='/my/address?select=shipping'
+ />
+ <Divider />
+ <SectionAddress
+ address={selectedAddress.invoicing}
+ label='Alamat Penagihan'
+ url='/my/address?select=invoice'
+ />
+ </Skeleton>
+ )}
+ <Divider />
+ <SectionValidation address={selectedAddress.invoicing} />
+ {!isApproval && (
+ <SectionExpedisi
+ address={selectedAddress.shipping}
+ listExpedisi={listExpedisi}
+ setSelectedExpedisi={setSelectedExpedisi}
+ checkWeigth={checkWeigth}
+ checkoutValidation={checkoutValidation}
+ expedisiValidation={expedisiValidation}
+ loadingRajaOngkir={loadingRajaOngkir}
+ />
+ )}
+
+ <Divider />
+ <SectionListService
+ listserviceExpedisi={listserviceExpedisi}
+ setSelectedServiceType={setSelectedServiceType}
+ />
+ {/* <div className='p-4'> */}
+ <div className='font-medium mb-6'>Detail Barang</div>
+ <CardProdcuctsList
+ isLoading={isLoading}
+ products={products}
+ source='checkout'
+ />
+ {/* </div> */}
</div>
<div className='w-1/4 pl-4'>
<div className='sticky top-48 border border-gray_r-6 bg-white rounded 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 className='text-gray_r-11 text-caption-1'>
+ {products?.length} Barang
+ </div>
</div>
<hr className='my-4 border-gray_r-6' />
@@ -205,6 +541,16 @@ const Quotation = () => {
<div className='text-gray_r-11'>PPN 11%</div>
<div>{currencyFormat(cartCheckout?.tax)}</div>
</div>
+ <div className='flex gap-x-2 justify-between'>
+ <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>
</div>
<hr className='my-4 border-gray_r-6' />
@@ -212,14 +558,18 @@ const Quotation = () => {
<div className='flex gap-x-2 justify-between mb-4'>
<div>Grand Total</div>
<div className='font-semibold text-gray_r-12'>
- {currencyFormat(cartCheckout?.grandTotal)}
+ {currencyFormat(
+ cartCheckout?.grandTotal +
+ Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000
+ )}
</div>
</div>
- <p className='text-caption-2 text-gray_r-11 mb-2'>
+ {/* <p className='text-caption-2 text-gray_r-11 mb-2'>
*) Belum termasuk biaya pengiriman
- </p>
+ </p> */}
<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>{' '}
@@ -240,7 +590,7 @@ const Quotation = () => {
</div>
</DesktopView>
</>
- )
-}
+ );
+};
-export default Quotation
+export default Quotation;
diff --git a/src/lib/transaction/api/approveApi.js b/src/lib/transaction/api/approveApi.js
new file mode 100644
index 00000000..891f0235
--- /dev/null
+++ b/src/lib/transaction/api/approveApi.js
@@ -0,0 +1,13 @@
+import odooApi from '@/core/api/odooApi'
+import { getAuth } from '@/core/utils/auth'
+
+const aprpoveApi = async ({ id }) => {
+ const auth = getAuth()
+ const dataCheckout = await odooApi(
+ 'POST',
+ `/api/v1/partner/${auth?.partnerId}/sale_order/${id}/approve`
+ )
+ return dataCheckout
+}
+
+export default aprpoveApi
diff --git a/src/lib/transaction/api/listSiteApi.js b/src/lib/transaction/api/listSiteApi.js
new file mode 100644
index 00000000..8b7740c5
--- /dev/null
+++ b/src/lib/transaction/api/listSiteApi.js
@@ -0,0 +1,10 @@
+import odooApi from '@/core/api/odooApi'
+import { getAuth } from '@/core/utils/auth'
+
+const getSite = async () => {
+ const auth = getAuth()
+ const dataSite = await odooApi('GET', `/api/v1/partner/${auth?.partnerId}/list/site`)
+ return dataSite
+}
+
+export default getSite \ No newline at end of file
diff --git a/src/lib/transaction/api/rejectApi.js b/src/lib/transaction/api/rejectApi.js
new file mode 100644
index 00000000..127c0d38
--- /dev/null
+++ b/src/lib/transaction/api/rejectApi.js
@@ -0,0 +1,13 @@
+import odooApi from '@/core/api/odooApi'
+import { getAuth } from '@/core/utils/auth'
+
+const rejectApi = async ({ id }) => {
+ const auth = getAuth()
+ const dataCheckout = await odooApi(
+ 'POST',
+ `/api/v1/partner/${auth?.partnerId}/sale_order/${id}/reject`
+ )
+ return dataCheckout
+}
+
+export default rejectApi
diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx
index 82eb1775..c6152ca9 100644
--- a/src/lib/transaction/components/Transaction.jsx
+++ b/src/lib/transaction/components/Transaction.jsx
@@ -1,83 +1,120 @@
-import Spinner from '@/core/components/elements/Spinner/Spinner'
-import useTransaction from '../hooks/useTransaction'
-import TransactionStatusBadge from './TransactionStatusBadge'
-import Divider from '@/core/components/elements/Divider/Divider'
-import { useMemo, useRef, useState } from 'react'
-import { downloadPurchaseOrder, downloadQuotation } from '../utils/transactions'
-import BottomPopup from '@/core/components/elements/Popup/BottomPopup'
-import uploadPoApi from '../api/uploadPoApi'
-import { toast } from 'react-hot-toast'
-import getFileBase64 from '@/core/utils/getFileBase64'
-import currencyFormat from '@/core/utils/currencyFormat'
-import VariantGroupCard from '@/lib/variant/components/VariantGroupCard'
-import { ChevronDownIcon, ChevronRightIcon, ChevronUpIcon } from '@heroicons/react/24/outline'
-import Link from '@/core/components/elements/Link/Link'
-import checkoutPoApi from '../api/checkoutPoApi'
-import cancelTransactionApi from '../api/cancelTransactionApi'
-import MobileView from '@/core/components/views/MobileView'
-import DesktopView from '@/core/components/views/DesktopView'
-import Menu from '@/lib/auth/components/Menu'
-import Image from '@/core/components/elements/Image/Image'
-import { createSlug } from '@/core/utils/slug'
-import toTitleCase from '@/core/utils/toTitleCase'
-import useAirwayBill from '../hooks/useAirwayBill'
-import Manifest from '@/lib/treckingAwb/component/Manifest'
+import Spinner from '@/core/components/elements/Spinner/Spinner';
+import useTransaction from '../hooks/useTransaction';
+import TransactionStatusBadge from './TransactionStatusBadge';
+import Divider from '@/core/components/elements/Divider/Divider';
+import { useEffect, useMemo, useRef, useState } from 'react';
+import ImageNext from 'next/image';
+import {
+ downloadPurchaseOrder,
+ downloadQuotation,
+} from '../utils/transactions';
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
+import uploadPoApi from '../api/uploadPoApi';
+import { toast } from 'react-hot-toast';
+import getFileBase64 from '@/core/utils/getFileBase64';
+import currencyFormat from '@/core/utils/currencyFormat';
+import VariantGroupCard from '@/lib/variant/components/VariantGroupCard';
+import {
+ ChevronDownIcon,
+ ChevronRightIcon,
+ ChevronUpIcon,
+} from '@heroicons/react/24/outline';
+import Link from '@/core/components/elements/Link/Link';
+import checkoutPoApi from '../api/checkoutPoApi';
+import cancelTransactionApi from '../api/cancelTransactionApi';
+import MobileView from '@/core/components/views/MobileView';
+import DesktopView from '@/core/components/views/DesktopView';
+import Menu from '@/lib/auth/components/Menu';
+import Image from '@/core/components/elements/Image/Image';
+import { createSlug } from '@/core/utils/slug';
+import toTitleCase from '@/core/utils/toTitleCase';
+import useAirwayBill from '../hooks/useAirwayBill';
+import Manifest from '@/lib/treckingAwb/component/Manifest';
+import useAuth from '@/core/hooks/useAuth';
+import StepApproval from './stepper';
+import aprpoveApi from '../api/approveApi';
+import rejectApi from '../api/rejectApi';
const Transaction = ({ id }) => {
- const { transaction } = useTransaction({ id })
- const { queryAirwayBill } = useAirwayBill({ orderId: id })
+ const auth = useAuth();
+ const { transaction } = useTransaction({ id });
- const [airwayBillPopup, setAirwayBillPopup] = useState(null)
+ const statusApprovalWeb = transaction.data?.approvalStep;
+
+ const { queryAirwayBill } = useAirwayBill({ orderId: id });
+ const [airwayBillPopup, setAirwayBillPopup] = useState(null);
+
+ const poNumber = useRef(null);
+ const poFile = useRef(null);
+ const [uploadPo, setUploadPo] = useState(false);
+ const [idAWB, setIdAWB] = useState(null);
+ const openUploadPo = () => setUploadPo(true);
+ const closeUploadPo = () => setUploadPo(false);
- const poNumber = useRef(null)
- const poFile = useRef(null)
- const [uploadPo, setUploadPo] = useState(false)
- const [idAWB, setIdAWB] = useState(null)
- const openUploadPo = () => setUploadPo(true)
- const closeUploadPo = () => setUploadPo(false)
const submitUploadPo = async () => {
- const file = poFile.current.files[0]
- const name = poNumber.current.value
+ const file = poFile.current.files[0];
+ const name = poNumber.current.value;
if (typeof file === 'undefined' || !name) {
- toast.error('Nomor dan Dokumen PO harus diisi')
- return
+ toast.error('Nomor dan Dokumen PO harus diisi');
+ return;
}
if (file.size > 5000000) {
- toast.error('Maksimal ukuran file adalah 5MB')
- return
+ toast.error('Maksimal ukuran file adalah 5MB');
+ return;
}
- const data = { name, file: await getFileBase64(file) }
- const isUploaded = await uploadPoApi({ id, data })
+ const data = { name, file: await getFileBase64(file) };
+ const isUploaded = await uploadPoApi({ id, data });
if (isUploaded) {
- toast.success('Berhasil upload PO')
- transaction.refetch()
- closeUploadPo()
- return
+ toast.success('Berhasil upload PO');
+ transaction.refetch();
+ closeUploadPo();
+ return;
}
- toast.error('Terjadi kesalahan internal, coba lagi nanti atau hubungi kami')
- }
+ toast.error(
+ 'Terjadi kesalahan internal, coba lagi nanti atau hubungi kami'
+ );
+ };
+
+ const [cancelTransaction, setCancelTransaction] = useState(false);
+ const openCancelTransaction = () => setCancelTransaction(true);
+ const closeCancelTransaction = () => setCancelTransaction(false);
- const [cancelTransaction, setCancelTransaction] = useState(false)
- const openCancelTransaction = () => setCancelTransaction(true)
- const closeCancelTransaction = () => setCancelTransaction(false)
+ const [rejectTransaction, setRejectTransaction] = useState(false);
+
+ const openRejectTransaction = () => setRejectTransaction(true);
+ const closeRejectTransaction = () => setRejectTransaction(false);
const submitCancelTransaction = async () => {
- const isCancelled = await cancelTransactionApi({ transaction: transaction.data })
+ const isCancelled = await cancelTransactionApi({
+ transaction: transaction.data,
+ });
if (isCancelled) {
- toast.success('Berhasil batalkan transaksi')
- transaction.refetch()
+ toast.success('Berhasil batalkan transaksi');
+ transaction.refetch();
}
- closeCancelTransaction()
- }
+ closeCancelTransaction();
+ };
const checkout = async () => {
if (!transaction.data?.purchaseOrderFile) {
- toast.error('Mohon upload dokumen PO anda sebelum melanjutkan pesanan')
- return
+ toast.error('Mohon upload dokumen PO anda sebelum melanjutkan pesanan');
+ return;
}
- await checkoutPoApi({ id })
- toast.success('Berhasil melanjutkan pesanan')
- transaction.refetch()
- }
+ await checkoutPoApi({ id });
+ toast.success('Berhasil melanjutkan pesanan');
+ transaction.refetch();
+ };
+
+ const handleApproval = async () => {
+ await aprpoveApi({ id });
+ toast.success('Berhasil melanjutkan approval');
+ transaction.refetch();
+ };
+
+ const handleReject = async () => {
+ await rejectApi({ id });
+ closeRejectTransaction();
+ transaction.refetch();
+ };
const memoizeVariantGroupCard = useMemo(
() => (
@@ -102,19 +139,19 @@ const Transaction = ({ id }) => {
</div>
),
[transaction.data]
- )
+ );
if (transaction.isLoading) {
return (
<div className='flex justify-center my-6'>
<Spinner className='w-6 text-gray_r-12/50 fill-gray_r-12' />
</div>
- )
+ );
}
const closePopup = () => {
- setIdAWB(null)
- }
+ setIdAWB(null);
+ };
return (
transaction.data?.name && (
@@ -146,6 +183,33 @@ const Transaction = ({ id }) => {
</div>
</BottomPopup>
+ <BottomPopup
+ active={rejectTransaction}
+ close={closeRejectTransaction}
+ title='Batalkan Transaksi'
+ >
+ <div className='leading-7 text-gray_r-12/80'>
+ Apakah anda yakin Membatalkan transaksi{' '}
+ <span className='underline'>{transaction.data?.name}</span>?
+ </div>
+ <div className='flex justify-end mt-6 gap-x-4'>
+ <button
+ className='btn-solid-red w-full md:w-fit'
+ type='button'
+ onClick={handleReject}
+ >
+ Ya, Batalkan
+ </button>
+ <button
+ className='btn-light w-full md:w-fit'
+ type='button'
+ onClick={closeRejectTransaction}
+ >
+ Batal
+ </button>
+ </div>
+ </BottomPopup>
+
<BottomPopup title='Upload PO' close={closeUploadPo} active={uploadPo}>
<div>
<label>Nomor PO</label>
@@ -156,10 +220,18 @@ const Transaction = ({ id }) => {
<input type='file' className='form-input mt-3 py-2' ref={poFile} />
</div>
<div className='grid grid-cols-2 gap-x-3 mt-6'>
- <button type='button' className='btn-light w-full' onClick={closeUploadPo}>
+ <button
+ type='button'
+ className='btn-light w-full'
+ onClick={closeUploadPo}
+ >
Batal
</button>
- <button type='button' className='btn-solid-red w-full' onClick={submitUploadPo}>
+ <button
+ type='button'
+ className='btn-solid-red w-full'
+ onClick={submitUploadPo}
+ >
Upload
</button>
</div>
@@ -167,18 +239,33 @@ const Transaction = ({ id }) => {
<Manifest idAWB={idAWB} closePopup={closePopup}></Manifest>
<MobileView>
+ <div className='p-4'>
+ {auth?.feature?.soApproval && (
+ <StepApproval
+ layer={statusApprovalWeb}
+ status={transaction?.data?.status}
+ className='ml-auto'
+ />
+ )}
+ </div>
<div className='flex flex-col gap-y-4 p-4'>
<DescriptionRow label='Status Transaksi'>
<div className='flex justify-end'>
<TransactionStatusBadge status={transaction.data?.status} />
</div>
</DescriptionRow>
- <DescriptionRow label='No Transaksi'>{transaction.data?.name}</DescriptionRow>
+ <DescriptionRow label='No Transaksi'>
+ {transaction.data?.name}
+ </DescriptionRow>
<DescriptionRow label='Ketentuan Pembayaran'>
{transaction.data?.paymentTerm}
</DescriptionRow>
- <DescriptionRow label='Nama Sales'>{transaction.data?.sales}</DescriptionRow>
- <DescriptionRow label='Waktu Transaksi'>{transaction.data?.dateOrder}</DescriptionRow>
+ <DescriptionRow label='Nama Sales'>
+ {transaction.data?.sales}
+ </DescriptionRow>
+ <DescriptionRow label='Waktu Transaksi'>
+ {transaction.data?.dateOrder}
+ </DescriptionRow>
</div>
<Divider />
@@ -214,25 +301,27 @@ const Transaction = ({ id }) => {
<Divider />
- <div className='p-4 flex flex-col gap-y-4'>
- <DescriptionRow label='Purchase Order'>
- {transaction.data?.purchaseOrderName || '-'}
- </DescriptionRow>
- <div className='flex items-center'>
- <p className='text-gray_r-11 leading-none'>Dokumen PO</p>
- <button
- type='button'
- className='btn-light py-1.5 px-3 ml-auto'
- onClick={
- transaction.data?.purchaseOrderFile
- ? () => downloadPurchaseOrder(transaction.data)
- : openUploadPo
- }
- >
- {transaction.data?.purchaseOrderFile ? 'Download' : 'Upload'}
- </button>
+ {!auth?.feature.soApproval && (
+ <div className='p-4 flex flex-col gap-y-4'>
+ <DescriptionRow label='Purchase Order'>
+ {transaction.data?.purchaseOrderName || '-'}
+ </DescriptionRow>
+ <div className='flex items-center'>
+ <p className='text-gray_r-11 leading-none'>Dokumen PO</p>
+ <button
+ type='button'
+ className='btn-light py-1.5 px-3 ml-auto'
+ onClick={
+ transaction.data?.purchaseOrderFile
+ ? () => downloadPurchaseOrder(transaction.data)
+ : openUploadPo
+ }
+ >
+ {transaction.data?.purchaseOrderFile ? 'Download' : 'Upload'}
+ </button>
+ </div>
</div>
- </div>
+ )}
<Divider />
@@ -278,11 +367,43 @@ const Transaction = ({ id }) => {
<Divider />
<div className='p-4 pt-0'>
- {transaction.data?.status == 'draft' && (
- <button className='btn-yellow w-full mt-4' onClick={checkout}>
- Lanjutkan Transaksi
- </button>
- )}
+ {transaction.data?.status == 'draft' &&
+ auth?.feature.soApproval && (
+ <div className='flex gap-x-2'>
+ <button
+ className='btn-yellow w-full'
+ onClick={checkout}
+ disabled={
+ transaction.data?.status === 'cancel'
+ ? true
+ : false || auth?.webRole === statusApprovalWeb
+ ? true
+ : false
+ }
+ >
+ Approve
+ </button>
+ <button
+ className='btn-solid-red px-7 w-full'
+ onClick={checkout}
+ disabled={
+ transaction.data?.status === 'cancel'
+ ? true
+ : false || auth?.webRole === statusApprovalWeb
+ ? true
+ : false
+ }
+ >
+ Reject
+ </button>
+ </div>
+ )}
+ {transaction.data?.status == 'draft' &&
+ !auth?.feature?.soApproval && (
+ <button className='btn-yellow w-full mt-4' onClick={checkout}>
+ Lanjutkan Transaksi
+ </button>
+ )}
<button
className='btn-light w-full mt-4'
disabled={transaction.data?.status != 'draft'}
@@ -308,10 +429,23 @@ const Transaction = ({ id }) => {
<Menu />
</div>
<div className='w-9/12 p-4 py-6 bg-white border border-gray_r-6 rounded'>
- <h1 className='text-title-sm font-semibold mb-6'>Detail Transaksi</h1>
+ <div className='flex justify-between'>
+ <h1 className='text-title-sm font-semibold mb-6'>
+ Detail Transaksi
+ </h1>
+ {auth?.feature?.soApproval && (
+ <StepApproval
+ layer={statusApprovalWeb}
+ status={transaction?.data?.status}
+ className='ml-auto'
+ />
+ )}
+ </div>
<div className='flex items-center gap-x-2 mb-3'>
- <span className='text-h-sm font-medium'>{transaction?.data?.name}</span>
+ <span className='text-h-sm font-medium'>
+ {transaction?.data?.name}
+ </span>
<TransactionStatusBadge status={transaction?.data?.status} />
</div>
<div className='flex gap-x-4'>
@@ -322,20 +456,58 @@ const Transaction = ({ id }) => {
>
Download
</button>
- {transaction.data?.status == 'draft' && (
- <button className='btn-yellow' onClick={checkout}>
- Lanjutkan Transaksi
- </button>
- )}
- {transaction.data?.status != 'draft' && (
- <button
- className='btn-light'
- disabled={transaction.data?.status != 'waiting'}
- onClick={openCancelTransaction}
- >
- Batalkan Transaksi
- </button>
- )}
+ {transaction.data?.status == 'draft' &&
+ auth?.feature?.soApproval &&
+ auth?.webRole && (
+ <div className='flex gap-x-2'>
+ <button
+ className='btn-yellow'
+ onClick={handleApproval}
+ disabled={
+ transaction.data?.status === 'cancel'
+ ? true
+ : false || auth?.webRole === statusApprovalWeb
+ ? true
+ : false || statusApprovalWeb < 1
+ ? true
+ : false
+ }
+ >
+ Approve
+ </button>
+ <button
+ className='btn-solid-red px-7'
+ onClick={openRejectTransaction}
+ disabled={
+ transaction.data?.status === 'cancel'
+ ? true
+ : false || auth?.webRole === statusApprovalWeb
+ ? true
+ : false || statusApprovalWeb < 1
+ ? true
+ : false
+ }
+ >
+ Reject
+ </button>
+ </div>
+ )}
+ {transaction.data?.status == 'draft' &&
+ !auth?.feature.soApproval && (
+ <button className='btn-yellow' onClick={checkout}>
+ Lanjutkan Transaksi
+ </button>
+ )}
+ {transaction.data?.status != 'draft' &&
+ !auth?.feature.soApproval && (
+ <button
+ className='btn-light'
+ disabled={transaction.data?.status != 'waiting'}
+ onClick={openCancelTransaction}
+ >
+ Batalkan Transaksi
+ </button>
+ )}
</div>
<div className='grid grid-cols-2 gap-x-6 mt-6'>
@@ -350,33 +522,50 @@ const Transaction = ({ id }) => {
<div>Ketentuan Pembayaran</div>
<div>: {transaction?.data?.paymentTerm}</div>
- <div>Purchase Order</div>
- <div>
- : {transaction?.data?.purchaseOrderName}{' '}
- <button
- type='button'
- className='inline-block text-danger-500'
- onClick={
- transaction.data?.purchaseOrderFile
- ? () => downloadPurchaseOrder(transaction.data)
- : openUploadPo
- }
- >
- {transaction?.data?.purchaseOrderFile ? 'Download' : 'Upload'}
- </button>
- </div>
+ {!auth?.feature?.soApproval ? (
+ <>
+ <div>Purchase Order</div>
+ <div>
+ : {transaction?.data?.purchaseOrderName}{' '}
+ <button
+ type='button'
+ className='inline-block text-danger-500'
+ onClick={
+ transaction.data?.purchaseOrderFile
+ ? () => downloadPurchaseOrder(transaction.data)
+ : openUploadPo
+ }
+ >
+ {transaction?.data?.purchaseOrderFile
+ ? 'Download'
+ : 'Upload'}
+ </button>
+ </div>
+ </>
+ ) : (
+ <>
+ <div>Site</div>
+ <div>: {transaction?.data?.sitePartner}</div>
+ </>
+ )}
</div>
</div>
- <div className='text-h-sm font-semibold mt-10 mb-4'>Informasi Pelanggan</div>
+ <div className='text-h-sm font-semibold mt-10 mb-4'>
+ Informasi Pelanggan
+ </div>
<div className='grid grid-cols-2 gap-x-4'>
<div className='border border-gray_r-6 rounded p-3'>
<div className='font-medium mb-4'>Detail Pelanggan</div>
- <SectionContent address={transaction?.data?.address?.customer} />
+ <SectionContent
+ address={transaction?.data?.address?.customer}
+ />
</div>
</div>
- <div className='text-h-sm font-semibold mt-10 mb-4'>Pengiriman</div>
+ <div className='text-h-sm font-semibold mt-10 mb-4'>
+ Pengiriman
+ </div>
<div className='grid grid-cols-3 gap-1'>
{transaction?.data?.pickings?.map((airway) => (
<button
@@ -403,12 +592,14 @@ const Transaction = ({ id }) => {
<div className='badge-red text-sm'>Belum ada pengiriman</div>
)}
- <div className='text-h-sm font-semibold mt-10 mb-4'>Rincian Pembelian</div>
+ <div className='text-h-sm font-semibold mt-10 mb-4'>
+ Rincian Pembelian
+ </div>
<table className='table-data'>
<thead>
<tr>
<th>Nama Produk</th>
- <th>Diskon</th>
+ {/* <th>Diskon</th> */}
<th>Jumlah</th>
<th>Harga</th>
<th>Subtotal</th>
@@ -426,11 +617,37 @@ const Transaction = ({ id }) => {
)}
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-32 w-full rounded-md'
- />
+ <div className='relative'>
+ <Image
+ src={product?.parent?.image}
+ alt={product?.name}
+ className='object-contain object-center border border-gray_r-6 h-32 w-full rounded-md'
+ />
+ <div className='absolute top-0 right-4 flex mt-3'>
+ <div className='gambarB '>
+ {product.isSni && (
+ <ImageNext
+ src='/images/sni-logo.png'
+ alt='SNI Logo'
+ className='w-2 h-4 object-contain object-top sm:h-4'
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ <div className='gambarC '>
+ {product.isTkdn && (
+ <ImageNext
+ src='/images/TKDN.png'
+ alt='TKDN'
+ className='w-5 h-4 object-contain object-top ml-1 sm:h-4'
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ </div>
+ </div>
</Link>
<div className='px-2 text-left'>
<Link
@@ -451,18 +668,18 @@ const Transaction = ({ id }) => {
</div>
</div>
</td>
- <td>
+ {/* <td>
{product.price.discountPercentage > 0
? `${product.price.discountPercentage}%`
: ''}
- </td>
+ </td> */}
<td>{product.quantity}</td>
<td>
- {product.price.discountPercentage > 0 && (
+ {/* {product.price.discountPercentage > 0 && (
<div className='line-through mb-1 text-caption-1 text-gray_r-12/70'>
{currencyFormat(product.price.price)}
</div>
- )}
+ )} */}
<div>{currencyFormat(product.price.priceDiscount)}</div>
</td>
<td>{currencyFormat(product.price.subtotal)}</td>
@@ -483,7 +700,9 @@ const Transaction = ({ id }) => {
{currencyFormat(transaction.data?.amountTax)}
</div>
- <div className='text-right whitespace-nowrap'>Biaya Pengiriman</div>
+ <div className='text-right whitespace-nowrap'>
+ Biaya Pengiriman
+ </div>
<div className='text-right font-medium'>
{currencyFormat(transaction.data?.deliveryAmount)}
</div>
@@ -578,18 +797,18 @@ const Transaction = ({ id }) => {
))} */}
</>
)
- )
-}
+ );
+};
const SectionAddress = ({ address }) => {
const [section, setSection] = useState({
customer: false,
invoice: false,
- shipping: false
- })
+ shipping: false,
+ });
const toggleSection = (name) => {
- setSection({ ...section, [name]: !section[name] })
- }
+ setSection({ ...section, [name]: !section[name] });
+ };
return (
<>
@@ -620,39 +839,50 @@ const SectionAddress = ({ address }) => {
/>
{section.invoice && <SectionContent address={address?.invoice} />} */}
</>
- )
-}
+ );
+};
const SectionButton = ({ label, active, toggle }) => (
- <button className='p-4 font-medium flex justify-between w-full' onClick={toggle}>
+ <button
+ className='p-4 font-medium flex justify-between w-full'
+ onClick={toggle}
+ >
<span>{label}</span>
- {active ? <ChevronUpIcon className='w-5' /> : <ChevronDownIcon className='w-5' />}
+ {active ? (
+ <ChevronUpIcon className='w-5' />
+ ) : (
+ <ChevronDownIcon className='w-5' />
+ )}
</button>
-)
+);
const SectionContent = ({ address }) => {
- let fullAddress = []
- if (address?.street) fullAddress.push(address.street)
- if (address?.subDistrict?.name) fullAddress.push(toTitleCase(address.subDistrict.name))
- if (address?.district?.name) fullAddress.push(toTitleCase(address.district.name))
- if (address?.city?.name) fullAddress.push(toTitleCase(address.city.name))
- fullAddress = fullAddress.join(', ')
+ let fullAddress = [];
+ if (address?.street) fullAddress.push(address.street);
+ if (address?.subDistrict?.name)
+ fullAddress.push(toTitleCase(address.subDistrict.name));
+ if (address?.district?.name)
+ fullAddress.push(toTitleCase(address.district.name));
+ if (address?.city?.name) fullAddress.push(toTitleCase(address.city.name));
+ fullAddress = fullAddress.join(', ');
return (
<div className='flex flex-col gap-y-4 p-4 md:p-0 border-t border-gray_r-6 md:border-0'>
<DescriptionRow label='Nama'>{address.name}</DescriptionRow>
<DescriptionRow label='Email'>{address.email || '-'}</DescriptionRow>
- <DescriptionRow label='No Telepon'>{address.mobile || '-'}</DescriptionRow>
+ <DescriptionRow label='No Telepon'>
+ {address.mobile || '-'}
+ </DescriptionRow>
<DescriptionRow label='Alamat'>{fullAddress}</DescriptionRow>
</div>
- )
-}
+ );
+};
const DescriptionRow = ({ children, label }) => (
<div className='grid grid-cols-2'>
<span className='text-gray_r-11'>{label}</span>
<span className='text-right leading-6'>{children}</span>
</div>
-)
+);
-export default Transaction
+export default Transaction;
diff --git a/src/lib/transaction/components/Transactions.jsx b/src/lib/transaction/components/Transactions.jsx
index be63effd..92bdd276 100644
--- a/src/lib/transaction/components/Transactions.jsx
+++ b/src/lib/transaction/components/Transactions.jsx
@@ -1,63 +1,163 @@
-import { useRouter } from 'next/router'
-import { useState } from 'react'
-import { toast } from 'react-hot-toast'
-import { EllipsisVerticalIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline'
+import { useRouter } from 'next/router';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-hot-toast';
+import {
+ EllipsisVerticalIcon,
+ MagnifyingGlassIcon,
+} from '@heroicons/react/24/outline';
+import useAuth from '@/core/hooks/useAuth';
-import { downloadPurchaseOrder, downloadQuotation } from '../utils/transactions'
-import useTransactions from '../hooks/useTransactions'
-import currencyFormat from '@/core/utils/currencyFormat'
-import cancelTransactionApi from '../api/cancelTransactionApi'
-import TransactionStatusBadge from './TransactionStatusBadge'
-import Spinner from '@/core/components/elements/Spinner/Spinner'
-import Link from '@/core/components/elements/Link/Link'
-import BottomPopup from '@/core/components/elements/Popup/BottomPopup'
-import Pagination from '@/core/components/elements/Pagination/Pagination'
-import { toQuery } from 'lodash-contrib'
-import _ from 'lodash'
-import Alert from '@/core/components/elements/Alert/Alert'
-import MobileView from '@/core/components/views/MobileView'
-import DesktopView from '@/core/components/views/DesktopView'
-import Menu from '@/lib/auth/components/Menu'
+import {
+ downloadPurchaseOrder,
+ downloadQuotation,
+} from '../utils/transactions';
+import useTransactions from '../hooks/useTransactions';
+import currencyFormat from '@/core/utils/currencyFormat';
+import cancelTransactionApi from '../api/cancelTransactionApi';
+import TransactionStatusBadge from './TransactionStatusBadge';
+import Spinner from '@/core/components/elements/Spinner/Spinner';
+import Link from '@/core/components/elements/Link/Link';
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
+import Pagination from '@/core/components/elements/Pagination/Pagination';
+import { toQuery } from 'lodash-contrib';
+import _ from 'lodash';
+import Alert from '@/core/components/elements/Alert/Alert';
+import MobileView from '@/core/components/views/MobileView';
+import DesktopView from '@/core/components/views/DesktopView';
+import Menu from '@/lib/auth/components/Menu';
+import * as XLSX from 'xlsx';
+import getSite from '../api/listSiteApi';
+import transactionsApi from '../api/transactionsApi';
const Transactions = ({ context = '' }) => {
- const router = useRouter()
- const { q = '', page = 1 } = router.query
+ const auth = useAuth();
+ const router = useRouter();
+ const { q = '', page = 1, site = null } = router.query;
- const limit = 15
+ const limit = 15;
+
+ const [inputQuery, setInputQuery] = useState(q);
+ const [toOthers, setToOthers] = useState(null);
+ const [toCancel, setToCancel] = useState(null);
+ const [listSites, setListSites] = useState([]);
+
+ const [siteFilter, setSiteFilter] = useState(site);
const query = {
name: q,
offset: (page - 1) * limit,
context,
- limit
- }
- const { transactions } = useTransactions({ query })
+ limit,
+ site:
+ siteFilter || (auth?.webRole === null && auth?.site ? auth.site : null),
+ };
+
+ const { transactions } = useTransactions({ query });
- const [inputQuery, setInputQuery] = useState(q)
- const [toOthers, setToOthers] = useState(null)
- const [toCancel, setToCancel] = useState(null)
+ const fetchSite = async () => {
+ const site = await getSite();
+ setListSites(site.sites);
+ };
const submitCancelTransaction = async () => {
const isCancelled = await cancelTransactionApi({
- transaction: toCancel
- })
+ transaction: toCancel,
+ });
if (isCancelled) {
- toast.success('Berhasil batalkan transaksi')
- transactions.refetch()
+ toast.success('Berhasil batalkan transaksi');
+ transactions.refetch();
}
- setToCancel(null)
- }
+ setToCancel(null);
+ };
- const pageCount = Math.ceil(transactions?.data?.saleOrderTotal / limit)
- let pageQuery = _.omit(query, ['limit', 'offset', 'context'])
- pageQuery = _.pickBy(pageQuery, _.identity)
- pageQuery = toQuery(pageQuery)
+ const pageCount = Math.ceil(transactions?.data?.saleOrderTotal / limit);
+ let pageQuery = _.omit(query, ['limit', 'offset', 'context']);
+ pageQuery = _.pickBy(
+ pageQuery,
+ (value, key) => value !== '' && !(key === 'page' && value === '1')
+ );
+ pageQuery = toQuery(pageQuery);
const handleSubmit = (e) => {
- e.preventDefault()
- router.push(`${router.pathname}?q=${inputQuery}`)
- }
+ e.preventDefault();
+ const queryParams = {};
+ if (inputQuery) queryParams.q = inputQuery;
+ if (siteFilter) queryParams.site = siteFilter;
+ router.push({
+ pathname: router.pathname,
+ query: queryParams,
+ });
+ };
+
+ const handleSiteFilterChange = (e) => {
+ setSiteFilter(e.target.value);
+ const queryParams = {};
+ if (inputQuery) queryParams.q = inputQuery;
+ if (e.target.value) queryParams.site = e.target.value;
+ router.push({
+ pathname: router.pathname,
+ query: queryParams,
+ });
+ };
+
+ const exportToExcel = (data, siteFilter) => {
+ const fieldsToExport = [
+ 'No. Transaksi',
+ 'No. PO',
+ 'Tanggal',
+ 'Created By',
+ 'Salesperson',
+ 'Total',
+ 'Status',
+ ];
+ const rowsToExport = [];
+
+ data.forEach((saleOrder) => {
+ const row = {
+ 'No. Transaksi': saleOrder.name,
+ 'No. PO': saleOrder.purchaseOrderName || '-',
+ Tanggal: saleOrder.dateOrder || '-',
+ 'Created By': saleOrder.address.customer?.name || '-',
+ Salesperson: saleOrder.sales,
+ Total: currencyFormat(saleOrder.amountTotal),
+ Status: saleOrder.status,
+ };
+ if (siteFilter) {
+ row['Site'] = siteFilter;
+ }
+ rowsToExport.push(row);
+ });
+ const worksheet = XLSX.utils.json_to_sheet(rowsToExport, {
+ header: fieldsToExport,
+ });
+
+ const workbook = XLSX.utils.book_new();
+ XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
+ XLSX.writeFile(workbook, 'transactions.xlsx');
+ };
+
+ const getAllData = async () => {
+ const query = {
+ name: q,
+ context,
+ site:
+ siteFilter || (auth?.webRole === null && auth?.site ? auth.site : null),
+ };
+ const queryString = toQuery(query)
+ const data = await transactionsApi({ query: queryString });
+ return data;
+ };
+
+ const handleExportExcel = async () => {
+ const dataToExport = await getAllData();
+
+ exportToExcel(dataToExport?.saleOrders, siteFilter);
+ };
+
+ useEffect(() => {
+ fetchSite();
+ }, []);
return (
<>
<MobileView>
@@ -81,17 +181,23 @@ const Transactions = ({ context = '' }) => {
</div>
)}
- {!transactions.isLoading && transactions.data?.saleOrders?.length === 0 && (
- <Alert type='info' className='text-center'>
- Tidak ada transaksi
- </Alert>
- )}
+ {!transactions.isLoading &&
+ transactions.data?.saleOrders?.length === 0 && (
+ <Alert type='info' className='text-center'>
+ Tidak ada transaksi
+ </Alert>
+ )}
{transactions.data?.saleOrders?.map((saleOrder, index) => (
- <div className='p-4 shadow border border-gray_r-3 rounded-md' key={index}>
+ <div
+ className='p-4 shadow border border-gray_r-3 rounded-md'
+ key={index}
+ >
<div className='grid grid-cols-2'>
<Link href={`${router.pathname}/${saleOrder.id}`}>
- <span className='text-caption-2 text-gray_r-11'>No. Transaksi</span>
+ <span className='text-caption-2 text-gray_r-11'>
+ No. Transaksi
+ </span>
<h2 className='text-danger-500 mt-1'>{saleOrder.name}</h2>
</Link>
<div className='flex gap-x-1 justify-end'>
@@ -105,13 +211,17 @@ const Transactions = ({ context = '' }) => {
<Link href={`${router.pathname}/${saleOrder.id}`}>
<div className='grid grid-cols-2 mt-3'>
<div>
- <span className='text-caption-2 text-gray_r-11'>No. Purchase Order</span>
+ <span className='text-caption-2 text-gray_r-11'>
+ No. Purchase Order
+ </span>
<p className='mt-1 font-medium text-gray_r-12'>
{saleOrder.purchaseOrderName || '-'}
</p>
</div>
<div className='text-right'>
- <span className='text-caption-2 text-gray_r-11'>Total Invoice</span>
+ <span className='text-caption-2 text-gray_r-11'>
+ Total Invoice
+ </span>
<p className='mt-1 font-medium text-gray_r-12'>
{saleOrder.invoiceCount} Invoice
</p>
@@ -120,10 +230,14 @@ const Transactions = ({ context = '' }) => {
<div className='grid grid-cols-2 mt-3'>
<div>
<span className='text-caption-2 text-gray_r-11'>Sales</span>
- <p className='mt-1 font-medium text-gray_r-12'>{saleOrder.sales}</p>
+ <p className='mt-1 font-medium text-gray_r-12'>
+ {saleOrder.sales}
+ </p>
</div>
<div className='text-right'>
- <span className='text-caption-2 text-gray_r-11'>Total Harga</span>
+ <span className='text-caption-2 text-gray_r-11'>
+ Total Harga
+ </span>
<p className='mt-1 font-medium text-gray_r-12'>
{currencyFormat(saleOrder.amountTotal)}
</p>
@@ -140,14 +254,18 @@ const Transactions = ({ context = '' }) => {
className='mt-2 mb-2'
/>
- <BottomPopup title='Lainnya' active={toOthers} close={() => setToOthers(null)}>
+ <BottomPopup
+ title='Lainnya'
+ active={toOthers}
+ close={() => setToOthers(null)}
+ >
<div className='flex flex-col gap-y-4 mt-2'>
<button
className='text-left disabled:opacity-60'
disabled={!toOthers?.purchaseOrderFile}
onClick={() => {
- downloadPurchaseOrder(toOthers)
- setToOthers(null)
+ downloadPurchaseOrder(toOthers);
+ setToOthers(null);
}}
>
Download PO
@@ -156,8 +274,8 @@ const Transactions = ({ context = '' }) => {
className='text-left disabled:opacity-60'
disabled={toOthers?.status != 'draft'}
onClick={() => {
- downloadQuotation(toOthers)
- setToOthers(null)
+ downloadQuotation(toOthers);
+ setToOthers(null);
}}
>
Download Quotation
@@ -166,8 +284,8 @@ const Transactions = ({ context = '' }) => {
className='text-left disabled:opacity-60'
disabled={toOthers?.status != 'waiting'}
onClick={() => {
- setToCancel(toOthers)
- setToOthers(null)
+ setToCancel(toOthers);
+ setToOthers(null);
}}
>
Batalkan Transaksi
@@ -175,7 +293,11 @@ const Transactions = ({ context = '' }) => {
</div>
</BottomPopup>
- <BottomPopup active={toCancel} close={() => setToCancel(null)} title='Batalkan Transaksi'>
+ <BottomPopup
+ active={toCancel}
+ close={() => setToCancel(null)}
+ title='Batalkan Transaksi'
+ >
<div className='leading-7 text-gray_r-12/80'>
Apakah anda yakin membatalkan transaksi{' '}
<span className='underline'>{toCancel?.name}</span>?
@@ -188,7 +310,11 @@ const Transactions = ({ context = '' }) => {
>
Ya, Batalkan
</button>
- <button className='btn-light flex-1' type='button' onClick={() => setToCancel(null)}>
+ <button
+ className='btn-light flex-1'
+ type='button'
+ onClick={() => setToCancel(null)}
+ >
Batal
</button>
</div>
@@ -205,21 +331,50 @@ const Transactions = ({ context = '' }) => {
<div className='flex mb-6 items-center justify-between'>
<h1 className='text-title-sm font-semibold'>
Daftar Transaksi{' '}
- {transactions?.data?.saleOrders ? `(${transactions?.data?.saleOrders.length})` : ''}
+ {transactions?.data?.saleOrders
+ ? `(${transactions?.data?.saleOrders.length})`
+ : ''}
</h1>
- <form className='flex gap-x-2' onSubmit={handleSubmit}>
- <input
- type='text'
- className='form-input'
- placeholder='Cari Transaksi...'
- value={inputQuery}
- onChange={(e) => setInputQuery(e.target.value)}
- />
- <button className='btn-light bg-transparent px-3' type='submit'>
- <MagnifyingGlassIcon className='w-6' />
- </button>
- </form>
+ <div className='grid grid-cols-2 gap-2'>
+ {listSites?.length > 0 ? (
+ <select
+ value={siteFilter}
+ onChange={handleSiteFilterChange}
+ className='form-input'
+ >
+ <option value=''>Pilih Site</option>
+ {listSites.map((site) => (
+ <option value={site} key={site}>
+ {site}
+ </option>
+ ))}
+ </select>
+ ) : (<div></div>)}
+
+ <form className='flex gap-x-1' onSubmit={handleSubmit}>
+ <input
+ type='text'
+ className='form-input'
+ placeholder='Cari Transaksi...'
+ value={inputQuery}
+ onChange={(e) => setInputQuery(e.target.value)}
+ />
+ <button
+ className='btn-light bg-transparent px-3'
+ type='submit'
+ >
+ <MagnifyingGlassIcon className='w-6' />
+ </button>
+ </form>
+ </div>
</div>
+ <button
+ onClick={handleExportExcel}
+ type='button'
+ className='btn-solid-red px-3 py-2 mr-auto mb-2'
+ >
+ <span>Download</span>
+ </button>
<table className='table-data'>
<thead>
<tr>
@@ -227,6 +382,9 @@ const Transactions = ({ context = '' }) => {
<th>No. PO</th>
<th>Tanggal</th>
<th>Created By</th>
+ {auth?.feature?.soApproval && (
+ <th>Site</th>
+ )}
<th className='!text-left'>Salesperson</th>
<th className='!text-left'>Total</th>
<th>Status</th>
@@ -252,13 +410,23 @@ const Transactions = ({ context = '' }) => {
{transactions.data?.saleOrders?.map((saleOrder) => (
<tr key={saleOrder.id}>
<td>
- <Link className='whitespace-nowrap' href={`${router.pathname}/${saleOrder.id}`}>{saleOrder.name}</Link>
+ <Link
+ className='whitespace-nowrap'
+ href={`${router.pathname}/${saleOrder.id}`}
+ >
+ {saleOrder.name}
+ </Link>
</td>
<td>{saleOrder.purchaseOrderName || '-'}</td>
<td>{saleOrder.dateOrder || '-'}</td>
<td>{saleOrder.address.customer?.name || '-'}</td>
+ {auth?.feature?.soApproval && (
+ <td>{saleOrder.sitePartner || '-'}</td>
+ )}
<td className='!text-left'>{saleOrder.sales}</td>
- <td className='!text-left'>{currencyFormat(saleOrder.amountTotal)}</td>
+ <td className='!text-left'>
+ {currencyFormat(saleOrder.amountTotal)}
+ </td>
<td>
<div className='flex justify-center'>
<TransactionStatusBadge status={saleOrder.status} />
@@ -272,14 +440,14 @@ const Transactions = ({ context = '' }) => {
<Pagination
pageCount={pageCount}
currentPage={parseInt(page)}
- url={router.pathname + pageQuery}
+ url={router.pathname + (pageQuery ? `?${pageQuery}` : '')}
className='mt-2 mb-2'
/>
</div>
</div>
</DesktopView>
</>
- )
-}
+ );
+};
-export default Transactions
+export default Transactions;
diff --git a/src/lib/transaction/components/stepper.jsx b/src/lib/transaction/components/stepper.jsx
new file mode 100644
index 00000000..9b0da0d9
--- /dev/null
+++ b/src/lib/transaction/components/stepper.jsx
@@ -0,0 +1,83 @@
+import {
+ Box,
+ Step,
+ StepDescription,
+ StepIcon,
+ StepIndicator,
+ StepNumber,
+ StepSeparator,
+ StepStatus,
+ StepTitle,
+ Stepper,
+ useSteps,
+} from '@chakra-ui/react';
+import Image from 'next/image';
+
+const StepApproval = ({ layer, status }) => {
+ const steps = [
+ { title: 'Indoteknik', layer_approval: 1 },
+ { title: 'Manager', layer_approval: 2 },
+ { title: 'Director', layer_approval: 3 },
+ ];
+ const { activeStep } = useSteps({
+ index: layer,
+ count: steps.length,
+ });
+ return (
+ <Stepper size='md' index={layer} colorScheme='green'>
+ {steps.map((step, index) => (
+ <Step key={index}>
+ <StepIndicator>
+ {layer === step.layer_approval && status === 'cancel' ? (
+ <StepStatus
+ complete={
+ <Image
+ src='/images/remove.png'
+ width={20}
+ height={20}
+ alt=''
+ className='w-full'
+ />
+ }
+ incomplete={<StepNumber />}
+ active={<StepNumber />}
+ />
+ ) : (
+ <StepStatus
+ complete={<StepIcon />}
+ incomplete={<StepNumber />}
+ active={<StepNumber />}
+ />
+ )}
+ </StepIndicator>
+
+ <Box flexShrink='0'>
+ <StepTitle className='md:text-xs'>{step.title}</StepTitle>
+ {status === 'cancel' ? (
+ layer > step.layer_approval ? (
+ <StepDescription className='md:text-[8px]'>
+ Approved
+ </StepDescription>
+ ) : (
+ <StepDescription className='md:text-[8px]'>
+ Rejected
+ </StepDescription>
+ )
+ ) : layer >= step.layer_approval ? (
+ <StepDescription className='md:text-[8px]'>
+ Approved
+ </StepDescription>
+ ) : (
+ <StepDescription className='md:text-[8px]'>
+ Pending
+ </StepDescription>
+ )}
+ </Box>
+ <StepSeparator _horizontal={{ ml: '0' }} />
+ </Step>
+ ))}
+ </Stepper>
+ );
+};
+
+export default StepApproval;
diff --git a/src/lib/variant/components/VariantCard.jsx b/src/lib/variant/components/VariantCard.jsx
index 9f1b5733..9f65fc3c 100644
--- a/src/lib/variant/components/VariantCard.jsx
+++ b/src/lib/variant/components/VariantCard.jsx
@@ -7,9 +7,14 @@ import { createSlug } from '@/core/utils/slug'
import currencyFormat from '@/core/utils/currencyFormat'
import { updateItemCart } from '@/core/utils/cart'
import whatsappUrl from '@/core/utils/whatsappUrl'
+import ImageNext from 'next/image';
+import { useMemo, useEffect, useState } from 'react';
const VariantCard = ({ product, openOnClick = true, buyMore = false }) => {
const router = useRouter()
+
+
+
const addItemToCart = () => {
toast.success('Berhasil menambahkan ke keranjang', { duration: 1500 })
@@ -27,11 +32,39 @@ const VariantCard = ({ product, openOnClick = true, buyMore = false }) => {
const Card = () => (
<div className='flex gap-x-3'>
<div className='w-4/12 flex items-center gap-x-2'>
- <Image
+
+ <div className="relative">
+ <Image
src={product.parent.image}
alt={product.parent.name}
className='object-contain object-center border border-gray_r-6 h-32 w-full rounded-md'
/>
+ <div className="absolute top-0 right-4 flex mt-3">
+ <div className="gambarB ">
+ {product.isSni && (
+ <ImageNext
+ src="/images/sni-logo.png"
+ alt="SNI Logo"
+ className="w-2 h-5 object-contain object-top sm:h-6"
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ <div className="gambarC ">
+ {product.isTkdn && (
+ <ImageNext
+ src="/images/TKDN.png"
+ alt="TKDN"
+ className="w-5 h-6 object-contain object-top ml-1 mr-1 sm:h-6"
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ </div>
+ </div>
+
</div>
<div className='w-8/12 flex flex-col'>
<p className='product-card__title wrap-line-ellipsis-2'>{product.parent.name}</p>
diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx
index 3fe1d3cf..bcb41dd6 100644
--- a/src/pages/_app.jsx
+++ b/src/pages/_app.jsx
@@ -1,69 +1,99 @@
-import '@/fonts/Inter/inter.css'
-import '@/styles/globals.css'
-import 'react-loading-skeleton/dist/skeleton.css'
+import '@/fonts/Inter/inter.css';
+import '@/styles/globals.css';
+import '@/styles/normalize.css';
+// import 'react-loading-skeleton/dist/skeleton.css';
-import NextProgress from 'next-progress'
-import { useRouter, Router } from 'next/router'
-import { AnimatePresence, motion } from 'framer-motion'
-import { Toaster } from 'react-hot-toast'
-import { QueryClient, QueryClientProvider } from 'react-query'
-import useDevice from '@/core/hooks/useDevice'
-import { useEffect, useState } from 'react'
-import LogoSpinner from '@/core/components/elements/Spinner/LogoSpinner'
-import { SessionProvider } from 'next-auth/react'
-import { ProductProvider } from '@/contexts/ProductContext'
-import { ProductCartProvider } from '@/contexts/ProductCartContext'
-import { ChakraProvider } from '@chakra-ui/react'
-import theme from '../../chakra.theme'
+import { useEffect, useState } from 'react';
+import dynamic from 'next/dynamic';
+import { useRouter, Router } from 'next/router';
+import { AnimatePresence, motion } from 'framer-motion';
+import { QueryClient, QueryClientProvider } from 'react-query';
-const queryClient = new QueryClient()
+import useDevice from '@/core/hooks/useDevice';
+import theme from '../../chakra.theme';
+
+const NextProgress = dynamic(() => import('next-progress'), { ssr: false });
+const ChakraProvider = dynamic(
+ () => import('@chakra-ui/react').then((mod) => mod.ChakraProvider),
+ { ssr: false }
+);
+const ProductProvider = dynamic(
+ () => import('@/contexts/ProductContext').then((mod) => mod.ProductProvider),
+ { ssr: false }
+);
+const ProductCartProvider = dynamic(
+ () =>
+ import('@/contexts/ProductCartContext').then(
+ (mod) => mod.ProductCartProvider
+ ),
+ { ssr: false }
+);
+const SessionProvider = dynamic(
+ () => import('next-auth/react').then((mod) => mod.SessionProvider),
+ { ssr: false }
+);
+const LogoSpinner = dynamic(
+ () => import('@/core/components/elements/Spinner/LogoSpinner'),
+ { ssr: false }
+);
+const ScrollToTop = dynamic(() => import('@/core/components/ScrollToTop'), {
+ ssr: false,
+});
+const Toaster = dynamic(
+ () => import('react-hot-toast').then((mod) => mod.Toaster),
+ { ssr: false }
+);
+
+const queryClient = new QueryClient();
function MyApp({ Component, pageProps: { session, ...pageProps } }) {
- const router = useRouter()
- const { isMobile } = useDevice()
+ const router = useRouter();
+ const { isMobile } = useDevice();
- const [animateLoader, setAnimateLoader] = useState(false)
+ const [animateLoader, setAnimateLoader] = useState(false);
useEffect(() => {
- const handleRouteChangeStart = () => setAnimateLoader(true)
- const handleRouteChangeComplete = () => setAnimateLoader(false)
+ const handleRouteChangeStart = () => setAnimateLoader(true);
+ const handleRouteChangeComplete = () => setAnimateLoader(false);
- Router.events.on('routeChangeStart', handleRouteChangeStart)
- Router.events.on('routeChangeComplete', handleRouteChangeComplete)
- Router.events.on('routeChangeError', handleRouteChangeComplete)
+ Router.events.on('routeChangeStart', handleRouteChangeStart);
+ Router.events.on('routeChangeComplete', handleRouteChangeComplete);
+ Router.events.on('routeChangeError', handleRouteChangeComplete);
return () => {
- Router.events.off('routeChangeStart', handleRouteChangeStart)
- Router.events.off('routeChangeComplete', handleRouteChangeComplete)
- Router.events.off('routeChangeError', handleRouteChangeComplete)
- }
- }, [])
+ Router.events.off('routeChangeStart', handleRouteChangeStart);
+ Router.events.off('routeChangeComplete', handleRouteChangeComplete);
+ Router.events.off('routeChangeError', handleRouteChangeComplete);
+ };
+ }, []);
- const [toasterStyle, setToasterStyle] = useState({})
+ const [toasterStyle, setToasterStyle] = useState({});
useEffect(() => {
- let elems = document.querySelectorAll('nav')
- let totalNavHeight = 0
+ let elems = document.querySelectorAll('nav');
+ let totalNavHeight = 0;
elems.forEach(function (elem) {
- totalNavHeight += elem.offsetHeight
- })
+ totalNavHeight += elem.offsetHeight;
+ });
setToasterStyle({
- marginTop: isMobile ? totalNavHeight - 8 : totalNavHeight
- })
- }, [isMobile])
+ marginTop: isMobile ? totalNavHeight - 8 : totalNavHeight,
+ });
+ }, [isMobile]);
return (
<SessionProvider session={session}>
+ <ScrollToTop />
+
<AnimatePresence>
{animateLoader && (
<motion.div
- initial={{ opacity: 0.4 }}
+ initial={{ opacity: 0.25 }}
animate={{ opacity: 1 }}
- exit={{ opacity: 0.4 }}
+ exit={{ opacity: 0.25 }}
transition={{
- duration: 0.1
+ duration: 0.1,
}}
className='fixed w-screen h-screen z-[500] bg-white flex justify-center items-center'
>
@@ -76,7 +106,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }) {
containerStyle={toasterStyle}
toastOptions={{
duration: 3000,
- className: 'border border-gray_r-8'
+ className: 'border border-gray_r-8',
}}
/>
<NextProgress color='#F01C21' options={{ showSpinner: false }} />
@@ -90,7 +120,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }) {
</ProductProvider>
</QueryClientProvider>
</SessionProvider>
- )
+ );
}
-export default MyApp
+export default MyApp;
diff --git a/src/pages/_document.jsx b/src/pages/_document.jsx
index 3762c63b..cd60bd89 100644
--- a/src/pages/_document.jsx
+++ b/src/pages/_document.jsx
@@ -1,16 +1,34 @@
-import { Html, Head, Main, NextScript } from 'next/document'
-import Script from 'next/script'
+import { Html, Head, Main, NextScript } from 'next/document';
+import Script from 'next/script';
export default function MyDocument() {
- const env = process.env.NODE_ENV
+ const env = process.env.NODE_ENV;
return (
<Html>
<Head>
+ <link rel='preconnect' href='https://connect.facebook.net' />
+ <link rel='dns-prefetch' href='https://connect.facebook.net' />
+
+ <link rel='preconnect' href='https://googleads.g.doubleclick.net' />
+ <link rel='dns-prefetch' href='https://googleads.g.doubleclick.net' />
+
+ <link rel='preconnect' href='https://www.googletagmanager.com' />
+ <link rel='dns-prefetch' href='https://www.googletagmanager.com' />
+
+ <link rel='preconnect' href={process.env.NEXT_PUBLIC_ODOO_API_HOST} />
+ <link rel='dns-prefetch' href={process.env.NEXT_PUBLIC_ODOO_API_HOST} />
+
+ <link rel='preconnect' href='/images/logo-indoteknik-gear.png' />
+ <link rel='dns-prefetch' href='/images/logo-indoteknik-gear.png' />
+
<link rel='icon' href='/favicon.ico' />
<link rel='manifest' href='/manifest.json' />
<link rel='apple-touch-icon' href='/icon.jpg'></link>
- <link rel='apple-touch-startup-image' href='/images/splash/launch.png' />
+ <link
+ rel='apple-touch-startup-image'
+ href='/images/splash/launch.png'
+ />
<meta name='mobile-web-app-capable' content='yes' />
<meta name='apple-mobile-web-app-capable' content='yes' />
@@ -18,9 +36,11 @@ export default function MyDocument() {
<meta name='apple-mobile-web-app-title' content='Indoteknik.com' />
<meta name='theme-color' content='#fff' />
- <link rel='prefetch' href='/images/logo-indoteknik-gear.png' />
+ <meta
+ name='facebook-domain-verification'
+ content='328wmjs7hcnz74rwsqzxvq50rmbtm2'
+ />
- <meta name='facebook-domain-verification' content='328wmjs7hcnz74rwsqzxvq50rmbtm2' />
<Script
async
strategy='beforeInteractive'
@@ -28,6 +48,7 @@ export default function MyDocument() {
/>
<Script
+ async
id='google-analytics-ua'
strategy='beforeInteractive'
dangerouslySetInnerHTML={{
@@ -36,7 +57,7 @@ export default function MyDocument() {
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-10501937-1');
- `
+ `,
}}
/>
@@ -47,6 +68,7 @@ export default function MyDocument() {
/>
<Script
+ async
id='google-analytics-ga'
strategy='beforeInteractive'
dangerouslySetInnerHTML={{
@@ -55,11 +77,12 @@ export default function MyDocument() {
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-G1W8MNZ11P');
- `
+ `,
}}
/>
<Script
+ async
id='google-tag-manager'
strategy='afterInteractive'
dangerouslySetInnerHTML={{
@@ -68,7 +91,7 @@ export default function MyDocument() {
f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-PHRB7RP');
- `
+ `,
}}
/>
@@ -79,6 +102,7 @@ export default function MyDocument() {
/>
<Script
+ async
id='google-ads'
strategy='afterInteractive'
dangerouslySetInnerHTML={{
@@ -87,7 +111,7 @@ export default function MyDocument() {
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
- gtag('config', 'AW-954540379');`
+ gtag('config', 'AW-954540379');`,
}}
/>
@@ -119,5 +143,5 @@ export default function MyDocument() {
<NextScript />
</body>
</Html>
- )
+ );
}
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/api/shop/brands.js b/src/pages/api/shop/brands.js
index dbbfcfe3..cc64a7e7 100644
--- a/src/pages/api/shop/brands.js
+++ b/src/pages/api/shop/brands.js
@@ -1,36 +1,37 @@
-import axios from 'axios'
+import axios from 'axios';
+
+const SOLR_HOST = process.env.SOLR_HOST;
export default async function handler(req, res) {
try {
- let params = '*:*'
- let sort = 'sort=if(exists(sequence_i),0,1) asc,sequence_i asc, if(exists(image_s),0,1) asc '
- let rows = 2000
+ let params = '*:*';
+ let sort =
+ 'sort=if(exists(sequence_i),0,1) asc,sequence_i asc, if(exists(image_s),0,1) asc ';
+ let rows = 2000;
if (req.query.params) {
- rows = 100
+ rows = 100;
switch (req?.query?.params) {
case 'level_s':
- params = 'level_s:prioritas'
- break
+ params = 'level_s:prioritas';
+ break;
case 'search':
- params = `name_s:${req?.query?.q.toLowerCase()}`
- sort = ''
- rows = 1
+ params = `name_s:"${req?.query?.q.toLowerCase()}"`;
+ sort = '';
+ rows = 1;
break;
default:
- params = `name_s:${req.query.params}`
+ params = `name_s:${req.query.params}`.toLowerCase();
}
}
- let brands = await axios(
- process.env.SOLR_HOST +
- `/solr/brands/select?q=${params}&q.op=OR&indent=true&rows=${rows}&${sort}`
- )
- let dataBrands = responseMap(brands.data.response.docs)
+ const url = `${SOLR_HOST}/solr/brands/select?q=${params}&q.op=OR&indent=true&rows=${rows}&${sort}`;
+ let brands = await axios(url);
+ let dataBrands = responseMap(brands.data.response.docs);
- res.status(200).json(dataBrands)
+ res.status(200).json(dataBrands);
} catch (error) {
- console.error('Error fetching data from Solr:', error)
- res.status(500).json({ error: 'Internal Server Error' })
+ console.error('Error fetching data from Solr:', error);
+ res.status(500).json({ error: 'Internal Server Error' });
}
}
@@ -40,9 +41,9 @@ const responseMap = (brands) => {
id: brand.id,
name: brand.display_name_s,
logo: brand.image_s || '',
- sequance: brand.sequence_i || ''
- }
+ sequance: brand.sequence_i || '',
+ };
- return brandMapping
- })
-}
+ return brandMapping;
+ });
+};
diff --git a/src/pages/api/shop/generate-recomendation.js b/src/pages/api/shop/generate-recomendation.js
new file mode 100644
index 00000000..dce8ae72
--- /dev/null
+++ b/src/pages/api/shop/generate-recomendation.js
@@ -0,0 +1,64 @@
+import { productMappingSolr } from '@/utils/solrMapping'
+import axios from 'axios';
+import camelcaseObjectDeep from 'camelcase-object-deep';
+
+export default async function handler(req, res) {
+ const { q = null, op = 'AND' } = req.query
+
+ if (!q) {
+ return res.status(422).json({ error: 'parameter missing' })
+ }
+
+ /*let parameter = [
+ `q=${escapeSolrQuery(q)}`,
+ `q.op=${op}`,
+ `indent=true`,
+ `fq=-publish_b:false`,
+ `qf=name_s^2 description_s`,
+ `facetch=true`,
+ `fq=price_tier1_v2_f:[1 TO *]`,
+ `rows=10`,
+ `sort=product_rating_f DESC, price_discount_f DESC`,
+ ];
+ let result = await axios(
+ process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&')
+ );*/
+ let parameter = [
+ `q=${q}`,
+ `q.op=${op}`,
+ `debugQuery=on`,
+ `defType=edismax`,
+ `df=display_name_s`,
+ `fq=-publish_b:false`,
+ `rows=5`,
+ ];
+ if(op == 'AND'){
+ parameter.push(`sort=product_rating_f DESC, price_discount_f DESC`);
+ parameter.push(`rows=1`);
+ }
+
+ let result = await axios(
+ process.env.SOLR_HOST + '/solr/recommendation/select?' + parameter.join('&')
+ );
+ try {
+ result.data = camelcaseObjectDeep(result.data)
+ res.status(200).json(result.data)
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+}
+
+const escapeSolrQuery = (query) => {
+ if (query == '*') return query;
+
+ const specialChars = /([\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g;
+ const words = query.split(/\s+/);
+ const escapedWords = words.map((word) => {
+ if (specialChars.test(word)) {
+ return `"${word.replace(specialChars, '\\$1')}"`;
+ }
+ return word;
+ });
+
+ return escapedWords.join(' ');
+};
diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js
index 576d028a..b6b8c795 100644
--- a/src/pages/api/shop/search.js
+++ b/src/pages/api/shop/search.js
@@ -1,6 +1,6 @@
-import { productMappingSolr } from '@/utils/solrMapping'
-import axios from 'axios'
-import camelcaseObjectDeep from 'camelcase-object-deep'
+import { productMappingSolr } from '@/utils/solrMapping';
+import axios from 'axios';
+import camelcaseObjectDeep from 'camelcase-object-deep';
export default async function handler(req, res) {
const {
@@ -14,35 +14,36 @@ export default async function handler(req, res) {
operation = 'AND',
fq = '',
limit = 30,
- stock = ''
- } = req.query
+ } = req.query;
- let paramOrderBy = ''
+ let { stock = '' } = req.query;
+
+ let paramOrderBy = '';
switch (orderBy) {
case 'price-asc':
- paramOrderBy += 'price_tier1_v2_f ASC'
- break
+ paramOrderBy += 'price_tier1_v2_f ASC';
+ break;
case 'price-desc':
- paramOrderBy += 'price_tier1_v2_f DESC'
- break
+ paramOrderBy += 'price_tier1_v2_f DESC';
+ break;
case 'popular':
- paramOrderBy += 'product_rating_f DESC, search_rank_i DESC,'
- break
+ paramOrderBy += 'product_rating_f DESC, search_rank_i DESC,';
+ break;
case 'popular-weekly':
- paramOrderBy += 'search_rank_weekly_i DESC'
- break
+ paramOrderBy += 'search_rank_weekly_i DESC';
+ break;
case 'stock':
- paramOrderBy += 'stock_total_f DESC'
- break
+ paramOrderBy += 'product_rating_f DESC, stock_total_f DESC';
+ break;
case 'flashsale-price-asc':
- paramOrderBy += 'flashsale_price_f ASC'
- break
+ paramOrderBy += 'flashsale_price_f ASC';
+ break;
default:
- paramOrderBy += 'product_rating_f DESC, price_discount_f DESC'
- break
+ paramOrderBy += 'product_rating_f DESC, price_discount_f DESC';
+ break;
}
- let offset = (page - 1) * limit
+ let offset = (page - 1) * limit;
let parameter = [
'facet.field=manufacture_name_s',
'facet.field=category_name',
@@ -55,59 +56,82 @@ export default async function handler(req, res) {
`start=${parseInt(offset)}`,
`rows=${limit}`,
`sort=${paramOrderBy}`,
- `fq=-publish_b:false`
- ]
+ `fq=-publish_b:false`,
+ ];
if (priceFrom > 0 || priceTo > 0) {
parameter.push(
`fq=price_tier1_v2_f:[${priceFrom == '' ? '*' : priceFrom} TO ${
priceTo == '' ? '*' : priceTo
}]`
- )
+ );
+ }
+
+ let { auth } = req.cookies;
+ if (auth) {
+ auth = JSON.parse(auth);
+ if (auth.feature.onlyReadyStock) stock = true;
}
- if (brand) parameter.push(`fq=${brand.split(',').map(manufacturer => `manufacture_name:"${manufacturer}"`).join(" OR ")}`)
- if (category) parameter.push(`fq=${category.split(',').map(cat => `category_name:"${cat}"`).join(' OR ')}`)
+ if (brand)
+ parameter.push(
+ `fq=${brand
+ .split(',')
+ .map((manufacturer) => `manufacture_name:"${encodeURIComponent(manufacturer)}"`)
+ .join(' OR ')}`
+ );
+ if (category)
+ parameter.push(
+ `fq=${category
+ .split(',')
+ .map((cat) => `category_name:"${encodeURIComponent(cat)}"`)
+ .join(' OR ')}`
+ );
// if (category) parameter.push(`fq=category_name:${capitalizeFirstLetter(category.replace(/,/g, ' OR '))}`)
- if (stock) parameter.push(`fq=stock_total_f:{1 TO *}`)
+ if (stock) parameter.push(`fq=stock_total_f:{1 TO *}`);
// Single fq in url params
- if (typeof fq === 'string') parameter.push(`fq=${fq}`)
+ if (typeof fq === 'string') parameter.push(`fq=${fq}`);
// Multi fq in url params
- if (Array.isArray(fq)) parameter = parameter.concat(fq.map((val) => `fq=${val}`))
+ if (Array.isArray(fq))
+ parameter = parameter.concat(fq.map((val) => `fq=${val}`));
- let result = await axios(process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&'))
+ let result = await axios(
+ process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&')
+ );
try {
- let { auth } = req.cookies
- if (auth) auth = JSON.parse(auth)
result.data.response.products = productMappingSolr(
result.data.response.docs,
auth?.pricelist || false
- )
- result.data.responseHeader.params.start = parseInt(result.data.responseHeader.params.start)
- result.data.responseHeader.params.rows = parseInt(result.data.responseHeader.params.rows)
- delete result.data.response.docs
- result.data = camelcaseObjectDeep(result.data)
- res.status(200).json(result.data)
+ );
+ result.data.responseHeader.params.start = parseInt(
+ result.data.responseHeader.params.start
+ );
+ result.data.responseHeader.params.rows = parseInt(
+ result.data.responseHeader.params.rows
+ );
+ delete result.data.response.docs;
+ result.data = camelcaseObjectDeep(result.data);
+ res.status(200).json(result.data);
} catch (error) {
- res.status(400).json({ error: error.message })
+ res.status(400).json({ error: error.message });
}
}
const escapeSolrQuery = (query) => {
- if (query == '*') return query
+ if (query == '*') return query;
- const specialChars = /([\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g
- const words = query.split(/\s+/)
+ const specialChars = /([\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g;
+ const words = query.split(/\s+/);
const escapedWords = words.map((word) => {
if (specialChars.test(word)) {
- return `"${word.replace(specialChars, '\\$1')}"`
+ return `"${word.replace(specialChars, '\\$1')}"`;
}
- return word
- })
+ return word;
+ });
- return escapedWords.join(' ')
-}
+ return escapedWords.join(' ');
+};
/*const productResponseMap = (products, pricelist) => {
return products.map((product) => {
diff --git a/src/pages/api/shop/variant-detail.js b/src/pages/api/shop/variant-detail.js
index fadbe000..08ce75b8 100644
--- a/src/pages/api/shop/variant-detail.js
+++ b/src/pages/api/shop/variant-detail.js
@@ -8,7 +8,10 @@ export default async function handler(req, res) {
`/solr/variants/select?q=id:${req.query.id}&q.op=OR&indent=true`
)
let auth = req.query.auth === 'false' ? JSON.parse(req.query.auth) : req.query.auth
- let result = variantsMappingSolr('',productVariants.data.response.docs, auth || false)
+ let productTemplate = await axios(
+ process.env.SOLR_HOST + `/solr/product/select?q=id:${req.query.id}&q.op=OR&indent=true`
+ )
+ let result = variantsMappingSolr(productTemplate.data.response.docs, productVariants.data.response.docs, auth || false)
res.status(200).json(result)
} catch (error) {
diff --git a/src/pages/google_merchant/products/[page].js b/src/pages/google_merchant/products/[page].js
index c8b4079b..6e0eb703 100644
--- a/src/pages/google_merchant/products/[page].js
+++ b/src/pages/google_merchant/products/[page].js
@@ -1,94 +1,102 @@
-import { createSlug } from '@/core/utils/slug'
-import toTitleCase from '@/core/utils/toTitleCase'
-import productSearchApi from '@/lib/product/api/productSearchApi'
-import variantSearchApi from '@/lib/product/api/variantSearchApi'
-import axios from 'axios'
-import _ from 'lodash-contrib'
-import { create } from 'xmlbuilder'
+import { createSlug } from '@/core/utils/slug';
+import toTitleCase from '@/core/utils/toTitleCase';
+import variantSearchApi from '@/lib/product/api/variantSearchApi';
+import axios from 'axios';
+import _ from 'lodash-contrib';
+import { create } from 'xmlbuilder';
export async function getServerSideProps({ res, query }) {
- const titleContent = 'Indoteknik.com: B2B Industrial Supply & Solution'
+ const titleContent = 'Indoteknik.com: B2B Industrial Supply & Solution';
const descriptionContent =
- 'Temukan pilihan produk B2B Industri & Alat Teknik untuk Perusahaan, UMKM & Pemerintah dengan lengkap, mudah dan transparan.'
+ 'Temukan pilihan produk B2B Industri & Alat Teknik untuk Perusahaan, UMKM & Pemerintah dengan lengkap, mudah dan transparan.';
- const { page } = query
- const limit = 5000
+ const { page } = query;
+ const limit = 5000;
const queries = {
limit,
page: page.replace('.xml', ''),
priceFrom: 1,
orderBy: 'popular',
- fq: 'image_s:["" TO *]'
- }
- const products = await variantSearchApi({ query: _.toQuery(queries) })
+ fq: 'image_s:["" TO *]',
+ };
+ const products = await variantSearchApi({ query: _.toQuery(queries) });
- const brandsData = {}
- const categoriesData = {}
+ const brandsData = {};
+ const categoriesData = {};
- const productItems = []
+ const productItems = [];
for (const product of products.response.products) {
- const productUrl = createSlug('/shop/product/variant/', product.name, product.id, true)
- const productId = product.code != '' ? product.code : product.id
- const regexHtmlTags = /(<([^>]+)>)/gi
- product.description = product.description?.replace(regexHtmlTags, ' ').trim()
+ const productUrl = createSlug(
+ '/shop/product/variant/',
+ product.name,
+ product.id,
+ true
+ );
+ const productId = product.code != '' ? product.code : product.id;
+ const regexHtmlTags = /(<([^>]+)>)/gi;
+ product.description = product.description
+ ?.replace(regexHtmlTags, ' ')
+ .trim();
const defaultProductDescription =
- 'Indoteknik.com menawarkan berbagai produk industri, konstruksi, dan teknik terpercaya. Temukan mesin industri, peralatan listrik, alat pengukur, dan banyak lagi. Pengalaman berbelanja mudah dengan deskripsi produk lengkap, spesifikasi teknis, dan gambar jelas. Pembayaran aman, pengiriman cepat ke seluruh Indonesia. Solusi lengkap untuk kebutuhan Kantor, Industri & Teknik Anda.'
+ 'Indoteknik.com menawarkan berbagai produk industri, konstruksi, dan teknik terpercaya. Temukan mesin industri, peralatan listrik, alat pengukur, dan banyak lagi. Pengalaman berbelanja mudah dengan deskripsi produk lengkap, spesifikasi teknis, dan gambar jelas. Pembayaran aman, pengiriman cepat ke seluruh Indonesia. Solusi lengkap untuk kebutuhan Kantor, Industri & Teknik Anda.';
if (!product.description) {
- product.description = defaultProductDescription
+ product.description = defaultProductDescription;
}
- let categoryName = null
+ let categoryName = null;
- let brandId = product.manufacture?.id ?? null
- let categoryId = null
+ let brandId = product.manufacture?.id ?? null;
+ let categoryId = null;
if (brandId && brandId in brandsData) {
- categoryId = brandsData[brandId].category_ids?.[0] ?? null
+ categoryId = brandsData[brandId].category_ids?.[0] ?? null;
} else {
- const solrBrand = await getBrandById(brandId)
- brandsData[brandId] = solrBrand
- categoryId = solrBrand?.category_ids?.[0] ?? null
+ const solrBrand = await getBrandById(brandId);
+ brandsData[brandId] = solrBrand;
+ categoryId = solrBrand?.category_ids?.[0] ?? null;
}
if (categoryId && categoryId in categoriesData) {
- categoryName = categoriesData[categoryId].name_s ?? null
+ categoryName = categoriesData[categoryId].name_s ?? null;
} else {
- const solrCategory = await getCategoryById(categoryId)
- categoriesData[categoryId] = solrCategory
- categoryName = solrCategory?.name_s ?? null
+ const solrCategory = await getCategoryById(categoryId);
+ categoriesData[categoryId] = solrCategory;
+ categoryName = solrCategory?.name_s ?? null;
}
- const availability = 'in_stock'
+ const availability = 'in_stock';
const item = {
'g:id': { '#text': productId },
'g:title': { '#text': toTitleCase(product.name) },
'g:description': { '#text': product.description },
'g:link': { '#text': productUrl },
- 'g:image_link': { '#text': product.image },
+ 'g:image_link': { '#text': product.image + '?variant=True' },
'g:condition': { '#text': 'new' },
'g:availability': { '#text': availability },
'g:brand': { '#text': product.manufacture?.name || '' },
- 'g:price': { '#text': `${Math.round(product.lowestPrice.price * 1.11)} IDR` }
- }
+ 'g:price': {
+ '#text': `${Math.round(product.lowestPrice.price * 1.11)} IDR`,
+ },
+ };
if (product.stockTotal == 0) {
- item['g:custom_label_0'] = { '#text': 'Stok Tidak Tersedia' }
+ item['g:custom_label_0'] = { '#text': 'Stok Tidak Tersedia' };
} else {
- item['g:custom_label_1'] = { '#text': 'Stok Tersedia' }
+ item['g:custom_label_1'] = { '#text': 'Stok Tersedia' };
}
if (categoryName) {
- item['g:custom_label_2'] = { '#text': categoryName }
+ item['g:custom_label_2'] = { '#text': categoryName };
}
if (product.lowestPrice.discountPercentage > 0) {
item['g:sale_price'] = {
- '#text': `${Math.round(product.lowestPrice.priceDiscount * 1.11)} IDR`
- }
+ '#text': `${Math.round(product.lowestPrice.priceDiscount * 1.11)} IDR`,
+ };
}
- productItems.push(item)
+ productItems.push(item);
}
const googleMerchant = {
@@ -99,28 +107,32 @@ export async function getServerSideProps({ res, query }) {
title: { '#text': `<![CDATA[${titleContent}]]>` },
link: { '#text': process.env.SELF_HOST },
description: { '#text': `<![CDATA[${descriptionContent}]]>` },
- item: productItems
- }
- }
- }
+ item: productItems,
+ },
+ },
+ };
- res.setHeader('Content-Type', 'text/xml;charset=iso-8859-1')
- res.write(create(googleMerchant).end())
- res.end()
+ res.setHeader('Content-Type', 'text/xml;charset=iso-8859-1');
+ res.write(create(googleMerchant).end());
+ res.end();
- return { props: {} }
+ return { props: {} };
}
const getBrandById = async (id) => {
- const brand = await axios(`${process.env.SOLR_HOST}/solr/brands/select?q=id:${id}`)
- return brand.data.response.docs[0] ?? null
-}
+ const brand = await axios(
+ `${process.env.SOLR_HOST}/solr/brands/select?q=id:${id}`
+ );
+ return brand.data.response.docs[0] ?? null;
+};
const getCategoryById = async (id) => {
- const category = await axios(`${process.env.SOLR_HOST}/solr/categories/select?q=id:${id}`)
- return category.data.response.docs[0] ?? null
-}
+ const category = await axios(
+ `${process.env.SOLR_HOST}/solr/categories/select?q=id:${id}`
+ );
+ return category.data.response.docs[0] ?? null;
+};
export default function GoogleMerchantPage() {
- return null
+ return null;
}
diff --git a/src/pages/index.jsx b/src/pages/index.jsx
index 65d953d2..8af963fb 100644
--- a/src/pages/index.jsx
+++ b/src/pages/index.jsx
@@ -1,15 +1,18 @@
import dynamic from 'next/dynamic';
-import MobileView from '@/core/components/views/MobileView';
-import DesktopView from '@/core/components/views/DesktopView';
import { useRef } from 'react';
-import Seo from '@/core/components/Seo';
-import DelayRender from '@/core/components/elements/DelayRender/DelayRender';
+
import { HeroBannerSkeleton } from '@/components/skeleton/BannerSkeleton';
import { PopularProductSkeleton } from '@/components/skeleton/PopularProductSkeleton';
-import PromotinProgram from '@/lib/promotinProgram/components/HomePage';
-import PreferredBrandSkeleton from '@/lib/home/components/Skeleton/PreferredBrandSkeleton';
+import Seo from '@/core/components/Seo';
+import DelayRender from '@/core/components/elements/DelayRender/DelayRender';
+import DesktopView from '@/core/components/views/DesktopView';
+import MobileView from '@/core/components/views/MobileView';
import { FlashSaleSkeleton } from '@/lib/flashSale/skeleton/FlashSaleSkeleton';
+import PreferredBrandSkeleton from '@/lib/home/components/Skeleton/PreferredBrandSkeleton';
+import PromotinProgram from '@/lib/promotinProgram/components/HomePage';
import PagePopupIformation from '~/modules/popup-information';
+import useProductDetail from '~/modules/product-detail/stores/useProductDetail';
+import { getAuth } from '~/libs/auth';
const BasicLayout = dynamic(() =>
import('@/core/components/layouts/BasicLayout')
@@ -40,6 +43,11 @@ const FlashSale = dynamic(
loading: () => <FlashSaleSkeleton />,
}
);
+
+const ProgramPromotion = dynamic(() =>
+ import('@/lib/home/components/PromotionProgram')
+);
+
const BannerSection = dynamic(() =>
import('@/lib/home/components/BannerSection')
);
@@ -55,6 +63,8 @@ export default function Home() {
const bannerRef = useRef(null);
const wrapperRef = useRef(null);
+ const auth = getAuth();
+
const handleOnLoad = () => {
wrapperRef.current.style.height =
bannerRef.current?.querySelector(':first-child')?.clientHeight + 'px';
@@ -74,8 +84,9 @@ export default function Home() {
]}
/>
+ <PagePopupIformation />
+
<DesktopView>
- <PagePopupIformation />
<div className='container mx-auto'>
<div
className='flex min-h-[400px] h-[460px]'
@@ -95,10 +106,16 @@ export default function Home() {
</div>
</div>
- <div className='my-16 flex flex-col gap-y-16'>
+ <div className='my-16 flex flex-col gap-y-8'>
<ServiceList />
- <PreferredBrand />
- <FlashSale />
+ <div id='flashsale'>
+ <PreferredBrand />
+ </div>
+ {!auth?.feature?.soApproval && (
+ <>
+ <ProgramPromotion /> <FlashSale />
+ </>
+ )}
<PromotinProgram />
<CategoryHomeId />
<BannerSection />
@@ -108,7 +125,6 @@ export default function Home() {
</DesktopView>
<MobileView>
- <PagePopupIformation />
<DelayRender renderAfter={200}>
<HeroBanner />
</DelayRender>
@@ -117,11 +133,20 @@ export default function Home() {
<ServiceList />
</DelayRender>
<DelayRender renderAfter={400}>
- <PreferredBrand />
- </DelayRender>
- <DelayRender renderAfter={600}>
- <FlashSale />
+ <div id='flashsale'>
+ <PreferredBrand />
+ </div>
</DelayRender>
+ {!auth?.feature?.soApproval && (
+ <>
+ <DelayRender renderAfter={400}>
+ <ProgramPromotion />
+ </DelayRender>
+ <DelayRender renderAfter={600}>
+ <FlashSale />
+ </DelayRender>
+ </>
+ )}
<DelayRender renderAfter={600}>
<PromotinProgram />
</DelayRender>
diff --git a/src/pages/my/recomendation/api/recomendation.js b/src/pages/my/recomendation/api/recomendation.js
new file mode 100644
index 00000000..8ff760d0
--- /dev/null
+++ b/src/pages/my/recomendation/api/recomendation.js
@@ -0,0 +1,17 @@
+import axios from 'axios';
+import { useQuery } from 'react-query';
+
+const GenerateRecomendations = ({ query }) => {
+ const queryString = _.toQuery(query);
+ const GenerateRecomendationProducts = async () =>
+ await axios(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/recomendation?${queryString}`
+ );
+ const productSearch = useQuery(
+ `generateRecomendation-${ququeryStringery}`,
+ GenerateRecomendationProducts
+ );
+
+ return productSearch;
+};
+export default GenerateRecomendations;
diff --git a/src/pages/my/recomendation/components/products-recomendatison.jsx b/src/pages/my/recomendation/components/products-recomendatison.jsx
new file mode 100644
index 00000000..d39d2a99
--- /dev/null
+++ b/src/pages/my/recomendation/components/products-recomendatison.jsx
@@ -0,0 +1,477 @@
+import Menu from '@/lib/auth/components/Menu';
+import { useEffect, useState } from 'react';
+import * as XLSX from 'xlsx';
+import GenerateRecomendations from '../api/recomendation';
+import axios from 'axios';
+import { Button, Link } from '@chakra-ui/react';
+import Image from 'next/image';
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
+import formatCurrency from '~/libs/formatCurrency';
+
+const exportToExcel = (data) => {
+ const worksheet = XLSX.utils.json_to_sheet(data);
+ const workbook = XLSX.utils.book_new();
+ XLSX.utils.book_append_sheet(workbook, worksheet, 'Results');
+
+ // Generate Excel file and trigger download in the browser
+ XLSX.writeFile(workbook, 'ProductRecommendations.xlsx');
+};
+
+const ProductsRecomendation = ({ id }) => {
+ const [excelData, setExcelData] = useState(null);
+ const [products, setProducts] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [isOpen, setIsOpen] = useState(false);
+ const [variantsOpen, setVariantsOpen] = useState([]);
+ const [otherRec, setOtherRec] = useState(false);
+
+ const mappingProducts = async ({ index, product, result, variants }) => {
+ const resultMapping = {
+ index: index,
+ product: product,
+ result: {
+ id: result?.id || '-',
+ name: result?.nameS || '-',
+ code: result?.defaultCodeS || '-',
+ },
+ };
+
+ return resultMapping;
+ };
+
+ const searchRecomendation = async ({ product, index, operator = 'AND' }) => {
+ let variants = [];
+ let resultMapping = {};
+ const searchProduct = await axios.post(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/generate-recomendation?q=${product}&op=${operator}`
+ );
+
+ if (operator === 'AND') {
+ const result =
+ searchProduct.data.response.numFound > 0
+ ? searchProduct.data.response.products[0]
+ : null;
+
+ if (result?.variantTotal > 1) {
+ const searchVariants = await axios.post(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/product-detail?id=${result.id}`
+ );
+ variants = searchVariants.data[0].variants;
+ }
+
+ resultMapping = await mappingProducts({
+ index,
+ product,
+ result,
+ variants,
+ });
+ } else {
+ const result =
+ searchProduct.data.response.numFound > 0
+ ? searchProduct.data.response.products
+ : null;
+
+ result.map((item) => {
+ if (item.variantTotal > 1) {
+ const searchVariants = axios.post(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/product-detail?id=${item.id}`
+ );
+ variants = searchVariants.data[0].variants;
+ }
+ });
+
+ console.log('ini result', searchProduct.data.response);
+ }
+
+ return resultMapping;
+ };
+
+ const handleSubmit = async (e) => {
+ setIsLoading(true);
+ e.preventDefault();
+ if (excelData) {
+ const results = await Promise.all(
+ excelData.map(async (row, i) => {
+ const index = i + 1;
+ const product = row['product'];
+ return await generateProductRecomendation({ product, index });
+ })
+ );
+
+ const formattedResults = results.map((result) => {
+ const formattedResult = { product: result.product };
+ for (let i = 0; i <= 5; i++) {
+ formattedResult[`recomendation product ${i + 1} - code`] = result.result[i] == null ? '-' : result.result[i]?.code;
+ formattedResult[`recomendation product ${i + 1} - name`] = result.result[i] == null ? '-' : result.result[i]?.name ;
+ }
+ return formattedResult;
+ });
+
+ exportToExcel(formattedResults);
+ setProducts(results);
+ setIsLoading(false);
+ } else {
+ setIsLoading(false);
+ console.log('No excel data available');
+ }
+ };
+
+ const handleFileChange = (e) => {
+ setIsLoading(true);
+ const file = e.target.files[0];
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ const data = new Uint8Array(event.target.result);
+ const workbook = XLSX.read(data, { type: 'array' });
+
+ const firstSheetName = workbook.SheetNames[0];
+ const worksheet = workbook.Sheets[firstSheetName];
+ const jsonData = XLSX.utils.sheet_to_json(worksheet);
+
+ setExcelData(jsonData);
+ console.log('ini json data', jsonData);
+
+ setIsLoading(false);
+ };
+ reader.readAsArrayBuffer(file);
+ };
+
+ const handleVariantsOpen = ({ variants }) => {
+ setVariantsOpen(variants);
+ setIsOpen(true);
+ };
+ const hadnliChooseVariants = ({ id, variant }) => {
+ let foundIndex = products.findIndex((item) => item.result.id === id);
+ if (foundIndex !== -1) {
+ products[foundIndex].result.code = variant?.code;
+ products[foundIndex].result.name = variant?.name;
+ } else {
+ console.log('Data not found.');
+ }
+ setIsOpen(false);
+ };
+
+ const handlingOtherRec = ({ product }) => {
+ console.log('ini product', product);
+ const result = async () =>
+ await searchRecomendation({ product, index: 0, operator: 'OR' });
+
+ result();
+ };
+
+ const generateProductRecomendation = async ({ product, index }) => {
+ let variants = [];
+ let resultMapping = {};
+ const searchProduct = await axios.post(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/generate-recomendation?q=${product}&op=AND`
+ );
+ const searchProductOR = await axios.post(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/generate-recomendation?q=${product}&op=OR`
+ );
+ const resultAND =
+ searchProduct.data.response.numFound > 0
+ ? searchProduct.data.response.docs[0]
+ : null; // hasil satu
+ const resultOR =
+ searchProductOR.data.response.numFound > 0
+ ? searchProductOR.data.response.docs
+ : []; // hasil 5
+
+ resultMapping = {
+ index: index,
+ product: product,
+ result: {},
+ };
+
+ // Add resultAND to resultMapping if it exists
+ resultMapping.result[0] = resultAND
+ ? {
+ id: resultAND?.id || '-',
+ name: resultAND?.nameS || '-',
+ code: resultAND?.defaultCodeS || '-',
+ }
+ : null;
+
+ // Add resultOR to resultMapping
+ if (resultOR.length > 0) {
+ resultOR.forEach((item, idx) => {
+ resultMapping.result[idx + 1] = {
+ id: item?.id || '-',
+ name: item?.nameS || '-',
+ code: item?.defaultCodeS || '-',
+ };
+ });
+ } else {
+ for (let i = 0; i <= 5; i++) {
+ resultMapping.result[i + 1] = null;
+ }
+ }
+ return resultMapping;
+ };
+ return (
+ <>
+ <BottomPopup
+ active={isOpen}
+ close={() => setIsOpen(false)}
+ className='w-full md:!w-[60%]'
+ title='List Variants'
+ >
+ <div className='container'>
+ <table className='table-data'>
+ <thead>
+ <tr>
+ <th>Part Number</th>
+ <th>Variants </th>
+ <th>Harga </th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {variantsOpen?.map((variant, index) => (
+ <tr key={index}>
+ <td>{variant.code}</td>
+ <td>{variant.attributes.join(', ') || '-'}</td>
+ <td>
+ {variant.price.discountPercentage > 0 && (
+ <div className='flex items-center gap-x-1'>
+ <div className={style['disc-badge']}>
+ {Math.floor(variant.price.discountPercentage)}%
+ </div>
+ <div className={style['disc-price']}>
+ Rp {formatCurrency(variant.price.price)}
+ </div>
+ </div>
+ )}
+ {variant.price.priceDiscount > 0 &&
+ `Rp ${formatCurrency(variant.price.priceDiscount)}`}
+ {variant.price.priceDiscount === 0 && '-'}
+ </td>
+ <td>
+ <Button
+ size='sm'
+ w='100%'
+ onClick={() =>
+ hadnliChooseVariants({
+ id: variant.parent.id,
+ variant: variant,
+ })
+ }
+ >
+ Pilih
+ </Button>
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ </BottomPopup>
+ <BottomPopup
+ active={otherRec}
+ close={() => setOtherRec(false)}
+ className='w-full md:!w-[60%]'
+ title='Other Recomendations'
+ >
+ <div className='container'>
+ <table className='table-data'>
+ <thead>
+ <tr>
+ <th>Product</th>
+ <th>Item Code </th>
+ <th>Description</th>
+ <th>Brand</th>
+ <th>Price</th>
+ <th>Image</th>
+ </tr>
+ </thead>
+ <tbody>
+ {variantsOpen?.map((variant, index) => (
+ <tr key={index}>
+ <td>{variant.code}</td>
+ <td>{variant.attributes.join(', ') || '-'}</td>
+ <td>
+ {variant.price.discountPercentage > 0 && (
+ <div className='flex items-center gap-x-1'>
+ <div className={style['disc-badge']}>
+ {Math.floor(variant.price.discountPercentage)}%
+ </div>
+ <div className={style['disc-price']}>
+ Rp {formatCurrency(variant.price.price)}
+ </div>
+ </div>
+ )}
+ {variant.price.priceDiscount > 0 &&
+ `Rp ${formatCurrency(variant.price.priceDiscount)}`}
+ {variant.price.priceDiscount === 0 && '-'}
+ </td>
+ <td>
+ <Button
+ size='sm'
+ w='100%'
+ onClick={() =>
+ hadnliChooseVariants({
+ id: variant.parent.id,
+ variant: variant,
+ })
+ }
+ >
+ Pilih
+ </Button>
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ </BottomPopup>
+ <div className='container mx-auto flex py-10'>
+ <div className='w-3/12 pr-4'>
+ <Menu />
+ </div>
+ <div className='w-9/12 p-4 bg-white border border-gray_r-6 rounded'>
+ <div className='flex mb-6 items-center justify-between'>
+ <h1 className='text-title-sm font-semibold'>
+ Generate Recomendation
+ </h1>
+ </div>
+ <div className='group'>
+ <h1 className='text-sm font-semibold'>Contoh Excel</h1>
+ <table className='table-data'>
+ <thead>
+ <tr>
+ <th>Product</th>
+ <th>Qty</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>Tekiro Long Nose Pliers Tang Lancip</td>
+ <td>10</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div className='container mx-auto mt-8'>
+ <div className='mb-4'>
+ <label htmlFor='excelFile' className='text-sm font-semibold'>
+ Upload Excel File (.xlsx)
+ </label>
+ <input
+ type='file'
+ id='excelFile'
+ accept='.xlsx'
+ onChange={handleFileChange}
+ className='mt-1 p-2 block w-full border border-gray-300 rounded-md focus:outline-none focus:border-blue-500'
+ />
+ </div>
+ <Button
+ colorScheme='red'
+ w='l'
+ isDisabled={isLoading}
+ onClick={handleSubmit}
+ >
+ Generate
+ </Button>
+ </div>
+ {/* <div className='grup mt-8'>
+ {products && products.length > 0 && (
+ <div className='group'>
+ <table className='table-data'>
+ <thead>
+ <tr>
+ <th>Product</th>
+ <th>Item Code </th>
+ <th>Description</th>
+ <th>Brand</th>
+ <th>Price</th>
+ <th>Image</th>
+ <th>lainnya</th>
+ </tr>
+ </thead>
+ <tbody>
+ {products.map((product, index) => (
+ <tr key={index}>
+ <td>{product?.product}</td>
+ <td>
+ {product?.result?.code === '-' &&
+ product.result.variantTotal > 1 && (
+ <Button
+ border='2px'
+ borderColor='yellow.300'
+ size='sm'
+ onClick={() =>
+ handleVariantsOpen({
+ variants: product?.result?.variants,
+ })
+ }
+ >
+ Lihat Variants
+ </Button>
+ )}
+ {product?.result.code !== '-' &&
+ product?.result.variantTotal > 1 ? (
+ <>
+ {product?.result.code}
+ <Button
+ variant='link'
+ colorScheme='yellow'
+ size='sm'
+ onClick={() =>
+ handleVariantsOpen({
+ variants: product?.result?.variants,
+ })
+ }
+ >
+ Variants lainya
+ </Button>
+ </>
+ ) : (
+ <>{product?.result.code}</>
+ )}
+ </td>
+ <td>{product?.result.name}</td>
+ <td>{product?.result.manufacture}</td>
+ <td>
+ {product?.result.price !== '-'
+ ? `Rp ${formatCurrency(product?.result.price)}`
+ : '-'}
+ </td>
+ <td>
+ {product?.result.image !== '-' ? (
+ <Image
+ src={product?.result.image}
+ width={100}
+ height={100}
+ alt={product?.result.name}
+ />
+ ) : (
+ '-'
+ )}
+ </td>
+ <td>
+ {' '}
+ <Button
+ border='2px'
+ borderColor='red.500'
+ size='sm'
+ onClick={() =>
+ handlingOtherRec({ product: product.product })
+ }
+ >
+ Other
+ </Button>
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ )}
+ </div> */}
+ </div>
+ </div>
+ </>
+ );
+};
+
+export default ProductsRecomendation;
diff --git a/src/pages/my/recomendation/index.jsx b/src/pages/my/recomendation/index.jsx
new file mode 100644
index 00000000..684b30c2
--- /dev/null
+++ b/src/pages/my/recomendation/index.jsx
@@ -0,0 +1,26 @@
+import DesktopView from '@/core/components/views/DesktopView';
+import MobileView from '@/core/components/views/MobileView';
+import IsAuth from '../../../lib/auth/components/IsAuth';
+import AppLayout from '@/core/components/layouts/AppLayout';
+import BasicLayout from '@/core/components/layouts/BasicLayout';
+import dynamic from 'next/dynamic';
+import Seo from '@/core/components/Seo'
+
+const ProductsRecomendation = dynamic(() => import('./components/products-recomendatison'))
+export default function MyRecomendation() {
+ return (
+ <IsAuth>
+
+ <Seo title='Dashboard Rekomendasi - Indoteknik.com' />
+
+ <MobileView>
+ <AppLayout></AppLayout>
+ </MobileView>
+ <DesktopView>
+ <BasicLayout>
+ <ProductsRecomendation />
+ </BasicLayout>
+ </DesktopView>
+ </IsAuth>
+ );
+}
diff --git a/src/pages/shop/cart.jsx b/src/pages/shop/cart.jsx
index 2da58c96..7475b23d 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('~/pages/shop/cart'));
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/category/[slug].jsx b/src/pages/shop/category/[slug].jsx
index 6d3985a8..1afe30bf 100644
--- a/src/pages/shop/category/[slug].jsx
+++ b/src/pages/shop/category/[slug].jsx
@@ -1,25 +1,31 @@
-import dynamic from 'next/dynamic'
-import { getIdFromSlug, getNameFromSlug } from '@/core/utils/slug'
-import { useRouter } from 'next/router'
-import _ from 'lodash'
-import Seo from '@/core/components/Seo'
-import Breadcrumb from '@/lib/category/components/Breadcrumb'
+import _ from 'lodash';
+import dynamic from 'next/dynamic';
+import { useRouter } from 'next/router';
-const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout'))
-const ProductSearch = dynamic(() => import('@/lib/product/components/ProductSearch'))
+import Seo from '@/core/components/Seo';
+import { getIdFromSlug, getNameFromSlug } from '@/core/utils/slug';
+import Breadcrumb from '@/lib/category/components/Breadcrumb';
+
+const BasicLayout = dynamic(() =>
+ import('@/core/components/layouts/BasicLayout')
+);
+const ProductSearch = dynamic(() =>
+ import('@/lib/product/components/ProductSearch')
+);
export default function CategoryDetail() {
- const router = useRouter()
- const { slug = '' } = router.query
+ const router = useRouter();
+ const { slug = '', page = 1 } = router.query;
- const categoryName = getNameFromSlug(slug)
- const categoryId = getIdFromSlug(slug)
- const q = router?.query.q || null
+ const categoryName = getNameFromSlug(slug);
+ const categoryId = getIdFromSlug(slug);
+ const q = router?.query.q || null;
const query = {
- fq: `category_id_i:${categoryId}`
- }
+ fq: `category_id_i:${categoryId}`,
+ page,
+ };
if (q) {
- query.q = q
+ query.q = q;
}
return (
@@ -30,8 +36,8 @@ export default function CategoryDetail() {
additionalMetaTags={[
{
property: 'keywords',
- content: `Jual ${categoryName}, harga ${categoryName}, ${categoryName} murah, toko ${categoryName}, ${categoryName} jakarta, ${categoryName} surabaya`
- }
+ content: `Jual ${categoryName}, harga ${categoryName}, ${categoryName} murah, toko ${categoryName}, ${categoryName} jakarta, ${categoryName} surabaya`,
+ },
]}
/>
@@ -41,5 +47,5 @@ export default function CategoryDetail() {
<ProductSearch query={query} prefixUrl={`/shop/category/${slug}`} />
)}
</BasicLayout>
- )
+ );
}
diff --git a/src/pages/shop/product/[slug].jsx b/src/pages/shop/product/[slug].jsx
index d8366d3c..73e8987c 100644
--- a/src/pages/shop/product/[slug].jsx
+++ b/src/pages/shop/product/[slug].jsx
@@ -1,110 +1,6 @@
-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 ProductDetailPage, {
+ getServerSideProps,
+} from '~/pages/shop/product/[slug]';
-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 || ''
-
- let response = await axios(
- `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/product-detail?id=` +
- getIdFromSlug(slug) +
- '&auth=' +
- tier
- )
- let product = response.data
- // let productSolr = await productApi({ id: getIdFromSlug(slug), headers: { Token: authToken } })
- // let productSolr = null
- if (product?.length == 1) {
- product = product[0]
- } else {
- product = null
- }
-
- return {
- props: { product }
- }
-}
-
-export default function ProductDetail({ product }) {
- const router = useRouter()
- const { setProduct } = useProductContext()
-
- useEffect(() => {
- if (product) {
- setProduct(product)
- }
- }, [product, setProduct])
-
- useEffect(() => {
- 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)
- }
-
- if (action && variantId && qty) {
- addToCart()
- }
- }, [router])
-
- if (!product) return <PageNotFound />
-
- return (
- <BasicLayout>
- <Seo
- title={product?.name || '' + ' - Indoteknik.com' || ''}
- description='Temukan pilihan produk B2B Industri &amp; Alat Teknik untuk Perusahaan, UMKM &amp; Pemerintah dengan lengkap, mudah dan transparan.'
- openGraph={{
- url: process.env.NEXT_PUBLIC_SELF_HOST + router.asPath,
- images: [
- {
- url: product?.image,
- width: 800,
- height: 800,
- alt: product?.name
- }
- ],
- type: 'product'
- }}
- additionalMetaTags={[
- {
- name: 'keywords',
- content: `${product?.name}, Harga ${product?.name}, Beli ${product?.name}, Spesifikasi ${product?.name}`
- }
- ]}
- />
- {!product && (
- <div className='container mx-auto flex justify-center pt-10'>
- <LogoSpinner width={36} height={36} />
- </div>
- )}
- {product && <Product product={product} />}
- </BasicLayout>
- )
-}
+export { getServerSideProps };
+export default ProductDetailPage;
diff --git a/src/pages/shop/product/variant/[slug].jsx b/src/pages/shop/product/variant/[slug].jsx
index 401bce82..cb335e0a 100644
--- a/src/pages/shop/product/variant/[slug].jsx
+++ b/src/pages/shop/product/variant/[slug].jsx
@@ -1,37 +1,41 @@
-import Seo from '@/core/components/Seo'
-import LogoSpinner from '@/core/components/elements/Spinner/LogoSpinner'
-import { getIdFromSlug } from '@/core/utils/slug'
-import PageNotFound from '@/pages/404'
-import dynamic from 'next/dynamic'
-import { useRouter } from 'next/router'
-import cookie from 'cookie'
-import variantApi from '@/lib/product/api/variantApi'
-import axios from 'axios'
-import { useProductContext } from '@/contexts/ProductContext'
-import { useEffect } from 'react'
+import axios from 'axios';
+import cookie from 'cookie';
+import dynamic from 'next/dynamic';
+import { useRouter } from 'next/router';
+import { useEffect } from 'react';
-const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout'))
-const Product = dynamic(() => import('@/lib/product/components/Product/Product'))
+import { useProductContext } from '@/contexts/ProductContext';
+import Seo from '@/core/components/Seo';
+import LogoSpinner from '@/core/components/elements/Spinner/LogoSpinner';
+import { getIdFromSlug } from '@/core/utils/slug';
+import PageNotFound from '@/pages/404';
+
+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;
+ const authToken = auth?.token || '';
let response = await axios(
`${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/variant-detail?id=` +
getIdFromSlug(slug) +
'&auth=' +
tier
- )
- let product = response.data
+ );
+ let product = response.data;
// let product = await variantApi({ id: getIdFromSlug(slug), headers: { Token: authToken } })
if (product?.length == 1) {
- product = product[0]
+ product = product[0];
/* const regexHtmlTags = /(<([^>]+)>)/gi
const regexHtmlTagsExceptP = /<\/?(?!p\b)[^>]*>/g
product.description = product.description
@@ -39,26 +43,26 @@ export async function getServerSideProps(context) {
.replace(regexHtmlTags, ' ')
.trim()*/
} else {
- product = null
+ product = null;
}
return {
- props: { product }
- }
+ props: { product },
+ };
}
export default function ProductDetail({ product }) {
- const router = useRouter()
+ const router = useRouter();
- const { setProduct } = useProductContext()
+ const { setProduct } = useProductContext();
useEffect(() => {
if (product) {
- setProduct(product)
+ setProduct(product);
}
- }, [product, setProduct])
+ }, [product, setProduct]);
- if (!product) return <PageNotFound />
+ if (!product) return <PageNotFound />;
return (
<BasicLayout>
@@ -72,16 +76,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 && (
@@ -91,5 +95,5 @@ export default function ProductDetail({ product }) {
)}
{product && <Product product={product} isVariant={true} />}
</BasicLayout>
- )
+ );
}
diff --git a/src/pages/shop/promo/[slug].tsx b/src/pages/shop/promo/[slug].tsx
new file mode 100644
index 00000000..bd69c071
--- /dev/null
+++ b/src/pages/shop/promo/[slug].tsx
@@ -0,0 +1,523 @@
+import dynamic from 'next/dynamic'
+import NextImage from 'next/image';
+import { useEffect, useState } from 'react'
+import { useRouter } from 'next/router'
+import Seo from '../../../core/components/Seo'
+import Promocrumb from '../../../lib/promo/components/Promocrumb'
+import { fetchPromoItemsSolr, fetchVariantSolr } from '../../../api/promoApi'
+import LogoSpinner from '../../../core/components/elements/Spinner/LogoSpinner.jsx'
+import ProductPromoCard from '../../../../src-migrate/modules/product-promo/components/Card'
+import { IPromotion } from '../../../../src-migrate/types/promotion'
+import React from 'react'
+import { SolrResponse } from "../../../../src-migrate/types/solr.ts";
+import DesktopView from '../../../core/components/views/DesktopView';
+import MobileView from '../../../core/components/views/MobileView';
+import 'swiper/swiper-bundle.css';
+import useDevice from '../../../core/hooks/useDevice'
+import ProductFilterDesktop from '../../../lib/product/components/ProductFilterDesktopPromotion';
+import ProductFilter from '../../../lib/product/components/ProductFilter';
+import { HStack, Image, Tag, TagCloseButton, TagLabel } from '@chakra-ui/react';
+import { formatCurrency } from '../../../core/utils/formatValue';
+import Pagination from '../../../core/components/elements/Pagination/Pagination';
+import SideBanner from '../../../../src-migrate/modules/side-banner';
+import whatsappUrl from '../../../core/utils/whatsappUrl';
+import { cons, toQuery } from 'lodash-contrib';
+import _ from 'lodash';
+import useActive from '../../../core/hooks/useActive';
+
+const BasicLayout = dynamic(() => import('../../../core/components/layouts/BasicLayout'))
+
+export default function PromoDetail() {
+ const router = useRouter()
+ const { slug = '', brand ='', category='', priceFrom = '', priceTo = '', page = '1' } = router.query
+ const [promoItems, setPromoItems] = useState<any[]>([])
+ const [promoData, setPromoData] = useState<IPromotion[] | null>(null)
+ const [currentPage, setCurrentPage] = useState(parseInt(page as string, 10) || 1);
+ const itemsPerPage = 12; // Jumlah item yang ingin ditampilkan per halaman
+ const [loading, setLoading] = useState(true);
+ const { isMobile, isDesktop } = useDevice()
+ const [brands, setBrands] = useState<Brand[]>([]);
+ const [categories, setCategories] = useState<Category[]>([]);
+ const [brandValues, setBrandValues] = useState<string[]>([]);
+ const [categoryValues, setCategoryValues] = useState<string[]>([]);
+ const [orderBy, setOrderBy] = useState(router.query?.orderBy || 'popular');
+ const popup = useActive();
+ const prefixUrl = `/shop/promo/${slug}`
+
+ useEffect(() => {
+ if (router.query.brand) {
+ let brandsArray: string[] = [];
+ if (Array.isArray(router.query.brand)) {
+ brandsArray = router.query.brand;
+ } else if (typeof router.query.brand === 'string') {
+ brandsArray = router.query.brand.split(',').map((brand) => brand.trim());
+ }
+ setBrandValues(brandsArray);
+ } else {
+ setBrandValues([]);
+ }
+
+ if (router.query.category) {
+ let categoriesArray: string[] = [];
+
+ if (Array.isArray(router.query.category)) {
+ categoriesArray = router.query.category;
+ } else if (typeof router.query.category === 'string') {
+ categoriesArray = router.query.category.split(',').map((category) => category.trim());
+ }
+ setCategoryValues(categoriesArray);
+ } else {
+ setCategoryValues([]);
+ }
+ }, [router.query.brand, router.query.category]);
+
+ interface Brand {
+ brand: string;
+ qty: number;
+ }
+
+ interface Category {
+ name: string;
+ qty: number;
+ }
+
+ useEffect(() => {
+ const loadPromo = async () => {
+ setLoading(true);
+ const brandsData: Brand[] = [];
+ const categoriesData: Category[] = [];
+
+ const pageNumber = Array.isArray(page) ? parseInt(page[0], 10) : parseInt(page, 10);
+ setCurrentPage(pageNumber)
+
+ try {
+ const items = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug}`);
+ setPromoItems(items);
+
+ if (items.length === 0) {
+ setPromoData([])
+ setLoading(false);
+ return;
+ }
+
+ const brandArray = Array.isArray(brand) ? brand : brand.split(',');
+ const categoryArray = Array.isArray(category) ? category : category.split(',');
+
+ const promoDataPromises = items.map(async (item) => {
+
+ try {
+ let brandQuery = '';
+ if (brand) {
+ brandQuery = brandArray.map(b => `manufacture_name_s:${b}`).join(' OR ');
+ brandQuery = `(${brandQuery})`;
+ }
+
+ let categoryQuery = '';
+ if (category) {
+ categoryQuery = categoryArray.map(c => `category_name:${c}`).join(' OR ');
+ categoryQuery = `(${categoryQuery})`;
+ }
+
+ let priceQuery = '';
+ if (priceFrom && priceTo) {
+ priceQuery = `price_f:[${priceFrom} TO ${priceTo}]`;
+ } else if (priceFrom) {
+ priceQuery = `price_f:[${priceFrom} TO *]`;
+ } else if (priceTo) {
+ priceQuery = `price_f:[* TO ${priceTo}]`;
+ }
+
+ let combinedQuery = '';
+ let combinedQueryPrice = `${priceQuery}`;
+ if (brand && category && priceFrom || priceTo) {
+ combinedQuery = `${brandQuery} AND ${categoryQuery} `;
+ } else if (brand && category) {
+ combinedQuery = `${brandQuery} AND ${categoryQuery}`;
+ } else if (brand && priceFrom || priceTo) {
+ combinedQuery = `${brandQuery}`;
+ } else if (category && priceFrom || priceTo) {
+ combinedQuery = `${categoryQuery}`;
+ } else if (brand) {
+ combinedQuery = brandQuery;
+ } else if (category) {
+ combinedQuery = categoryQuery;
+ }
+
+ if (combinedQuery && priceFrom || priceTo) {
+ const response = await fetchVariantSolr(`id:${item.product_id} AND ${combinedQuery}`);
+ const product = response.response.docs[0];
+ const product_id = product.id;
+ const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} AND ${combinedQueryPrice}`);
+ return response2;
+ }else if(combinedQuery){
+ const response = await fetchVariantSolr(`id:${item.product_id} AND ${combinedQuery}`);
+ const product = response.response.docs[0];
+ const product_id = product.id;
+ const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} `);
+ return response2;
+ } else {
+ const response = await fetchPromoItemsSolr(`id:${item.id}`);
+ return response;
+ }
+ } catch (fetchError) {
+ return [];
+ }
+ });
+
+ const promoDataArray = await Promise.all(promoDataPromises);
+ const mergedPromoData = promoDataArray.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []);
+ setPromoData(mergedPromoData);
+
+ const dataBrandCategoryPromises = promoDataArray.map(async (promoData) => {
+ if (promoData) {
+ const dataBrandCategory = promoData.map(async (item) => {
+ let response;
+ if(category){
+ const categoryQuery = categoryArray.map(c => `category_name:${c}`).join(' OR ');
+ response = await fetchVariantSolr(`id:${item.products[0].product_id} AND (${categoryQuery})`);
+ }else{
+ response = await fetchVariantSolr(`id:${item.products[0].product_id}`)
+ }
+
+
+ if (response.response?.docs?.length > 0) {
+ const product = response.response.docs[0];
+ const manufactureNameS = product.manufacture_name;
+ if (Array.isArray(manufactureNameS)) {
+ for (let i = 0; i < manufactureNameS.length; i += 2) {
+ const brand = manufactureNameS[i];
+ const qty = 1;
+ const existingBrandIndex = brandsData.findIndex(b => b.brand === brand);
+ if (existingBrandIndex !== -1) {
+ brandsData[existingBrandIndex].qty += qty;
+ } else {
+ brandsData.push({ brand, qty });
+ }
+ }
+ }
+
+ const categoryNameS = product.category_name;
+ if (Array.isArray(categoryNameS)) {
+ for (let i = 0; i < categoryNameS.length; i += 2) {
+ const name = categoryNameS[i];
+ const qty = 1;
+ const existingCategoryIndex = categoriesData.findIndex(c => c.name === name);
+ if (existingCategoryIndex !== -1) {
+ categoriesData[existingCategoryIndex].qty += qty;
+ } else {
+ categoriesData.push({ name, qty });
+ }
+ }
+ }
+ }
+ });
+
+ return Promise.all(dataBrandCategory);
+ }
+ });
+
+ await Promise.all(dataBrandCategoryPromises);
+ setBrands(brandsData);
+ setCategories(categoriesData);
+ setLoading(false);
+
+ } catch (loadError) {
+ // console.error("Error loading promo items:", loadError)
+ setLoading(false);
+ }
+ }
+
+ if (slug) {
+ loadPromo()
+ }
+ },[slug, brand, category, priceFrom, priceTo, currentPage]);
+
+
+ function capitalizeFirstLetter(string) {
+ string = string.replace(/_/g, ' ');
+ return string.replace(/(^\w|\s\w)/g, function(match) {
+ return match.toUpperCase();
+ });
+ }
+
+ const handleDeleteFilter = async (source, value) => {
+ let params = {
+ q: router.query.q,
+ orderBy: '',
+ brand: brandValues.join(','),
+ category: categoryValues.join(','),
+ priceFrom: priceFrom || '',
+ priceTo: priceTo || '',
+ };
+
+ let brands = brandValues;
+ let catagories = categoryValues;
+ switch (source) {
+ case 'brands':
+ brands = brandValues.filter((item) => item !== value);
+ params.brand = brands.join(',');
+ await setBrandValues(brands);
+ break;
+ case 'category':
+ catagories = categoryValues.filter((item) => item !== value);
+ params.category = catagories.join(',');
+ await setCategoryValues(catagories);
+ break;
+ case 'price':
+ params.priceFrom = '';
+ params.priceTo = '';
+ break;
+ case 'delete':
+ params = {
+ q: router.query.q,
+ orderBy: '',
+ brand: '',
+ category: '',
+ priceFrom: '',
+ priceTo: '',
+ };
+ break;
+ }
+
+ handleSubmitFilter(params);
+ };
+ const handleSubmitFilter = (params) => {
+ params = _.pickBy(params, _.identity);
+ params = toQuery(params);
+ router.push(`${slug}?${params}`);
+ };
+
+ const visiblePromotions = promoData?.slice( (currentPage-1) * itemsPerPage, currentPage * 12)
+
+ const toQuery = (obj) => {
+ const str = Object.keys(obj)
+ .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`)
+ .join('&')
+ return str
+ }
+
+ const whatPromo = capitalizeFirstLetter(slug)
+ const queryWithoutSlug = _.omit(router.query, ['slug'])
+ const queryString = toQuery(queryWithoutSlug)
+
+ return (
+ <BasicLayout>
+ <Seo
+ title={`Promo ${Array.isArray(slug) ? slug[0] : slug} Terkini`}
+ description='B2B Marketplace MRO & Industri dengan Layanan Pembayaran Tempo, Faktur Pajak, Online Quotation, Garansi Resmi & Harga Kompetitif'
+ />
+ <Promocrumb brandName={whatPromo} />
+ <MobileView>
+ <div className='p-4 pt-0'>
+ <h1 className='mb-2 font-semibold text-h-sm'>Promo {whatPromo}</h1>
+
+ <FilterChoicesComponent
+ brandValues={brandValues}
+ categoryValues={categoryValues}
+ priceFrom={priceFrom}
+ priceTo={priceTo}
+ handleDeleteFilter={handleDeleteFilter}
+ />
+ {promoItems.length >= 1 && (
+ <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}
+ >
+ Filter
+ </button>
+ </div>
+ </div>
+ )}
+
+ {loading ? (
+ <div className='container flex justify-center my-4'>
+ <LogoSpinner width={48} height={48} />
+ </div>
+ ) : promoData && promoItems.length >= 1 ? (
+ <>
+ <div className='grid grid-cols-1 gap-x-1 gap-y-1'>
+ {visiblePromotions?.map((promotion) => (
+ <div key={promotion.id} className="min-w-36 max-w-[400px] mb-[20px] sm:w-full md:w-1/2 lg:w-1/3 xl:w-1/4 ">
+ <ProductPromoCard promotion={promotion}/>
+ </div>
+ ))}
+ </div>
+ </>
+ ) : (
+ <div className="text-center my-8">
+ <p>Belum ada promo pada kategori ini</p>
+ </div>
+ )}
+
+ <Pagination
+ pageCount={Math.ceil((promoData?.length ?? 0) / itemsPerPage)}
+ currentPage={currentPage}
+ url={`${prefixUrl}?${toQuery(_.omit(queryWithoutSlug, ['page']))}`}
+ className='mt-6 mb-2'
+ />
+ <ProductFilter
+ active={popup.active}
+ close={popup.deactivate}
+ brands={brands || []}
+ categories={categories || []}
+ prefixUrl={router.asPath.includes('?') ? `${router.asPath}` : `${router.asPath}?`}
+ defaultBrand={null}
+ />
+ </div>
+
+ </MobileView>
+ <DesktopView>
+ <div className='container mx-auto flex mb-3 flex-col'>
+ <div className='w-full pl-6'>
+ <h1 className='text-2xl mb-2 font-semibold'>Promo {whatPromo}</h1>
+ <div className=' w-full h-full flex flex-row items-center '>
+
+ <div className='detail-filter w-1/2 flex justify-start items-center mt-4'>
+
+ <FilterChoicesComponent
+ brandValues={brandValues}
+ categoryValues={categoryValues}
+ priceFrom={priceFrom}
+ priceTo={priceTo}
+ handleDeleteFilter={handleDeleteFilter}
+ />
+ </div>
+ <div className='Filter w-1/2 flex flex-col'>
+
+ <ProductFilterDesktop
+ brands={brands || []}
+ categories={categories || []}
+ prefixUrl={'/shop/promo'}
+ // defaultBrand={null}
+ />
+ </div>
+ </div>
+ {loading ? (
+ <div className='container flex justify-center my-4'>
+ <LogoSpinner width={48} height={48} />
+ </div>
+ ) : promoData && promoItems.length >= 1 ? (
+ <>
+ <div className='grid grid-cols-1 gap-x-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3'>
+ {visiblePromotions?.map((promotion) => (
+ <div key={promotion.id} className="min-w-[400px] max-w-[400px] mb-[20px] sm:min-w-[350px] md:min-w-[380px] lg:min-w-[400px] xl:min-w-[400px] ">
+ <ProductPromoCard promotion={promotion}/>
+ </div>
+ ))}
+ </div>
+ </>
+ ) : (
+ <div className="text-center my-8">
+ <p>Belum ada promo pada kategori ini</p>
+ </div>
+ )}
+ <div className='flex justify-between items-center mt-6 mb-2'>
+ <div className='pt-2 pb-6 flex items-center gap-x-3'>
+ <NextImage
+ src='/images/logo-question.png'
+ alt='Logo Question Indoteknik'
+ width={60}
+ height={60}
+ />
+ <div className='text-gray_r-12/90'>
+ <span>
+ Barang yang anda cari tidak ada?{' '}
+ <a
+ href={
+ router.query?.q
+ ? whatsappUrl('productSearch', {
+ name: router.query.q,
+ })
+ : whatsappUrl()
+ }
+ className='text-danger-500'
+ >
+ Hubungi Kami
+ </a>
+ </span>
+ </div>
+ </div>
+
+
+
+ <Pagination
+ pageCount={Math.ceil((promoData?.length ?? 0) / itemsPerPage)}
+ currentPage={currentPage}
+ url={`${prefixUrl}?${toQuery(_.omit(queryWithoutSlug, ['page']))}`}
+ className='mt-6 mb-2'
+ />
+ </div>
+
+ </div>
+ </div>
+ </DesktopView>
+ </BasicLayout>
+ )
+ }
+
+const FilterChoicesComponent = ({
+ brandValues,
+ categoryValues,
+ priceFrom,
+ priceTo,
+ handleDeleteFilter,
+ }) => (
+ <div className='flex items-center mb-4'>
+ <HStack spacing={2} className='flex-wrap'>
+ {brandValues?.map((value, index) => (
+ <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'
+ >
+ <TagLabel>{value}</TagLabel>
+ <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)}
+ />
+ </Tag>
+ )}
+ {brandValues?.length > 0 ||
+ categoryValues?.length > 0 ||
+ priceFrom ||
+ priceTo ? (
+ <span>
+ <button
+ className='btn-transparent py-2 px-5 h-[40px] text-red-700'
+ onClick={() => handleDeleteFilter('delete')}
+ >
+ Hapus Semua
+ </button>
+ </span>
+ ) : (
+ ''
+ )}
+ </HStack>
+ </div>
+);
diff --git a/src/pages/shop/promo/index.tsx b/src/pages/shop/promo/index.tsx
new file mode 100644
index 00000000..7ec4f6b0
--- /dev/null
+++ b/src/pages/shop/promo/index.tsx
@@ -0,0 +1,186 @@
+import dynamic from 'next/dynamic'
+import { useEffect, useState } from 'react'
+import { useRouter } from 'next/router'
+import Seo from '../../../core/components/Seo.jsx'
+import Promocrumb from '../../../lib/promo/components/Promocrumb.jsx'
+import { fetchPromoItemsSolr } from '../../../api/promoApi.js'
+import LogoSpinner from '../../../core/components/elements/Spinner/LogoSpinner.jsx'
+import ProductPromoCard from '../../../../src-migrate/modules/product-promo/components/Card.tsx'
+import { IPromotion } from '../../../../src-migrate/types/promotion.ts'
+import React from 'react'
+import { SolrResponse } from "../../../../src-migrate/types/solr.ts";
+
+const BasicLayout = dynamic(() => import('../../../core/components/layouts/BasicLayout.jsx'))
+
+export default function Promo() {
+ const router = useRouter()
+ const { slug = '' } = router.query
+ const [promoItems, setPromoItems] = useState<any[]>([])
+ const [promoData, setPromoData] = useState<IPromotion[] | null>(null)
+ const [loading, setLoading] = useState(true)
+ const [currentPage, setCurrentPage] = useState(1)
+ const [fetchingData, setFetchingData] = useState(false)
+
+ useEffect(() => {
+ const loadPromo = async () => {
+ try {
+ const items = await fetchPromoItemsSolr(`*:*`)
+
+
+ setPromoItems(items)
+
+
+ if (items.length === 0) {
+ setPromoData([])
+ setLoading(false);
+ return;
+ }
+
+ const promoDataPromises = items.map(async (item) => {
+ const queryParams = new URLSearchParams({ q: `id:${item.id}` })
+
+
+ try {
+ const response = await fetch(`/solr/promotion_program_lines/select?${queryParams.toString()}`)
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`)
+ }
+
+ const data: SolrResponse<any[]> = await response.json()
+
+
+ const promotions = await map(data.response.docs)
+ return promotions;
+ } catch (fetchError) {
+ console.error("Error fetching promotion data:", fetchError)
+ return [];
+ }
+ });
+
+ const promoDataArray = await Promise.all(promoDataPromises);
+ const mergedPromoData = promoDataArray.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []);
+ setPromoData(mergedPromoData);
+ setTimeout(() => setLoading(false), 120); // Menambahkan delay 200ms sebelum mengubah status loading
+ } catch (loadError) {
+ console.error("Error loading promo items:", loadError)
+ setLoading(false);
+ }
+ }
+
+ if (slug) {
+ loadPromo()
+ }
+ }, [slug])
+
+ const map = async (promotions: any[]): Promise<IPromotion[]> => {
+ const result: IPromotion[] = []
+
+ for (const promotion of promotions) {
+ const data: IPromotion = {
+ id: promotion.id,
+ program_id: promotion.program_id_i,
+ name: promotion.name_s,
+ type: {
+ value: promotion.type_value_s,
+ label: promotion.type_label_s,
+ },
+ limit: promotion.package_limit_i,
+ limit_user: promotion.package_limit_user_i,
+ limit_trx: promotion.package_limit_trx_i,
+ price: promotion.price_f,
+ total_qty: promotion.total_qty_i,
+ products: JSON.parse(promotion.products_s),
+ free_products: JSON.parse(promotion.free_products_s),
+ }
+
+ result.push(data)
+ }
+
+ return result
+ }
+
+
+
+
+ useEffect(() => {
+ const handleScroll = () => {
+ if (
+ !fetchingData &&
+ window.innerHeight + document.documentElement.scrollTop >= 0.95 * document.documentElement.offsetHeight
+ ) {
+ // User has scrolled to 95% of page height
+
+ setTimeout(() => setFetchingData(true), 120);
+ setCurrentPage((prevPage) => prevPage + 1)
+ }
+ }
+
+ window.addEventListener('scroll', handleScroll)
+ return () => window.removeEventListener('scroll', handleScroll)
+ }, [fetchingData])
+
+ useEffect(() => {
+ if (fetchingData) {
+ // Fetch more data
+ // You may need to adjust this logic according to your API
+ fetchMoreData()
+ }
+ }, [fetchingData])
+
+ const fetchMoreData = async () => {
+ try {
+ // Add a delay of approximately 150ms
+ setTimeout(async () => {
+ // Fetch more data
+ // Update promoData state with the new data
+ }, 150)
+ } catch (error) {
+ console.error('Error fetching more data:', error)
+ } finally {
+ setTimeout(() => setFetchingData(false), 120);
+
+ }
+ }
+
+ const visiblePromotions = promoData?.slice(0, currentPage * 12)
+
+ return (
+ <BasicLayout>
+ <Seo
+ title={`Promo ${Array.isArray(slug) ? slug[0] : slug} Terkini`}
+ description='B2B Marketplace MRO & Industri dengan Layanan Pembayaran Tempo, Faktur Pajak, Online Quotation, Garansi Resmi & Harga Kompetitif'
+ />
+ {/* <Promocrumb brandName={capitalizeFirstLetter(Array.isArray(slug) ? slug[0] : slug)} /> */}
+ <div className='container mx-auto mt-1 flex mb-1'>
+ <div className=''>
+ <h1 className='font-semibold'>Semua Promo di Indoteknik</h1>
+ </div>
+ </div>
+ {loading ? (
+ <div className='container flex justify-center my-4'>
+ <LogoSpinner width={48} height={48} />
+ </div>
+ ) : promoData && promoItems.length >= 1 ? (
+ <>
+ <div className='flex flex-wrap justify-center'>
+ {visiblePromotions?.map((promotion) => (
+ <div key={promotion.id} className="min-w-[40px] max-w-[400px] mr-[20px] mb-[20px] sm:w-full md:w-1/2 lg:w-1/3 xl:w-1/4">
+ <ProductPromoCard promotion={promotion} />
+ </div>
+ ))}
+ </div>
+ {fetchingData && (
+ <div className='container flex justify-center my-4'>
+ <LogoSpinner width={48} height={48} />
+ </div>
+ )}
+ </>
+ ) : (
+ <div className="text-center my-8">
+ <p>Belum ada promo pada kategori ini</p>
+ </div>
+ )}
+ </BasicLayout>
+ )
+}
diff --git a/src/pages/video.jsx b/src/pages/video.jsx
index 61790dbb..7d1f8372 100644
--- a/src/pages/video.jsx
+++ b/src/pages/video.jsx
@@ -44,7 +44,7 @@ export default function Video() {
</LazyLoadComponent>
<div className='p-3'>
<a
- href='https://www.youtube.com/@indoteknikb2bindustriale-c778'
+ href='https://www.youtube.com/@indoteknikcom'
className='text-danger-500 mb-2 block'
>
{video.channelName}
diff --git a/src/styles/globals.css b/src/styles/globals.css
index bf9fec10..f6561b00 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -107,7 +107,7 @@ button {
disabled:bg-gray_r-5;
}
- .form-input[aria-invalid="true"] {
+ .form-input[aria-invalid='true'] {
@apply border-danger-500
focus:border-danger-500;
}
@@ -676,3 +676,21 @@ button {
@apply text-warning-500;
}
}
+
+::-webkit-scrollbar {
+ width: 12px;
+}
+
+::-webkit-scrollbar-track {
+ background-color: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background-color: #888;
+ border-radius: 6px;
+ border: 3px solid #f5f5f5;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background-color: #555;
+}
diff --git a/src/styles/normalize.css b/src/styles/normalize.css
new file mode 100644
index 00000000..92aed47e
--- /dev/null
+++ b/src/styles/normalize.css
@@ -0,0 +1,351 @@
+/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
+
+/* Document
+ ========================================================================== */
+
+/**
+ * 1. Correct the line height in all browsers.
+ * 2. Prevent adjustments of font size after orientation changes in iOS.
+ */
+
+html {
+ line-height: 1.15; /* 1 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/* Sections
+ ========================================================================== */
+
+/**
+ * Remove the margin in all browsers.
+ */
+
+body {
+ margin: 0;
+}
+
+/**
+ * Render the `main` element consistently in IE.
+ */
+
+main {
+ display: block;
+}
+
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/* Grouping content
+ ========================================================================== */
+
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+
+hr {
+ box-sizing: content-box; /* 1 */
+ height: 0; /* 1 */
+ overflow: visible; /* 2 */
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+pre {
+ font-family: monospace, monospace; /* 1 */
+ font-size: 1em; /* 2 */
+}
+
+/* Text-level semantics
+ ========================================================================== */
+
+/**
+ * Remove the gray background on active links in IE 10.
+ */
+
+a {
+ background-color: transparent;
+}
+
+/**
+ * 1. Remove the bottom border in Chrome 57-
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+ */
+
+abbr[title] {
+ border-bottom: none; /* 1 */
+ text-decoration: underline; /* 2 */
+ text-decoration: underline dotted; /* 2 */
+}
+
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+
+b,
+strong {
+ font-weight: bolder;
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+code,
+kbd,
+samp {
+ font-family: monospace, monospace; /* 1 */
+ font-size: 1em; /* 2 */
+}
+
+/**
+ * Add the correct font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+sup {
+ top: -0.5em;
+}
+
+/* Embedded content
+ ========================================================================== */
+
+/**
+ * Remove the border on images inside links in IE 10.
+ */
+
+img {
+ border-style: none;
+}
+
+/* Forms
+ ========================================================================== */
+
+/**
+ * 1. Change the font styles in all browsers.
+ * 2. Remove the margin in Firefox and Safari.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ font-family: inherit; /* 1 */
+ font-size: 100%; /* 1 */
+ line-height: 1.15; /* 1 */
+ margin: 0; /* 2 */
+}
+
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+
+button,
+input {
+ /* 1 */
+ overflow: visible;
+}
+
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+
+button,
+select {
+ /* 1 */
+ text-transform: none;
+}
+
+/**
+ * Correct the inability to style clickable types in iOS and Safari.
+ */
+
+button,
+[type='button'],
+[type='reset'],
+[type='submit'] {
+ -webkit-appearance: button;
+}
+
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+
+button::-moz-focus-inner,
+[type='button']::-moz-focus-inner,
+[type='reset']::-moz-focus-inner,
+[type='submit']::-moz-focus-inner {
+ border-style: none;
+ padding: 0;
+}
+
+/**
+ * Restore the focus styles unset by the previous rule.
+ */
+
+button:-moz-focusring,
+[type='button']:-moz-focusring,
+[type='reset']:-moz-focusring,
+[type='submit']:-moz-focusring {
+ outline: 1px dotted ButtonText;
+}
+
+/**
+ * Correct the padding in Firefox.
+ */
+
+fieldset {
+ padding: 0.35em 0.75em 0.625em;
+}
+
+/**
+ * 1. Correct the text wrapping in Edge and IE.
+ * 2. Correct the color inheritance from `fieldset` elements in IE.
+ * 3. Remove the padding so developers are not caught out when they zero out
+ * `fieldset` elements in all browsers.
+ */
+
+legend {
+ box-sizing: border-box; /* 1 */
+ color: inherit; /* 2 */
+ display: table; /* 1 */
+ max-width: 100%; /* 1 */
+ padding: 0; /* 3 */
+ white-space: normal; /* 1 */
+}
+
+/**
+ * Add the correct vertical alignment in Chrome, Firefox, and Opera.
+ */
+
+progress {
+ vertical-align: baseline;
+}
+
+/**
+ * Remove the default vertical scrollbar in IE 10+.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * 1. Add the correct box sizing in IE 10.
+ * 2. Remove the padding in IE 10.
+ */
+
+[type='checkbox'],
+[type='radio'] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+
+[type='number']::-webkit-inner-spin-button,
+[type='number']::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+
+[type='search'] {
+ -webkit-appearance: textfield; /* 1 */
+ outline-offset: -2px; /* 2 */
+}
+
+/**
+ * Remove the inner padding in Chrome and Safari on macOS.
+ */
+
+[type='search']::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+
+::-webkit-file-upload-button {
+ -webkit-appearance: button; /* 1 */
+ font: inherit; /* 2 */
+}
+
+/* Interactive
+ ========================================================================== */
+
+/*
+ * Add the correct display in Edge, IE 10+, and Firefox.
+ */
+
+details {
+ display: block;
+}
+
+/*
+ * Add the correct display in all browsers.
+ */
+
+summary {
+ display: list-item;
+}
+
+/* Misc
+ ========================================================================== */
+
+/**
+ * Add the correct display in IE 10+.
+ */
+
+template {
+ display: none;
+}
+
+/**
+ * Add the correct display in IE 10.
+ */
+
+[hidden] {
+ display: none;
+}
diff --git a/src/utils/solrMapping.js b/src/utils/solrMapping.js
index 41d24b53..dd90ac7d 100644
--- a/src/utils/solrMapping.js
+++ b/src/utils/solrMapping.js
@@ -1,17 +1,18 @@
export const productMappingSolr = (products, pricelist) => {
return products.map((product) => {
- let price = product.price_tier1_v2_f || 0
- let priceDiscount = 0
- let discountPercentage = 0
+ let price = product.price_tier1_v2_f || 0;
+ let priceDiscount = price;
+ let discountPercentage = 0;
if (pricelist && product?.[`price_${pricelist}_f`] < price) {
- price = product?.[`price_${pricelist}_f`] || 0
+ price = product?.[`price_${pricelist}_f`] || 0;
+ priceDiscount = price;
}
- if (product?.flashsale_id_i > 0 ) {
- price = product?.flashsale_base_price_f || 0
- priceDiscount = product?.flashsale_price_f || 0
- discountPercentage = product?.flashsale_discount_f || 0
+ if (product?.flashsale_id_i > 0) {
+ price = product?.flashsale_base_price_f || 0;
+ priceDiscount = product?.flashsale_price_f || 0;
+ discountPercentage = product?.flashsale_discount_f || 0;
}
let productMapped = {
@@ -29,48 +30,50 @@ export const productMappingSolr = (products, pricelist) => {
categories: [],
flashSale: {
id: product?.flashsale_id_i,
- remainingTime: flashsaleTime(product?.flashsale_end_date_s)?.remainingTime,
+ remainingTime: flashsaleTime(product?.flashsale_end_date_s)
+ ?.remainingTime,
name: product?.product?.flashsale_name_s,
- tag: product?.flashsale_tag_s || 'FLASH SALE'
+ tag: product?.flashsale_tag_s || 'FLASH SALE',
},
- qtySold : product?.qty_sold_f || 0
- }
+ qtySold: product?.qty_sold_f || 0,
+ isTkdn:product?.tkdn_b || false,
+ isSni:product?.sni_b || false,
+ };
if (product.manufacture_id_i && product.manufacture_name_s) {
productMapped.manufacture = {
id: product.manufacture_id_i || '',
name: product.manufacture_name_s || '',
imagePromotion1: product.image_promotion_1_s || '',
- imagePromotion2: product.image_promotion_2_s || ''
- }
+ imagePromotion2: product.image_promotion_2_s || '',
+ };
}
productMapped.categories = [
{
id: product.category_id_i || '',
- name: product.category_name_s || ''
- }
- ]
- return productMapped
- })
-}
+ name: product.category_name_s || '',
+ },
+ ];
+ return productMapped;
+ });
+};
export const variantsMappingSolr = (parent, products, pricelist) => {
return products.map((product) => {
- let price = product.price_tier1_v2_f || 0
- let priceDiscount = 0
- let discountPercentage = 0
+ let price = product.price_tier1_v2_f || 0;
+ let priceDiscount = price;
+ let discountPercentage = 0;
- if (pricelist) {
- if (product?.[`price_${pricelist}_f`] < price) {
- price = product?.[`price_${pricelist}_f`] || 0
- }
+ if (pricelist && product?.[`price_${pricelist}_f`] < price) {
+ price = product?.[`price_${pricelist}_f`] || 0;
+ priceDiscount = price;
}
if (product?.flashsale_id_i > 0 && product?.flashsale_price_f < price) {
- price = product?.flashsale_base_price_f || 0
- priceDiscount = product?.flashsale_price_f || 0
- discountPercentage = product?.flashsale_discount_f || 0
+ price = product?.flashsale_base_price_f || 0;
+ priceDiscount = product?.flashsale_price_f || 0;
+ discountPercentage = product?.flashsale_discount_f || 0;
}
let productMapped = {
@@ -87,30 +90,34 @@ export const variantsMappingSolr = (parent, products, pricelist) => {
weight: product.weight_f || 0,
manufacture: {},
parent: {},
- qtySold : product?.qty_sold_f || 0
- }
+ qtySold: product?.qty_sold_f || 0,
+ };
if (product.manufacture_id_i && product.manufacture_name_s) {
productMapped.manufacture = {
id: product.manufacture_id_i || '',
- name: product.manufacture_name_s || ''
- }
+ name: product.manufacture_name_s || '',
+ };
}
productMapped.parent = {
- id: parent.product_id_i || '',
- image: parent.image_s || '',
- name: parent.name_s || ''
- }
- return productMapped
- })
-}
+ id: parent[0]?.product_id_i || '',
+ image: parent[0]?.image_s || '',
+ name: parent[0]?.name_s || '',
+ description: parent[0]?.description_t || '',
+ };
+ return productMapped;
+ });
+};
const flashsaleTime = (endDate) => {
- const flashsaleEndDate = new Date(endDate)
- const currentTime = new Date()
+ const flashsaleEndDate = new Date(endDate);
+ const currentTime = new Date();
- const timeDifferenceInMillis = flashsaleEndDate - currentTime
- const timeDifferenceInSeconds = timeDifferenceInMillis / 1000
+ const timeDifferenceInMillis = flashsaleEndDate - currentTime;
+ const timeDifferenceInSeconds = timeDifferenceInMillis / 1000;
- return { remainingTime: timeDifferenceInSeconds, isFlashSale: flashsaleEndDate > currentTime }
-}
+ return {
+ remainingTime: timeDifferenceInSeconds,
+ isFlashSale: flashsaleEndDate > currentTime,
+ };
+};