summaryrefslogtreecommitdiff
path: root/src-migrate/modules
diff options
context:
space:
mode:
authorit-fixcomart <it@fixcomart.co.id>2024-11-28 10:47:43 +0700
committerit-fixcomart <it@fixcomart.co.id>2024-11-28 10:47:43 +0700
commit5bc7a6807847610b190ea9d5046021d2db15afc5 (patch)
treee895b02c65bf97e3c6c970bb8d777922120f4570 /src-migrate/modules
parent7ed3fd96322d08bd91434b8ec4dcbc542a610998 (diff)
parent952421c810b53ec4d25ad5ef605bae1bd1d5d616 (diff)
Merge branch 'new-release' into Feature/switch-account
Diffstat (limited to 'src-migrate/modules')
-rw-r--r--src-migrate/modules/cart/components/Item.tsx17
-rw-r--r--src-migrate/modules/page-content/index.tsx29
-rw-r--r--src-migrate/modules/product-card/components/ProductCard.tsx176
-rw-r--r--src-migrate/modules/product-detail/components/AddToCart.tsx312
-rw-r--r--src-migrate/modules/product-detail/components/AddToQuotation.tsx227
-rw-r--r--src-migrate/modules/product-detail/components/Image.tsx118
-rw-r--r--src-migrate/modules/product-detail/components/Information.tsx234
-rw-r--r--src-migrate/modules/product-detail/components/PriceAction.tsx139
-rw-r--r--src-migrate/modules/product-detail/components/ProductDetail.tsx195
-rw-r--r--src-migrate/modules/product-detail/stores/useProductDetail.ts12
-rw-r--r--src-migrate/modules/product-detail/styles/information.module.css4
-rw-r--r--src-migrate/modules/product-detail/styles/price-action.module.css5
-rw-r--r--src-migrate/modules/promo/components/FlashSaleNonDisplay.tsx17
-rw-r--r--src-migrate/modules/promo/components/PromoList.tsx79
-rw-r--r--src-migrate/modules/register/components/TermCondition.tsx48
15 files changed, 1112 insertions, 500 deletions
diff --git a/src-migrate/modules/cart/components/Item.tsx b/src-migrate/modules/cart/components/Item.tsx
index 6ffbb524..ab2e7ce1 100644
--- a/src-migrate/modules/cart/components/Item.tsx
+++ b/src-migrate/modules/cart/components/Item.tsx
@@ -36,26 +36,29 @@ const CartItem = ({ item, editable = true, selfPicking}: Props) => {
)}
<div className='w-2' />
<div>
- Selamat! Pembelian anda lebih hemat {' '}
+ Selamat! Pembelian anda lebih hemat{' '}
<span className={style.savingAmt}>
- Rp{formatCurrency((item.package_price || 0) * item.quantity - item.subtotal)}
+ Rp
+ {formatCurrency(
+ (item.package_price || 0) * item.quantity - item.subtotal
+ )}
</span>
</div>
</div>
)}
<div className={style.mainProdWrapper}>
- {editable && (
- <CartItemSelect item={item} />
- )}
+ {editable && <CartItemSelect item={item} />}
<div className='w-4' />
<CartItem.Image item={item} />
<div className={style.details}>
- {(item.is_in_bu) && (item.on_hand_qty >= item.quantity) && (
+ {item?.available_quantity > 0 && (
<div className='text-[10px] text-red-500 italic'>
- *Barang ini bisa di pickup maksimal pukul 16.00
+ {item.quantity <= item?.available_quantity
+ ? '*Barang ini bisa di pickup maksimal pukul 16.00'
+ : `*${item?.available_quantity} Barang ini bisa di pickup maksimal pukul 16.00`}
</div>
)}
<CartItem.Name item={item} />
diff --git a/src-migrate/modules/page-content/index.tsx b/src-migrate/modules/page-content/index.tsx
index edecb855..54ee0a04 100644
--- a/src-migrate/modules/page-content/index.tsx
+++ b/src-migrate/modules/page-content/index.tsx
@@ -1,4 +1,4 @@
-import { useMemo } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { PageContentProps } from '~/types/pageContent';
import { getPageContent } from '~/services/pageContent';
@@ -8,18 +8,31 @@ type Props = {
};
const PageContent = ({ path }: Props) => {
- const { data, isLoading } = useQuery<PageContentProps>(
- `page-content:${path}`,
- async () => await getPageContent({ path })
- );
+ const [localData, setData] = useState<PageContentProps>();
+ const [shouldFetch, setShouldFetch] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ setIsLoading(true);
+ const res = await fetch(`/api/page-content?path=${path}`);
+ const { data } = await res.json();
+ if (data) {
+ setData(data);
+ }
+ setIsLoading(false);
+ };
+
+ fetchData();
+ }, []);
const parsedContent = useMemo<string>(() => {
- if (!data) return '';
- return data.content.replaceAll(
+ if (!localData) return '';
+ return localData.content.replaceAll(
'src="/web/image',
`src="${process.env.NEXT_PUBLIC_ODOO_API_HOST}/web/image`
);
- }, [data]);
+ }, [localData]);
if (isLoading) return <PageContentSkeleton />;
return <div dangerouslySetInnerHTML={{ __html: parsedContent || '' }}></div>;
diff --git a/src-migrate/modules/product-card/components/ProductCard.tsx b/src-migrate/modules/product-card/components/ProductCard.tsx
index 0febfadb..a439cdc8 100644
--- a/src-migrate/modules/product-card/components/ProductCard.tsx
+++ b/src-migrate/modules/product-card/components/ProductCard.tsx
@@ -1,95 +1,108 @@
-import style from '../styles/product-card.module.css'
+import style from '../styles/product-card.module.css';
import ImageNext from 'next/image';
-import clsx from 'clsx'
-import Link from 'next/link'
-import React, { useEffect, useMemo, useState } from 'react'
-import Image from '~/components/ui/image'
-import useUtmSource from '~/hooks/useUtmSource'
-import clsxm from '~/libs/clsxm'
-import formatCurrency from '~/libs/formatCurrency'
-import { formatToShortText } from '~/libs/formatNumber'
-import { createSlug } from '~/libs/slug'
-import { IProduct } from '~/types/product'
-
+import clsx from 'clsx';
+import Link from 'next/link';
+import React, { useEffect, useMemo, useState } from 'react';
+import Image from '~/components/ui/image';
+import useUtmSource from '~/hooks/useUtmSource';
+import clsxm from '~/libs/clsxm';
+import formatCurrency from '~/libs/formatCurrency';
+import { formatToShortText } from '~/libs/formatNumber';
+import { createSlug } from '~/libs/slug';
+import { IProduct } from '~/types/product';
+import useDevice from '@/core/hooks/useDevice';
type Props = {
- product: IProduct
- layout?: 'vertical' | 'horizontal'
-}
+ product: IProduct;
+ layout?: 'vertical' | 'horizontal';
+};
const ProductCard = ({ product, layout = 'vertical' }: Props) => {
- const utmSource = useUtmSource()
-
+ const utmSource = useUtmSource();
+ const { isDesktop, isMobile } = useDevice();
const URL = {
- product: createSlug('/shop/product/', product.name, product.id.toString()) + `?utm_source=${utmSource}`,
- manufacture: createSlug('/shop/brands/', product.manufacture.name, product.manufacture.id.toString()),
- }
+ product:
+ createSlug('/shop/product/', product.name, product.id.toString()) +
+ `?utm_source=${utmSource}`,
+ manufacture: createSlug(
+ '/shop/brands/',
+ product.manufacture.name,
+ product.manufacture.id.toString()
+ ),
+ };
const image = useMemo(() => {
- if (product.image) return product.image + '?ratio=square'
- return '/images/noimage.jpeg'
- }, [product.image])
+ if (!isDesktop && product.image_mobile) {
+ return product.image_mobile + '?ratio=square';
+ } else {
+ if (product.image) return product.image + '?ratio=square';
+ return '/images/noimage.jpeg';
+ }
+ }, [product.image, product.image_mobile]);
return (
- <div className={clsxm(style['wrapper'], {
- [style['wrapper-v']]: layout === 'vertical',
- [style['wrapper-h']]: layout === 'horizontal',
- })}
+ <div
+ className={clsxm(style['wrapper'], {
+ [style['wrapper-v']]: layout === 'vertical',
+ [style['wrapper-h']]: layout === 'horizontal',
+ })}
>
- <div className={clsxm('relative', {
- [style['image-v']]: layout === 'vertical',
- [style['image-h']]: layout === 'horizontal',
- })}>
+ <div
+ className={clsxm('relative', {
+ [style['image-v']]: layout === 'vertical',
+ [style['image-h']]: layout === 'horizontal',
+ })}
+ >
<Link href={URL.product}>
-
- <div className="relative">
- <Image
- src={image}
- alt={product.name}
- width={128}
- height={128}
- className='object-contain object-center h-full w-full'
- />
- <div className="absolute top-0 right-0 flex mt-2">
- <div className="gambarB ">
- {product.isSni && (
- <ImageNext
- src="/images/sni-logo.png"
- alt="SNI Logo"
- className="w-3 h-4 object-contain object-top sm:h-4"
- width={50}
- height={50}
- />
- )}
- </div>
- <div className="gambarC ">
- {product.isTkdn && (
- <ImageNext
- src="/images/TKDN.png"
- alt="TKDN"
- className="w-5 h-4 object-contain object-top ml-1 mr-1 sm:h-6"
- width={50}
- height={50}
- />
- )}
+ <div className='relative'>
+ <Image
+ src={image}
+ alt={product.name}
+ width={128}
+ height={128}
+ className='object-contain object-center h-full w-full'
+ />
+ <div className='absolute top-0 right-0 flex mt-2'>
+ <div className='gambarB '>
+ {product.isSni && (
+ <ImageNext
+ src='/images/sni-logo.png'
+ alt='SNI Logo'
+ className='w-3 h-4 object-contain object-top sm:h-4'
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ <div className='gambarC '>
+ {product.isTkdn && (
+ <ImageNext
+ src='/images/TKDN.png'
+ alt='TKDN'
+ className='w-5 h-4 object-contain object-top ml-1 mr-1 sm:h-6'
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
</div>
</div>
- </div>
{product.variant_total > 1 && (
- <div className={style['variant-badge']}>{product.variant_total} Varian</div>
+ <div className={style['variant-badge']}>
+ {product.variant_total} Varian
+ </div>
)}
</Link>
</div>
- <div className={clsxm({
- [style['content-v']]: layout === 'vertical',
- [style['content-h']]: layout === 'horizontal',
- })}>
- <Link
- href={URL.manufacture}
- className={style['brand']}
- >
+ <div
+ className={clsxm({
+ [style['content-v']]: layout === 'vertical',
+ [style['content-h']]: layout === 'horizontal',
+ })}
+ >
+ <Link href={URL.manufacture} className={style['brand']}>
{product.manufacture.name}
</Link>
@@ -113,17 +126,15 @@ const ProductCard = ({ product, layout = 'vertical' }: Props) => {
<div className='h-1.5' />
<div className={style['price-inc']}>
- Inc PPN:
- Rp {formatCurrency(Math.round(product.lowest_price.price * 1.11))}
+ Inc PPN: Rp{' '}
+ {formatCurrency(Math.round(product.lowest_price.price * 1.11))}
</div>
<div className='h-1' />
<div className='flex items-center gap-x-2.5'>
{product.stock_total > 0 && (
- <div className={style['ready-stock']}>
- Ready Stock
- </div>
+ <div className={style['ready-stock']}>Ready Stock</div>
)}
{product.qty_sold > 0 && (
<div className={style['sold']}>
@@ -131,14 +142,11 @@ const ProductCard = ({ product, layout = 'vertical' }: Props) => {
</div>
)}
</div>
-
</div>
</div>
- )
-}
-
-const classPrefix = ({ layout }: Props) => {
+ );
+};
-}
+const classPrefix = ({ layout }: Props) => {};
-export default ProductCard \ No newline at end of file
+export default ProductCard;
diff --git a/src-migrate/modules/product-detail/components/AddToCart.tsx b/src-migrate/modules/product-detail/components/AddToCart.tsx
index a5284637..280e4a7a 100644
--- a/src-migrate/modules/product-detail/components/AddToCart.tsx
+++ b/src-migrate/modules/product-detail/components/AddToCart.tsx
@@ -1,51 +1,55 @@
-import BottomPopup from '@/core/components/elements/Popup/BottomPopup'
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
import style from '../styles/price-action.module.css';
-import { Button, Link, useToast } from '@chakra-ui/react'
-import product from 'next-seo/lib/jsonld/product'
-import { useRouter } from 'next/router'
-import { useEffect, useState } from 'react'
-import Image from '~/components/ui/image'
-import { getAuth } from '~/libs/auth'
-import { upsertUserCart } from '~/services/cart'
+import { Button, Link, useToast } from '@chakra-ui/react';
+import product from 'next-seo/lib/jsonld/product';
+import { useRouter } from 'next/router';
+import { useEffect, useState } from 'react';
+import Image from '~/components/ui/image';
+import { getAuth } from '~/libs/auth';
+import { upsertUserCart } from '~/services/cart';
import LazyLoad from 'react-lazy-load';
import ProductSimilar from '../../../../src/lib/product/components/ProductSimilar';
import { IProductDetail } from '~/types/product';
import ImageNext from 'next/image';
-import { useProductCartContext } from '@/contexts/ProductCartContext'
-import { createSlug } from '~/libs/slug'
-import formatCurrency from '~/libs/formatCurrency'
+import { useProductCartContext } from '@/contexts/ProductCartContext';
+import { createSlug } from '~/libs/slug';
+import formatCurrency from '~/libs/formatCurrency';
import { useProductDetail } from '../stores/useProductDetail';
type Props = {
- variantId: number | null,
+ variantId: number | null;
quantity?: number;
source?: 'buy' | 'add_to_cart';
- products : IProductDetail
-}
+ products: IProductDetail;
+};
-type Status = 'idle' | 'loading' | 'success'
+type Status = 'idle' | 'loading' | 'success';
const AddToCart = ({
variantId,
quantity = 1,
source = 'add_to_cart',
- products
+ products,
}: Props) => {
- const auth = getAuth()
- const router = useRouter()
+ let auth = getAuth();
+ const router = useRouter();
const toast = useToast({
position: 'top',
- isClosable: true
- })
+ isClosable: true,
+ });
- const {
- askAdminUrl,
- } = useProductDetail();
+ const { askAdminUrl } = useProductDetail();
const [product, setProducts] = useState(products);
- const [status, setStatus] = useState<Status>('idle')
- const { productCart, setRefreshCart, setProductCart, refreshCart, isLoading, setIsloading } =
- useProductCartContext()
+ const [status, setStatus] = useState<Status>('idle');
+ const {
+ productCart,
+ setRefreshCart,
+ setProductCart,
+ refreshCart,
+ isLoading,
+ setIsloading,
+ } = useProductCartContext();
const productSimilarQuery = [
product?.name,
@@ -55,32 +59,48 @@ const AddToCart = ({
const [addCartAlert, setAddCartAlert] = useState(false);
const handleButton = async () => {
- if (typeof auth !== 'object') {
- const currentUrl = encodeURIComponent(router.asPath)
- router.push(`/login?next=${currentUrl}`)
- return;
+ let isLoggedIn = typeof auth === 'object';
+
+ if (!isLoggedIn) {
+ const currentUrl = encodeURIComponent(router.asPath);
+ await router.push(`/login?next=${currentUrl}`);
+
+ // Tunggu login berhasil, misalnya dengan memantau perubahan status auth.
+ const authCheckInterval = setInterval(() => {
+ const newAuth = getAuth();
+ if (typeof newAuth === 'object') {
+ isLoggedIn = true;
+ auth = newAuth; // Update nilai auth setelah login
+ clearInterval(authCheckInterval);
+ }
+ }, 500); // Periksa status login setiap 500ms
+
+ await new Promise((resolve) => {
+ const checkLogin = setInterval(() => {
+ if (isLoggedIn) {
+ clearInterval(checkLogin);
+ resolve(null);
+ }
+ }, 500);
+ });
}
-
- if (
- !variantId ||
- isNaN(quantity) ||
- typeof auth !== 'object'
- ) return;
- if (status === 'success') return
- setStatus('loading')
+
+ if (!variantId || isNaN(quantity) || typeof auth !== 'object') return;
+ if (status === 'success') return;
+ setStatus('loading');
await upsertUserCart({
userId: auth.id,
- type: 'product',
- id: variantId,
- qty: quantity,
- selected: true,
- source: source,
- qtyAppend: true
- })
- setStatus('idle')
+ type: 'product',
+ id: variantId,
+ qty: quantity,
+ selected: true,
+ source: source,
+ qtyAppend: true,
+ });
+ setStatus('idle');
setRefreshCart(true);
setAddCartAlert(true);
-
+
toast({
title: 'Tambah ke keranjang',
description: 'Berhasil menambahkan barang ke keranjang belanja',
@@ -88,120 +108,130 @@ const AddToCart = ({
duration: 3000,
isClosable: true,
position: 'top',
- })
-
+ });
+
if (source === 'buy') {
- router.push('/shop/checkout?source=buy')
+ router.push('/shop/checkout?source=buy');
}
- }
+ };
useEffect(() => {
- if (status === 'success') setTimeout(() => { setStatus('idle') }, 3000)
- }, [status])
+ if (status === 'success')
+ setTimeout(() => {
+ setStatus('idle');
+ }, 3000);
+ }, [status]);
const btnConfig = {
- 'add_to_cart': {
+ add_to_cart: {
colorScheme: 'yellow',
- text: 'Keranjang'
+ text: 'Keranjang',
},
- 'buy': {
+ buy: {
colorScheme: 'red',
- text: 'Beli'
- }
- }
+ text: 'Beli',
+ },
+ };
return (
<div className='w-full'>
- <Button onClick={handleButton} colorScheme={btnConfig[source].colorScheme} className='w-full'>
+ <Button
+ onClick={handleButton}
+ colorScheme={btnConfig[source].colorScheme}
+ className='w-full'
+ >
{btnConfig[source].text}
</Button>
<BottomPopup
- className='!container'
- title='Berhasil Ditambahkan'
- active={addCartAlert}
- close={() => {
- setAddCartAlert(false);
- }}
- >
- <div className='flex mt-4'>
- <div className='w-[10%]'>
- <ImageNext
- src={product.image}
- alt={product.name}
- className='h-32 object-contain object-center w-full border border-gray_r-4'
- width={80}
- height={80}
- />
- </div>
- <div className='ml-3 flex flex-1 items-start font-medium justify-center flex-col gap-y-1'>
- {!!product.manufacture.name ? (
- <Link
- href={createSlug('/shop/brands/', product.manufacture.name, product.manufacture.id.toString())}
- className=' hover:underline'
- color={"red"}
- >
- {product.manufacture.name}
- </Link>
- ) : '-'}
- <p className='text-ellipsis overflow-hidden'>
- {product.name}
- </p>
- <p>
- {product.code}
- </p>
- {!!product.lowest_price && product.lowest_price.price > 0 && (
+ className='!container'
+ title='Berhasil Ditambahkan'
+ active={addCartAlert}
+ close={() => {
+ setAddCartAlert(false);
+ }}
+ >
+ <div className='flex mt-4'>
+ <div className='w-[10%]'>
+ <ImageNext
+ src={product.image}
+ alt={product.name}
+ className='h-32 object-contain object-center w-full border border-gray_r-4'
+ width={80}
+ height={80}
+ />
+ </div>
+ <div className='ml-3 flex flex-1 items-start font-medium justify-center flex-col gap-y-1'>
+ {!!product.manufacture.name ? (
+ <Link
+ href={createSlug(
+ '/shop/brands/',
+ product.manufacture.name,
+ product.manufacture.id.toString()
+ )}
+ className=' hover:underline'
+ color={'red'}
+ >
+ {product.manufacture.name}
+ </Link>
+ ) : (
+ '-'
+ )}
+ <p className='text-ellipsis overflow-hidden'>{product.name}</p>
+ <p>{product.code}</p>
+ {!!product.lowest_price && product.lowest_price.price > 0 && (
+ <>
+ <div className='flex items-end gap-x-2'>
+ {product.lowest_price.discount_percentage > 0 && (
<>
- <div className='flex items-end gap-x-2'>
- {product.lowest_price.discount_percentage > 0 && (
- <>
- <div className='badge-solid-red'>
- {Math.floor(product.lowest_price.discount_percentage)}%
- </div>
- <div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'>
- Rp {formatCurrency(product.lowest_price.price || 0)}
- </div>
- </>
- )}
- <div className='text-danger-500 font-semibold'>
- Rp {formatCurrency(product.lowest_price.price_discount || 0)}
- </div>
+ <div className='badge-solid-red'>
+ {Math.floor(product.lowest_price.discount_percentage)}%
+ </div>
+ <div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'>
+ Rp {formatCurrency(product.lowest_price.price || 0)}
</div>
</>
)}
+ <div className='text-danger-500 font-semibold'>
+ Rp{' '}
+ {formatCurrency(product.lowest_price.price_discount || 0)}
+ </div>
+ </div>
+ </>
+ )}
- {!!product.lowest_price && product.lowest_price.price === 0 && (
- <span>
- Hubungi kami untuk dapatkan harga terbaik,{' '}
- <Link
- href={askAdminUrl}
- target='_blank'
- className='font-medium underline'
- color={'red'}
- >
- klik disini
- </Link>
- </span>
- )}
- </div>
- <div className='ml-3 flex items-center font-normal'>
- <Link
- href='/shop/cart'
- className='flex-1 py-2 text-gray_r-12 btn-yellow'
- >
- Lihat Keranjang
- </Link>
- </div>
+ {!!product.lowest_price && product.lowest_price.price === 0 && (
+ <span>
+ Hubungi kami untuk dapatkan harga terbaik,{' '}
+ <Link
+ href={askAdminUrl}
+ target='_blank'
+ className='font-medium underline'
+ color={'red'}
+ >
+ klik disini
+ </Link>
+ </span>
+ )}
+ </div>
+ <div className='ml-3 flex items-center font-normal'>
+ <Link
+ href='/shop/cart'
+ className='flex-1 py-2 text-gray_r-12 btn-yellow'
+ >
+ Lihat Keranjang
+ </Link>
</div>
- <div className='mt-8 mb-4'>
- <div className='text-h-sm font-semibold mb-6'>
- Kamu Mungkin Juga Suka
- </div>
- <LazyLoad>
- <ProductSimilar query={productSimilarQuery} />
- </LazyLoad>
+ </div>
+ <div className='mt-8 mb-4'>
+ <div className='text-h-sm font-semibold mb-6'>
+ Kamu Mungkin Juga Suka
</div>
- </BottomPopup>
+ <LazyLoad>
+ <ProductSimilar query={productSimilarQuery} />
+ </LazyLoad>
+ </div>
+ </BottomPopup>
</div>
- )
-}
+ );
+};
-export default AddToCart \ No newline at end of file
+export default AddToCart;
diff --git a/src-migrate/modules/product-detail/components/AddToQuotation.tsx b/src-migrate/modules/product-detail/components/AddToQuotation.tsx
new file mode 100644
index 00000000..f9b6c2b3
--- /dev/null
+++ b/src-migrate/modules/product-detail/components/AddToQuotation.tsx
@@ -0,0 +1,227 @@
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
+import style from '../styles/price-action.module.css';
+import { Button, Link, useToast } from '@chakra-ui/react';
+import { ArrowDownTrayIcon } from '@heroicons/react/24/outline';
+import product from 'next-seo/lib/jsonld/product';
+import { useRouter } from 'next/router';
+import { useEffect, useState } from 'react';
+import Image from '~/components/ui/image';
+import { getAuth } from '~/libs/auth';
+import { upsertUserCart } from '~/services/cart';
+import LazyLoad from 'react-lazy-load';
+import ProductSimilar from '../../../../src/lib/product/components/ProductSimilar';
+import { IProductDetail } from '~/types/product';
+import ImageNext from 'next/image';
+import { useProductCartContext } from '@/contexts/ProductCartContext';
+import { createSlug } from '~/libs/slug';
+import formatCurrency from '~/libs/formatCurrency';
+import { useProductDetail } from '../stores/useProductDetail';
+
+type Props = {
+ variantId: number | null;
+ quantity?: number;
+ source?: 'buy' | 'add_to_cart';
+ products: IProductDetail;
+};
+
+type Status = 'idle' | 'loading' | 'success';
+
+const AddToQuotation = ({
+ variantId,
+ quantity = 1,
+ source = 'add_to_cart',
+ products,
+}: Props) => {
+ const auth = getAuth();
+ const router = useRouter();
+ const toast = useToast({
+ position: 'top',
+ isClosable: true,
+ });
+
+ const { askAdminUrl } = useProductDetail();
+
+ const [product, setProducts] = useState(products);
+ const [status, setStatus] = useState<Status>('idle');
+ const {
+ productCart,
+ setRefreshCart,
+ setProductCart,
+ refreshCart,
+ isLoading,
+ setIsloading,
+ } = useProductCartContext();
+
+ const productSimilarQuery = [
+ product?.name,
+ `fq=-product_id_i:${product.id}`,
+ `fq=-manufacture_id_i:${product.manufacture?.id || 0}`,
+ ].join('&');
+ const [addCartAlert, setAddCartAlert] = useState(false);
+
+ const handleButton = async () => {
+ if (typeof auth !== 'object') {
+ const currentUrl = encodeURIComponent(router.asPath);
+ router.push(`/login?next=${currentUrl}`);
+ return;
+ }
+
+ if (!variantId || isNaN(quantity) || typeof auth !== 'object') return;
+ if (status === 'success') return;
+ setStatus('loading');
+ await upsertUserCart({
+ userId: auth.id,
+ type: 'product',
+ id: variantId,
+ qty: quantity,
+ selected: true,
+ source: source,
+ qtyAppend: true,
+ });
+ setStatus('idle');
+ setRefreshCart(true);
+ setAddCartAlert(true);
+
+ toast({
+ title: 'Tambah ke keranjang',
+ description: 'Berhasil menambahkan barang ke keranjang belanja',
+ status: 'success',
+ duration: 3000,
+ isClosable: true,
+ position: 'top',
+ });
+
+ if (source === 'buy') {
+ router.push('/shop/quotation?source=buy');
+ }
+ };
+ useEffect(() => {
+ if (status === 'success')
+ setTimeout(() => {
+ setStatus('idle');
+ }, 3000);
+ }, [status]);
+
+ const btnConfig = {
+ add_to_cart: {
+ colorScheme: 'yellow',
+
+ text: 'Keranjang',
+ },
+ buy: {
+ colorScheme: 'red',
+ text: 'Beli',
+ },
+ };
+
+ return (
+ <div className='w-full'>
+ <Button
+ onClick={handleButton}
+ color={'red'}
+ colorScheme='white'
+ className='w-full border-2 p-2 gap-1 hover:bg-slate-100 flex items-center'
+ >
+ <ImageNext
+ src='/images/writing.png'
+ alt='penawaran instan'
+ className=''
+ width={25}
+ height={25}
+ />
+ Penawaran Harga Instan
+ </Button>
+ <BottomPopup
+ className='!container'
+ title='Berhasil Ditambahkan'
+ active={addCartAlert}
+ close={() => {
+ setAddCartAlert(false);
+ }}
+ >
+ <div className='flex mt-4'>
+ <div className='w-[10%]'>
+ <ImageNext
+ src={product.image}
+ alt={product.name}
+ className='h-32 object-contain object-center w-full border border-gray_r-4'
+ width={80}
+ height={80}
+ />
+ </div>
+ <div className='ml-3 flex flex-1 items-start font-medium justify-center flex-col gap-y-1'>
+ {!!product.manufacture.name ? (
+ <Link
+ href={createSlug(
+ '/shop/brands/',
+ product.manufacture.name,
+ product.manufacture.id.toString()
+ )}
+ className=' hover:underline'
+ color={'red'}
+ >
+ {product.manufacture.name}
+ </Link>
+ ) : (
+ '-'
+ )}
+ <p className='text-ellipsis overflow-hidden'>{product.name}</p>
+ <p>{product.code}</p>
+ {!!product.lowest_price && product.lowest_price.price > 0 && (
+ <>
+ <div className='flex items-end gap-x-2'>
+ {product.lowest_price.discount_percentage > 0 && (
+ <>
+ <div className='badge-solid-red'>
+ {Math.floor(product.lowest_price.discount_percentage)}%
+ </div>
+ <div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'>
+ Rp {formatCurrency(product.lowest_price.price || 0)}
+ </div>
+ </>
+ )}
+ <div className='text-danger-500 font-semibold'>
+ Rp{' '}
+ {formatCurrency(product.lowest_price.price_discount || 0)}
+ </div>
+ </div>
+ </>
+ )}
+
+ {!!product.lowest_price && product.lowest_price.price === 0 && (
+ <span>
+ Hubungi kami untuk dapatkan harga terbaik,{' '}
+ <Link
+ href={askAdminUrl}
+ target='_blank'
+ className='font-medium underline'
+ color={'red'}
+ >
+ klik disini
+ </Link>
+ </span>
+ )}
+ </div>
+ <div className='ml-3 flex items-center font-normal'>
+ <Link
+ href='/shop/cart'
+ className='flex-1 py-2 text-gray_r-12 btn-yellow'
+ >
+ Lihat Keranjang
+ </Link>
+ </div>
+ </div>
+ <div className='mt-8 mb-4'>
+ <div className='text-h-sm font-semibold mb-6'>
+ Kamu Mungkin Juga Suka
+ </div>
+ <LazyLoad>
+ <ProductSimilar query={productSimilarQuery} />
+ </LazyLoad>
+ </div>
+ </BottomPopup>
+ </div>
+ );
+};
+
+export default AddToQuotation;
diff --git a/src-migrate/modules/product-detail/components/Image.tsx b/src-migrate/modules/product-detail/components/Image.tsx
index 30ca0d34..96ae2027 100644
--- a/src-migrate/modules/product-detail/components/Image.tsx
+++ b/src-migrate/modules/product-detail/components/Image.tsx
@@ -1,22 +1,22 @@
import style from '../styles/image.module.css';
import ImageNext from 'next/image';
-import React, { useEffect, useMemo, useState } from 'react'
-import { InfoIcon } from 'lucide-react'
-import { Tooltip } from '@chakra-ui/react'
+import React, { useEffect, useMemo, useState } from 'react';
+import { InfoIcon } from 'lucide-react';
+import { Tooltip } from '@chakra-ui/react';
-import { IProductDetail } from '~/types/product'
-import ImageUI from '~/components/ui/image'
+import { IProductDetail } from '~/types/product';
+import ImageUI from '~/components/ui/image';
import moment from 'moment';
-
+import useDevice from '@/core/hooks/useDevice';
type Props = {
- product: IProductDetail
-}
+ product: IProductDetail;
+};
const Image = ({ product }: Props) => {
- const flashSale = product.flash_sale
+ const flashSale = product.flash_sale;
const [count, setCount] = useState(flashSale?.remaining_time || 0);
-
+ const { isDesktop, isMobile } = useDevice();
useEffect(() => {
let interval: NodeJS.Timeout;
@@ -34,59 +34,60 @@ const Image = ({ product }: Props) => {
};
}, [flashSale?.remaining_time]);
- const duration = moment.duration(count, 'seconds')
-
+ const duration = moment.duration(count, 'seconds');
const image = useMemo(() => {
- if (product.image) return product.image + '?ratio=square'
- return '/images/noimage.jpeg'
- }, [product.image])
+ if (!isDesktop && product.image_mobile) {
+ return product.image_mobile + '?ratio=square';
+ } else {
+ if (product.image) return product.image + '?ratio=square';
+ return '/images/noimage.jpeg';
+ }
+ }, [product.image, product.image_mobile]);
return (
<div className={style['wrapper']}>
{/* <div className="relative"> */}
- <ImageUI
- src={image}
- alt={product.name}
- width={256}
- height={256}
- className={style['image']}
- loading='eager'
- priority
- />
- <div className="absolute top-4 right-10 flex ">
- <div className="gambarB ">
- {product.isSni && (
- <ImageNext
- src="/images/sni-logo.png"
- alt="SNI Logo"
- className="w-12 h-8 object-contain object-top sm:h-6"
- width={50}
- height={50}
- />
- )}
- </div>
- <div className="gambarC ">
- {product.isTkdn && (
- <ImageNext
- src="/images/TKDN.png"
- alt="TKDN"
- className="w-16 h-8 object-contain object-top ml-1 mr-1 sm:h-6"
- width={50}
- height={50}
- />
- )}
- </div>
- </div>
- {/* </div> */}
-
-
+ <ImageUI
+ src={image}
+ alt={product.name}
+ width={256}
+ height={256}
+ className={style['image']}
+ loading='eager'
+ priority
+ />
+ <div className='absolute top-4 right-10 flex '>
+ <div className='gambarB '>
+ {product.isSni && (
+ <ImageNext
+ src='/images/sni-logo.png'
+ alt='SNI Logo'
+ className='w-12 h-8 object-contain object-top sm:h-6'
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ <div className='gambarC '>
+ {product.isTkdn && (
+ <ImageNext
+ src='/images/TKDN.png'
+ alt='TKDN'
+ className='w-16 h-8 object-contain object-top ml-1 mr-1 sm:h-6'
+ width={50}
+ height={50}
+ />
+ )}
+ </div>
+ </div>
+ {/* </div> */}
<div className={style['absolute-info']}>
<Tooltip
placement='bottom-end'
label='Gambar atau foto berperan sebagai ilustrasi produk. Kadang tidak sesuai dengan kondisi terbaru dengan berbagai perubahan dan perbaikan. Hubungi admin kami untuk informasi yang lebih baik perihal gambar.'
>
- <div className="text-gray-600">
+ <div className='text-gray-600'>
<InfoIcon size={20} />
</div>
</Tooltip>
@@ -94,7 +95,7 @@ const Image = ({ product }: Props) => {
{flashSale.remaining_time > 0 && (
<div className='absolute bottom-0 w-full h-14'>
- <div className="relative w-full h-full">
+ <div className='relative w-full h-full'>
<ImageUI
src='/images/BG-FLASH-SALE.jpg'
alt='Flash Sale Indoteknik'
@@ -105,7 +106,9 @@ const Image = ({ product }: Props) => {
<div className={style['flashsale']}>
<div className='flex items-center gap-x-3'>
- <div className={style['disc-badge']}>{Math.floor(product.lowest_price.discount_percentage)}%</div>
+ <div className={style['disc-badge']}>
+ {Math.floor(product.lowest_price.discount_percentage)}%
+ </div>
<div className={style['flashsale-text']}>
<ImageUI
src='/images/ICON_FLASH_SALE_WEBSITE_INDOTEKNIK.svg'
@@ -122,12 +125,11 @@ const Image = ({ product }: Props) => {
<span>{duration.seconds().toString().padStart(2, '0')}</span>
</div>
</div>
-
</div>
</div>
)}
</div>
- )
-}
+ );
+};
-export default Image \ No newline at end of file
+export default Image;
diff --git a/src-migrate/modules/product-detail/components/Information.tsx b/src-migrate/modules/product-detail/components/Information.tsx
index 75ae3c41..5e1ea186 100644
--- a/src-migrate/modules/product-detail/components/Information.tsx
+++ b/src-migrate/modules/product-detail/components/Information.tsx
@@ -1,56 +1,232 @@
-import style from '../styles/information.module.css'
+import {
+ AutoComplete,
+ AutoCompleteInput,
+ AutoCompleteItem,
+ AutoCompleteList,
+} from '@choc-ui/chakra-autocomplete';
+import style from '../styles/information.module.css';
-import React from 'react'
-import dynamic from 'next/dynamic'
-import Link from 'next/link'
-import { useQuery } from 'react-query'
+import dynamic from 'next/dynamic';
+import Link from 'next/link';
+import { useEffect, useRef, useState } from 'react';
-import { IProductDetail } from '~/types/product'
-import { IProductVariantSLA } from '~/types/productVariant'
-import { createSlug } from '~/libs/slug'
-import { getVariantSLA } from '~/services/productVariant'
-import { formatToShortText } from '~/libs/formatNumber'
+import currencyFormat from '@/core/utils/currencyFormat';
+import { InputGroup, InputRightElement } from '@chakra-ui/react';
+import { ChevronDownIcon } from '@heroicons/react/24/outline';
+import Image from 'next/image';
+import { formatToShortText } from '~/libs/formatNumber';
+import { createSlug } from '~/libs/slug';
+import { getVariantSLA } from '~/services/productVariant';
+import { IProductDetail } from '~/types/product';
+import { useProductDetail } from '../stores/useProductDetail';
-const Skeleton = dynamic(() => import('@chakra-ui/react').then((mod) => mod.Skeleton))
+const Skeleton = dynamic(() =>
+ import('@chakra-ui/react').then((mod) => mod.Skeleton)
+);
type Props = {
- product: IProductDetail
-}
+ product: IProductDetail;
+};
const Information = ({ product }: Props) => {
- const querySLA = useQuery<IProductVariantSLA>({
- queryKey: ['variant-sla', product.variants[0]?.id],
- queryFn: () => getVariantSLA(product.variants[0].id),
- enabled: product.variant_total === 1
- })
+ const { selectedVariant, setSelectedVariant, setSla, setActive, sla } =
+ useProductDetail();
- const sla = querySLA?.data
+ const [inputValue, setInputValue] = useState<string | null>(
+ selectedVariant?.code + ' - ' + selectedVariant?.attributes[0]
+ );
+ const [disableFilter, setDisableFilter] = useState<boolean>(false);
+ const inputRef = useRef<HTMLInputElement>(null);
+
+ const [variantOptions, setVariantOptions] = useState<any[]>(
+ product?.variants
+ );
+ // let variantOptions = product?.variants;
+
+ // const querySLA = useQuery<IProductVariantSLA>({
+ // queryKey: ['variant-sla', selectedVariant?.id],
+ // queryFn: () => getVariantSLA(selectedVariant?.id),
+ // enabled: !!selectedVariant?.id,
+ // });
+ // const sla = querySLA?.data;
+
+ const getsla = async () => {
+ const querySLA = await getVariantSLA(selectedVariant?.id);
+ setSla(querySLA);
+ };
+
+ useEffect(() => {
+ if (selectedVariant) {
+ getsla();
+ setInputValue(
+ selectedVariant?.code +
+ (selectedVariant?.attributes[0]
+ ? ' - ' + selectedVariant?.attributes[0]
+ : '')
+ );
+ }
+ }, [selectedVariant]);
+
+ const handleOnChange = (vals: any) => {
+ setDisableFilter(true);
+ let code = vals.replace(/\s-\s.*$/, '').trim();
+ let variant = variantOptions.find((item) => item.code === code);
+ setSelectedVariant(variant);
+ setInputValue(
+ variant?.code +
+ (variant?.attributes[0] ? ' - ' + variant?.attributes[0] : '')
+ );
+ if (variant) {
+ const filteredOptions = product?.variants.filter(
+ (item) => item !== variant
+ );
+ const newOptions = [variant, ...filteredOptions];
+ setVariantOptions(newOptions);
+ }
+ };
+
+ const handleOnKeyUp = (e: any) => {
+ setDisableFilter(false);
+ setInputValue(e.target.value);
+ };
return (
<div className={style['wrapper']}>
+ <div className='realtive mb-5'>
+ <label className='form-label mb-2 text-lg text-red-600'>
+ Pilih Variant * :{' '}
+ <span className='text-gray_r-9 text-sm'>
+ {product?.variants?.length} Variants
+ </span>{' '}
+ </label>
+ <AutoComplete
+ disableFilter={disableFilter}
+ openOnFocus
+ className='form-input'
+ onChange={(vals) => handleOnChange(vals)}
+ >
+ <InputGroup>
+ <AutoCompleteInput
+ ref={inputRef}
+ value={inputValue as string}
+ onChange={(e) => handleOnKeyUp(e)}
+ onFocus={() => setDisableFilter(true)}
+ />
+ <InputRightElement className='mr-4'>
+ <ChevronDownIcon
+ className='h-6 w-6 text-gray-500'
+ onClick={() => inputRef?.current?.focus()}
+ />
+ </InputRightElement>
+ </InputGroup>
+
+ <AutoCompleteList>
+ {variantOptions.map((option, cid) => (
+ <AutoCompleteItem
+ key={`option-${cid}`}
+ value={
+ option.code +
+ (option?.attributes[0] ? ' - ' + option?.attributes[0] : '')
+ }
+ _selected={
+ option.id === selectedVariant?.id
+ ? {
+ bg: 'gray.300',
+ }
+ : undefined
+ }
+ textTransform='capitalize'
+ >
+ <div
+ key={cid}
+ className='flex gap-x-2 w-full justify-between px-3 items-center p-2'
+ >
+ <div className='text-small'>
+ {option.code +
+ (option?.attributes[0]
+ ? ' - ' + option?.attributes[0]
+ : '')}
+ </div>
+ <div
+ className={
+ option?.price?.discount_percentage
+ ? 'flex gap-x-4 items-center justify-between'
+ : ''
+ }
+ >
+ {option?.price?.discount_percentage > 0 && (
+ <>
+ <div className='badge-solid-red text-xs'>
+ {Math.floor(option?.price?.discount_percentage)}%
+ </div>
+ <div className='min-w-16 sm:min-w-24 text-gray_r-11 line-through text-[11px] sm:text-caption-2'>
+ {currencyFormat(option?.price?.price)}
+ </div>
+ </>
+ )}
+ <div className='min-w-20 sm:min-w-28 text-danger-500 font-semibold'>
+ {currencyFormat(option?.price?.price_discount)}
+ </div>
+ </div>
+ </div>
+ </AutoCompleteItem>
+ ))}
+ </AutoCompleteList>
+ </AutoComplete>
+ </div>
+
<div className={style['row']}>
- <div className={style['label']}>SKU Number</div>
- <div className={style['value']}>SKU-{product.id}</div>
+ <div className={style['label']}>Item Code</div>
+ <div className={style['value']}>{selectedVariant?.code}</div>
</div>
<div className={style['row']}>
<div className={style['label']}>Manufacture</div>
<div className={style['value']}>
{!!product.manufacture.name ? (
<Link
- href={createSlug('/shop/brands/', product.manufacture.name, product.manufacture.id.toString())}
- className='text-danger-500 hover:underline'
+ href={createSlug(
+ '/shop/brands/',
+ product.manufacture.name,
+ product.manufacture.id.toString()
+ )}
>
- {product.manufacture.name}
+ {product?.manufacture.logo ? (
+ <Image
+ height={50}
+ width={100}
+ src={product.manufacture.logo}
+ alt={product.manufacture.name}
+ className='h-8 object-fit'
+ />
+ ) : (
+ <p className='font-bold text-red-500'>
+ {product.manufacture.name}
+ </p>
+ )}
</Link>
- ) : '-'}
+ ) : (
+ '-'
+ )}
+ </div>
+ </div>
+ <div className={style['row']}>
+ <div className={style['label']}>Berat Barang</div>
+ <div className={style['value']}>
+ {selectedVariant?.weight > 0 ? `${selectedVariant?.weight} Kg` : '-'}
</div>
</div>
<div className={style['row']}>
<div className={style['label']}>Terjual</div>
- <div className={style['value']}>{product.qty_sold > 0 ? formatToShortText(product.qty_sold) : '-'}</div>
+ <div className={style['value']}>
+ {product.qty_sold > 0 ? formatToShortText(product.qty_sold) : '-'}
+ </div>
+ </div>
+ <div className={style['row']}>
+ <div className={style['label']}>Persiapan Barang</div>
+ <div className={style['value']}>{sla?.sla_date}</div>
</div>
</div>
- )
-}
+ );
+};
-export default Information \ No newline at end of file
+export default Information;
diff --git a/src-migrate/modules/product-detail/components/PriceAction.tsx b/src-migrate/modules/product-detail/components/PriceAction.tsx
index 9021264e..0b27b1b3 100644
--- a/src-migrate/modules/product-detail/components/PriceAction.tsx
+++ b/src-migrate/modules/product-detail/components/PriceAction.tsx
@@ -1,12 +1,17 @@
import style from '../styles/price-action.module.css';
-import React, { useEffect } from 'react';
+import Image from 'next/image';
+import Link from 'next/link';
+import { useEffect, useState } from 'react';
import formatCurrency from '~/libs/formatCurrency';
import { IProductDetail } from '~/types/product';
import { useProductDetail } from '../stores/useProductDetail';
import AddToCart from './AddToCart';
-import Link from 'next/link';
+import AddToQuotation from './AddToQuotation';
import { getAuth } from '~/libs/auth';
+import useDevice from '@/core/hooks/useDevice';
+import odooApi from '~/libs/odooApi';
+import { Button, Skeleton } from '@chakra-ui/react';
type Props = {
product: IProductDetail;
@@ -22,27 +27,58 @@ const PriceAction = ({ product }: Props) => {
askAdminUrl,
isApproval,
setIsApproval,
+ selectedVariant,
+ sla,
} = useProductDetail();
-
+ const [qtyPickUp, setQtyPickUp] = useState(0);
+ const { isDesktop, isMobile } = useDevice();
useEffect(() => {
- setActive(product.variants[0])
- if(product.variants.length > 2 && product.variants[0].price.price === 0){
- const variants = product.variants
+ setActive(selectedVariant);
+ if (product.variants.length > 2 && product.variants[0].price.price === 0) {
+ const variants = product.variants;
for (let i = 0; i < variants.length; i++) {
- if(variants[i].price.price > 0){
- setActive(variants[i])
+ if (variants[i].price.price > 0) {
+ setActive(variants[i]);
break;
}
}
}
-
- }, [product, setActive]);
+ }, [product, setActive, selectedVariant]);
+ useEffect(() => {
+ const fetchData = async () => {
+ const qty_available = await odooApi(
+ 'GET',
+ `/api/v1/product_variant/${selectedVariant.id}/qty_available`
+ );
+
+ setQtyPickUp(qty_available?.qty);
+ };
+ fetchData();
+ }, [selectedVariant]);
+ useEffect(() => {
+ setQuantityInput('1');
+ }, [selectedVariant]);
+
+ let voucherPastiHemat = 0;
+
+ if (
+ product?.voucher_pasti_hemat
+ ? product?.voucher_pasti_hemat.length
+ : voucherPastiHemat > 0
+ ) {
+ const stringVoucher = product?.voucher_pasti_hemat[0];
+ const validJsonString = stringVoucher.replace(/'/g, '"');
+ voucherPastiHemat = JSON.parse(validJsonString);
+ }
return (
<div
- className='block md:sticky top-[150px] bg-white py-0 md:py-6 z-10'
+ className={`block md:sticky md:top-[150px] md:py-6 fixed bottom-0 left-0 right-0 bg-white p-2 z-10 ${
+ isMobile &&
+ 'pb-6 pt-6 rounded-lg shadow-[rgba(0,0,4,0.1)_0px_-4px_4px_0px] '
+ }`}
id='price-section'
>
{!!activePrice && activePrice.price > 0 && (
@@ -84,18 +120,69 @@ const PriceAction = ({ product }: Props) => {
)}
<div className='h-4' />
+ <div className='flex gap-x-5 items-center'>
+ <div className='relative flex items-center'>
+ <button
+ type='button'
+ className='absolute left-0 px-2 py-1 h-full text-gray-500'
+ onClick={() =>
+ setQuantityInput(String(Math.max(1, Number(quantityInput) - 1)))
+ }
+ >
+ -
+ </button>
+ <input
+ type='number'
+ id='quantity'
+ min={1}
+ value={quantityInput}
+ onChange={(e) => setQuantityInput(e.target.value)}
+ className={style['quantity-input']}
+ />
+ <button
+ type='button'
+ className='absolute right-0 px-2 py-1 h-full text-gray-500'
+ onClick={() => setQuantityInput(String(Number(quantityInput) + 1))}
+ >
+ +
+ </button>
+ </div>
+
+ <div>
+ <Skeleton
+ isLoaded={sla}
+ h='21px'
+ // w={16}
+ className={sla?.qty < 10 ? 'text-red-600 font-medium' : ''}
+ >
+ Stock : {sla?.qty}{' '}
+ </Skeleton>
+ {/* <span className={sla?.qty < 10 ? 'text-red-600 font-medium' : ''}>
+ {' '}
+ </span> */}
+ </div>
+ <div>
+ {selectedVariant?.is_in_bu && (
+ <Link href='/panduan-pick-up-service' className='group'>
+ <Image
+ src='/images/PICKUP-NOW.png'
+ className='group-hover:scale-105 transition-transform duration-200'
+ alt='pickup now'
+ width={100}
+ height={12}
+ />
+ </Link>
+ )}
+ </div>
+ </div>
+ {qtyPickUp > 0 && (
+ <div className='text-[12px] mt-1 text-red-500 italic'>
+ * {qtyPickUp} barang bisa di pickup
+ </div>
+ )}
+ <div className='h-4' />
- <div className={style['action-wrapper']}>
- <label htmlFor='quantity' className='hidden'>
- Quantity
- </label>
- <input
- type='number'
- id='quantity'
- value={quantityInput}
- onChange={(e) => setQuantityInput(e.target.value)}
- className={style['quantity-input']}
- />
+ <div className={`${style['action-wrapper']}`}>
<AddToCart
products={product}
variantId={activeVariantId}
@@ -110,6 +197,14 @@ const PriceAction = ({ product }: Props) => {
/>
)}
</div>
+ <div className='mt-4'>
+ <AddToQuotation
+ source='buy'
+ products={product}
+ variantId={activeVariantId}
+ quantity={Number(quantityInput)}
+ />
+ </div>
</div>
);
};
diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx
index e4555913..b036cc2d 100644
--- a/src-migrate/modules/product-detail/components/ProductDetail.tsx
+++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx
@@ -1,46 +1,52 @@
-import style from '../styles/product-detail.module.css'
-
-import Link from 'next/link'
-import { useRouter } from 'next/router'
-import { useEffect } from 'react'
-
-import { Button } from '@chakra-ui/react'
-import { MessageCircleIcon, Share2Icon } from 'lucide-react'
-import { LazyLoadComponent } from 'react-lazy-load-image-component'
-import { RWebShare } from 'react-web-share'
-
-import useDevice from '@/core/hooks/useDevice'
-import { whatsappUrl } from '~/libs/whatsappUrl'
-import ProductPromoSection from '~/modules/product-promo/components/Section'
-import { IProductDetail } from '~/types/product'
-import { useProductDetail } from '../stores/useProductDetail'
-import AddToWishlist from './AddToWishlist'
-import Breadcrumb from './Breadcrumb'
-import ProductImage from './Image'
-import Information from './Information'
-import PriceAction from './PriceAction'
-import SimilarBottom from './SimilarBottom'
-import SimilarSide from './SimilarSide'
-import VariantList from './VariantList'
-import { getAuth } from '~/libs/auth'
-
-import { gtagProductDetail } from '@/core/utils/googleTag'
+import style from '../styles/product-detail.module.css';
+
+import Link from 'next/link';
+import { useRouter } from 'next/router';
+import { useEffect } from 'react';
+
+import { Button } from '@chakra-ui/react';
+import { MessageCircleIcon, Share2Icon } from 'lucide-react';
+import { LazyLoadComponent } from 'react-lazy-load-image-component';
+import { RWebShare } from 'react-web-share';
+
+import useDevice from '@/core/hooks/useDevice';
+import { getAuth } from '~/libs/auth';
+import { whatsappUrl } from '~/libs/whatsappUrl';
+import ProductPromoSection from '~/modules/product-promo/components/Section';
+import { IProductDetail } from '~/types/product';
+import { useProductDetail } from '../stores/useProductDetail';
+import AddToWishlist from './AddToWishlist';
+import Breadcrumb from './Breadcrumb';
+import ProductImage from './Image';
+import Information from './Information';
+import PriceAction from './PriceAction';
+import SimilarBottom from './SimilarBottom';
+import SimilarSide from './SimilarSide';
+
+import { gtagProductDetail } from '@/core/utils/googleTag';
type Props = {
- product: IProductDetail
-}
+ product: IProductDetail;
+};
-const SELF_HOST = process.env.NEXT_PUBLIC_SELF_HOST
+const SELF_HOST = process.env.NEXT_PUBLIC_SELF_HOST;
const ProductDetail = ({ product }: Props) => {
- const { isDesktop, isMobile } = useDevice()
- const router = useRouter()
- const auth = getAuth()
- const { setAskAdminUrl, askAdminUrl, activeVariantId, setIsApproval, isApproval } = useProductDetail()
+ const { isDesktop, isMobile } = useDevice();
+ const router = useRouter();
+ const auth = getAuth();
+ const {
+ setAskAdminUrl,
+ askAdminUrl,
+ activeVariantId,
+ setIsApproval,
+ isApproval,
+ setSelectedVariant,
+ } = useProductDetail();
useEffect(() => {
gtagProductDetail(product);
- },[product])
+ }, [product]);
useEffect(() => {
const createdAskUrl = whatsappUrl({
@@ -48,76 +54,43 @@ const ProductDetail = ({ product }: Props) => {
payload: {
manufacture: product.manufacture.name,
productName: product.name,
- url: process.env.NEXT_PUBLIC_SELF_HOST + router.asPath
+ url: process.env.NEXT_PUBLIC_SELF_HOST + router.asPath,
},
- fallbackUrl: router.asPath
- })
+ fallbackUrl: router.asPath,
+ });
- setAskAdminUrl(createdAskUrl)
- }, [router.asPath, product.manufacture.name, product.name, setAskAdminUrl])
+ setAskAdminUrl(createdAskUrl);
+ }, [router.asPath, product.manufacture.name, product.name, setAskAdminUrl]);
useEffect(() => {
if (typeof auth === 'object') {
setIsApproval(auth?.feature?.soApproval);
}
+ setSelectedVariant(product?.variants[0])
}, []);
return (
<>
<div className='md:flex md:flex-wrap'>
- <div className="w-full mb-4 md:mb-0 px-4 md:px-0">
+ <div className='w-full mb-4 md:mb-0 px-4 md:px-0'>
<Breadcrumb id={product.id} name={product.name} />
</div>
<div className='md:w-9/12 md:flex md:flex-col md:pr-4 md:pt-6'>
<div className='md:flex md:flex-wrap'>
- <div className="md:w-4/12">
+ <div className='md:w-4/12'>
<ProductImage product={product} />
</div>
<div className='md:w-8/12 px-4 md:pl-6'>
<div className='h-6 md:h-0' />
- <h1 className={style['title']}>
- {product.name}
- </h1>
+ <h1 className={style['title']}>{product.name}</h1>
- <div className='h-6 md:h-8' />
+ <div className='h-3 md:h-0' />
<Information product={product} />
<div className='h-6' />
-
- <div className="flex gap-x-5">
- <Button
- as={Link}
- href={askAdminUrl}
- variant='link'
- target='_blank'
- colorScheme='gray'
- leftIcon={<MessageCircleIcon size={18} />}
- >
- Ask Admin
- </Button>
-
- <AddToWishlist productId={product.id} />
-
- <RWebShare
- data={{
- text: 'Check out this product',
- title: `${product.name} - Indoteknik.com`,
- url: SELF_HOST + router.asPath
- }}
- >
- <Button
- variant='link'
- colorScheme='gray'
- leftIcon={<Share2Icon size={18} />}
- >
- Share
- </Button>
- </RWebShare>
- </div>
-
</div>
</div>
@@ -131,38 +104,72 @@ const ProductDetail = ({ product }: Props) => {
<div className='h-4 md:h-10' />
{!!activeVariantId && !isApproval && <ProductPromoSection product={product} productId={activeVariantId} />}
- <div className={style['section-card']}>
+ {/* <div className={style['section-card']}>
<h2 className={style['heading']}>
Variant ({product.variant_total})
</h2>
<div className='h-4' />
<VariantList variants={product.variants} />
- </div>
+ </div> */}
<div className='h-0 md:h-6' />
<div className={style['section-card']}>
- <h2 className={style['heading']}>
- Informasi Produk
- </h2>
+ <h2 className={style['heading']}>Informasi Produk</h2>
<div className='h-4' />
<div
className={style['description']}
- dangerouslySetInnerHTML={{ __html: !product.description || product.description == '<p><br></p>' ? 'Belum ada deskripsi' : product.description }}
+ dangerouslySetInnerHTML={{
+ __html:
+ !product.description || product.description == '<p><br></p>'
+ ? 'Belum ada deskripsi'
+ : product.description,
+ }}
/>
</div>
</div>
</div>
{isDesktop && (
- <div className="md:w-3/12">
+ <div className='md:w-3/12'>
<PriceAction product={product} />
+ <div className='flex gap-x-5 items-center justify-center'>
+ <Button
+ as={Link}
+ href={askAdminUrl}
+ variant='link'
+ target='_blank'
+ colorScheme='gray'
+ leftIcon={<MessageCircleIcon size={18} />}
+ >
+ Ask Admin
+ </Button>
+
+ <span>|</span>
+
+ <AddToWishlist productId={product.id} />
+
+ <span>|</span>
+
+ <RWebShare
+ data={{
+ text: 'Check out this product',
+ title: `${product.name} - Indoteknik.com`,
+ url: SELF_HOST + router.asPath,
+ }}
+ >
+ <Button
+ variant='link'
+ colorScheme='gray'
+ leftIcon={<Share2Icon size={18} />}
+ >
+ Share
+ </Button>
+ </RWebShare>
+ </div>
<div className='h-6' />
-
- <div className={style['heading']}>
- Produk Serupa
- </div>
+ <div className={style['heading']}>Produk Serupa</div>
<div className='h-4' />
@@ -171,9 +178,7 @@ const ProductDetail = ({ product }: Props) => {
)}
<div className='md:w-full pt-4 md:py-10 px-4 md:px-0'>
- <div className={style['heading']}>
- Kamu Mungkin Juga Suka
- </div>
+ <div className={style['heading']}>Kamu Mungkin Juga Suka</div>
<div className='h-6' />
@@ -185,7 +190,7 @@ const ProductDetail = ({ product }: Props) => {
<div className='h-6 md:h-0' />
</div>
</>
- )
-}
+ );
+};
-export default ProductDetail \ No newline at end of file
+export default ProductDetail;
diff --git a/src-migrate/modules/product-detail/stores/useProductDetail.ts b/src-migrate/modules/product-detail/stores/useProductDetail.ts
index eb409930..dee6b342 100644
--- a/src-migrate/modules/product-detail/stores/useProductDetail.ts
+++ b/src-migrate/modules/product-detail/stores/useProductDetail.ts
@@ -7,6 +7,8 @@ type State = {
quantityInput: string;
askAdminUrl: string;
isApproval : boolean;
+ selectedVariant : any;
+ sla : any;
};
type Action = {
@@ -14,6 +16,8 @@ type Action = {
setQuantityInput: (value: string) => void;
setAskAdminUrl: (url: string) => void;
setIsApproval : (value : boolean) => void;
+ setSelectedVariant : (value : any) => void;
+ setSla : (value : any) => void;
};
export const useProductDetail = create<State & Action>((set, get) => ({
@@ -22,6 +26,8 @@ export const useProductDetail = create<State & Action>((set, get) => ({
quantityInput: '1',
askAdminUrl: '',
isApproval : false,
+ selectedVariant: null,
+ sla : null,
setActive: (variant) => {
set({ activeVariantId: variant?.id, activePrice: variant?.price });
},
@@ -33,5 +39,11 @@ export const useProductDetail = create<State & Action>((set, get) => ({
},
setIsApproval : (value : boolean) => {
set({ isApproval : value })
+ },
+ setSelectedVariant : (value : any) => {
+ set({ selectedVariant : value })
+ },
+ setSla : (value : any ) => {
+ set({ sla : value })
}
}));
diff --git a/src-migrate/modules/product-detail/styles/information.module.css b/src-migrate/modules/product-detail/styles/information.module.css
index c9b29020..5aa64fe5 100644
--- a/src-migrate/modules/product-detail/styles/information.module.css
+++ b/src-migrate/modules/product-detail/styles/information.module.css
@@ -3,11 +3,11 @@
}
.row {
- @apply flex p-3 rounded;
+ @apply flex p-4 rounded-sm bg-gray-100;
}
.row:nth-child(odd) {
- @apply bg-gray-100;
+ @apply bg-white;
}
.label {
diff --git a/src-migrate/modules/product-detail/styles/price-action.module.css b/src-migrate/modules/product-detail/styles/price-action.module.css
index 651de958..cea50bff 100644
--- a/src-migrate/modules/product-detail/styles/price-action.module.css
+++ b/src-migrate/modules/product-detail/styles/price-action.module.css
@@ -8,7 +8,10 @@
@apply flex gap-x-2.5;
}
.quantity-input {
- @apply px-2 rounded text-center border border-gray-300 w-14 h-10 focus:outline-none;
+ @apply w-24 h-10 text-center border border-gray-300 rounded focus:outline-none;
+ /* Padding di kiri dan kanan untuk memberi ruang bagi tombol */
+ padding-left: 2rem;
+ padding-right: 2rem;
}
.contact-us {
diff --git a/src-migrate/modules/promo/components/FlashSaleNonDisplay.tsx b/src-migrate/modules/promo/components/FlashSaleNonDisplay.tsx
new file mode 100644
index 00000000..5685b83a
--- /dev/null
+++ b/src-migrate/modules/promo/components/FlashSaleNonDisplay.tsx
@@ -0,0 +1,17 @@
+import dynamic from 'next/dynamic';
+import React from 'react';
+import { FlashSaleSkeleton } from '@/lib/flashSale/skeleton/FlashSaleSkeleton';
+const FlashSaleNonDisplay = dynamic(
+ () => import('@/lib/flashSale/components/FlashSaleNonDisplay'),
+ {
+ loading: () => <FlashSaleSkeleton />,
+ }
+);
+const FlashSalePromo = () => {
+ return (
+ <>
+ <FlashSaleNonDisplay />
+ </>
+ );
+};
+export default FlashSalePromo;
diff --git a/src-migrate/modules/promo/components/PromoList.tsx b/src-migrate/modules/promo/components/PromoList.tsx
index d59d1867..9f808718 100644
--- a/src-migrate/modules/promo/components/PromoList.tsx
+++ b/src-migrate/modules/promo/components/PromoList.tsx
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
-import { Button, Skeleton } from '@chakra-ui/react'
-import clsxm from "~/libs/clsxm"
+import { Button, Skeleton } from '@chakra-ui/react';
+import clsxm from '~/libs/clsxm';
import ProductPromoCard from '../../product-promo/components/Card';
import { fetchPromoItemsSolr } from '../../../../src/api/promoApi';
import { Swiper, SwiperSlide } from 'swiper/react';
@@ -8,7 +8,7 @@ import SwiperCore, { Navigation, Pagination } from 'swiper';
import useDevice from '@/core/hooks/useDevice';
import LogoSpinner from '../../../../src/core/components/elements/Spinner/LogoSpinner';
import usePromoStore from './promoStore';
-import Link from "next/link"
+import Link from 'next/link';
import { IPromotion } from '~/types/promotion';
interface PromoListProps {
selectedPromo: string; // Tipe selectedPromo ditetapkan sebagai string
@@ -32,11 +32,11 @@ const PromoList: React.FC<PromoListProps> = ({ selectedPromo }) => {
const swiperBanner = {
modules: [Navigation],
- className: 'h-[400px] w-full',
+ className: 'h-full w-full',
slidesPerView: isMobile ? 1.1 : 3.25,
spaceBetween: 10,
- navigation:isMobile? true : false,
- allowTouchMove:isMobile? false : true,
+ navigation: isMobile ? true : false,
+ allowTouchMove: isMobile ? false : true,
};
useEffect(() => {
@@ -56,7 +56,7 @@ const PromoList: React.FC<PromoListProps> = ({ selectedPromo }) => {
const fetchPromotions = async () => {
setIsLoading(true);
try {
- const items = await fetchPromoItemsSolr(`type_value_s:${slug}`, 0, 10);
+ const items = await fetchPromoItemsSolr(`type_value_s:${slug}`, 0, 10);
setPromoItems(items);
const promoDataPromises = items?.map(async (item) => {
@@ -69,9 +69,11 @@ const PromoList: React.FC<PromoListProps> = ({ selectedPromo }) => {
});
const promoDataArray = await Promise.all(promoDataPromises);
- const mergedPromoData = promoDataArray?.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []);
+ const mergedPromoData = promoDataArray?.reduce(
+ (accumulator, currentValue) => accumulator.concat(currentValue),
+ []
+ );
setPromoData(mergedPromoData);
-
} catch (error) {
console.error('Error fetching promo items:', error);
} finally {
@@ -92,44 +94,49 @@ const PromoList: React.FC<PromoListProps> = ({ selectedPromo }) => {
<div className='flex justify-between items-center'>
<h1 className='text-h-sm md:text-h-lg font-semibold py-4'>{title}</h1>
<div>
- <Link href={`/shop/promo/${slug}`} className='!text-red-500 font-semibold'>
+ <Link
+ href={`/shop/promo/${slug}`}
+ className='!text-red-500 font-semibold'
+ >
Lihat Semua
</Link>
</div>
</div>
{isLoading ? (
- <div className="loading-spinner flex justify-center">
+ <div className='loading-spinner flex justify-center'>
<LogoSpinner width={48} height={48} />
</div>
) : (
<Skeleton
- isLoaded={!isLoading}
- className={clsxm(
- "flex gap-x-4 overflow-x-auto px-4 md:px-0", {
- "min-h-[340px]": promoData[0] && promoData?.length > 0
- })}
- >
- {isDesktop && (
- <Swiper {...swiperBanner}>
- {promoData?.map((promotion: IPromotion) => (
- <SwiperSlide key={promotion.id}>
- <div className="min-w-36 max-w-[400px] mb-[20px] sm:w-full md:w-full lg:w-full xl:w-full">
- <ProductPromoCard product={promoItems} promotion={promotion} />
- </div>
- </SwiperSlide>
- ))}
- </Swiper>
- )}
- {isMobile && (promoData?.map((promotion: IPromotion) => (
- <div key={promotion.id} className="min-w-[400px] max-w-[400px]">
- <ProductPromoCard product={promoItems} promotion={promotion} />
- </div>
- )))}
-
- </Skeleton>
+ isLoaded={!isLoading}
+ className={clsxm('flex gap-x-4 overflow-x-auto px-4 md:px-0', {
+ 'min-h-[340px]': promoData[0] && promoData?.length > 0,
+ })}
+ >
+ {isDesktop && (
+ <Swiper {...swiperBanner}>
+ {promoData?.map((promotion: IPromotion) => (
+ <SwiperSlide key={promotion.id}>
+ <div className='min-w-36 max-w-[400px] mb-[20px] sm:w-full md:w-full lg:w-full xl:w-full'>
+ <ProductPromoCard
+ product={promoItems}
+ promotion={promotion}
+ />
+ </div>
+ </SwiperSlide>
+ ))}
+ </Swiper>
+ )}
+ {isMobile &&
+ promoData?.map((promotion: IPromotion) => (
+ <div key={promotion.id} className='min-w-[400px] max-w-[400px]'>
+ <ProductPromoCard product={promoItems} promotion={promotion} />
+ </div>
+ ))}
+ </Skeleton>
)}
</div>
);
};
-export default PromoList; \ No newline at end of file
+export default PromoList;
diff --git a/src-migrate/modules/register/components/TermCondition.tsx b/src-migrate/modules/register/components/TermCondition.tsx
index d54fe921..44275917 100644
--- a/src-migrate/modules/register/components/TermCondition.tsx
+++ b/src-migrate/modules/register/components/TermCondition.tsx
@@ -1,34 +1,48 @@
-import { Checkbox } from '@chakra-ui/react'
-import React from 'react'
-import { Modal } from '~/components/ui/modal'
-import { useRegisterStore } from "../stores/useRegisterStore";
-import PageContent from '~/modules/page-content'
+import { Checkbox } from '@chakra-ui/react';
+import React from 'react';
+import { Modal } from '~/components/ui/modal';
+import { useRegisterStore } from '../stores/useRegisterStore';
+
+import dynamic from 'next/dynamic';
+const PageContent = dynamic(
+ () => import('@/lib/content/components/PageContent')
+);
const TermCondition = () => {
- const { isOpenTNC, closeTNC, isCheckedTNC, toggleCheckTNC, openTNC } = useRegisterStore()
+ const { isOpenTNC, closeTNC, isCheckedTNC, toggleCheckTNC, openTNC } =
+ useRegisterStore();
return (
<>
- <div className="mt-4 flex items-center gap-x-2">
- <Checkbox id='tnc' name='tnc' colorScheme='red' isChecked={isCheckedTNC} onChange={toggleCheckTNC} />
+ <div className='mt-4 flex items-center gap-x-2'>
+ <Checkbox
+ id='tnc'
+ name='tnc'
+ colorScheme='red'
+ isChecked={isCheckedTNC}
+ onChange={toggleCheckTNC}
+ />
<div>
- <label htmlFor="tnc" className="cursor-pointer">Dengan ini saya menyetujui</label>
- {' '}
+ <label htmlFor='tnc' className='cursor-pointer'>
+ Dengan ini saya menyetujui
+ </label>{' '}
<span
- className="font-medium text-danger-500 cursor-pointer"
+ className='font-medium text-danger-500 cursor-pointer'
onClick={openTNC}
>
syarat dan ketentuan
</span>
- <label htmlFor="tnc" className="ml-2 cursor-pointer">yang berlaku</label>
+ <label htmlFor='tnc' className='ml-2 cursor-pointer'>
+ yang berlaku
+ </label>
</div>
</div>
- <Modal active={isOpenTNC} close={closeTNC} >
- <PageContent path='/register#tnd' />
+ <Modal active={isOpenTNC} close={closeTNC}>
+ <PageContent path='/registerTnd' />
</Modal>
</>
- )
-}
+ );
+};
-export default TermCondition \ No newline at end of file
+export default TermCondition;