diff options
Diffstat (limited to 'src-migrate/modules')
25 files changed, 1936 insertions, 457 deletions
diff --git a/src-migrate/modules/cart/components/Item.tsx b/src-migrate/modules/cart/components/Item.tsx index 47893498..6ffbb524 100644 --- a/src-migrate/modules/cart/components/Item.tsx +++ b/src-migrate/modules/cart/components/Item.tsx @@ -17,11 +17,11 @@ import CartItemSelect from './ItemSelect' type Props = { item: CartItemProps editable?: boolean + selfPicking?: boolean pilihSemuaCart?: boolean } -const CartItem = ({ item, editable = true,}: Props) => { - +const CartItem = ({ item, editable = true, selfPicking}: Props) => { return ( <div className={style.wrapper}> {item.cart_type === 'promotion' && ( @@ -53,6 +53,11 @@ const CartItem = ({ item, editable = true,}: Props) => { <CartItem.Image item={item} /> <div className={style.details}> + {(item.is_in_bu) && (item.on_hand_qty >= item.quantity) && ( + <div className='text-[10px] text-red-500 italic'> + *Barang ini bisa di pickup maksimal pukul 16.00 + </div> + )} <CartItem.Name item={item} /> <div className='mt-2 flex justify-between w-full'> diff --git a/src-migrate/modules/cart/components/ItemAction.tsx b/src-migrate/modules/cart/components/ItemAction.tsx index e5e7f314..7220e362 100644 --- a/src-migrate/modules/cart/components/ItemAction.tsx +++ b/src-migrate/modules/cart/components/ItemAction.tsx @@ -13,7 +13,6 @@ import { useDebounce } from 'usehooks-ts' import { useCartStore } from '../stores/useCartStore' import { useProductCartContext } from '@/contexts/ProductCartContext' - type Props = { item: CartItem } diff --git a/src-migrate/modules/cart/stores/useCartStore.ts b/src-migrate/modules/cart/stores/useCartStore.ts index 3b50ec32..c2ebf50f 100644 --- a/src-migrate/modules/cart/stores/useCartStore.ts +++ b/src-migrate/modules/cart/stores/useCartStore.ts @@ -54,7 +54,7 @@ export const useCartStore = create<State & Action>((set, get) => ({ const computeSummary = (cart: CartProps) => { let subtotal = 0; let discount = 0; - for (const item of cart.products) { + for (const item of cart?.products) { if (!item.selected) continue; let price = 0; @@ -71,4 +71,4 @@ const computeSummary = (cart: CartProps) => { let grandTotal = total + tax; return { subtotal, discount, total, tax, grandTotal }; -}; +};
\ No newline at end of file diff --git a/src-migrate/modules/page-content/index.tsx b/src-migrate/modules/page-content/index.tsx index 547b1957..af597641 100644 --- a/src-migrate/modules/page-content/index.tsx +++ b/src-migrate/modules/page-content/index.tsx @@ -1,44 +1,45 @@ -import { useMemo } from "react" -import { useQuery } from "react-query" -import { PageContentProps } from "~/types/pageContent" -import { getPageContent } from "~/services/pageContent" +import { useMemo } from 'react'; +import { useQuery } from 'react-query'; +import { PageContentProps } from '~/types/pageContent'; +import { getPageContent } from '~/services/pageContent'; type Props = { - path: string -} + path: string; +}; const PageContent = ({ path }: Props) => { - const { data, isLoading } = useQuery<PageContentProps>(`page-content:${path}`, async () => await getPageContent({ path })) + const { data, isLoading } = useQuery<PageContentProps>( + `page-content:${path}`, + async () => await getPageContent({ path }) + ); const parsedContent = useMemo<string>(() => { - if (!data) return '' + if (!data) return ''; return data.content.replaceAll( 'src="/web/image', `src="${process.env.NEXT_PUBLIC_ODOO_API_HOST}/web/image` - ) - }, [data]) + ); + }, [data]); + console.log('parsedContent', parsedContent); + if (isLoading) return <PageContentSkeleton />; - if (isLoading) return <PageContentSkeleton /> - - return ( - <div dangerouslySetInnerHTML={{ __html: parsedContent || '' }}></div> - ) -} + return <div dangerouslySetInnerHTML={{ __html: parsedContent || '' }}></div>; +}; const PageContentSkeleton = () => ( - <div className="animate-pulse grid gap-y-4"> - <div className="w-full h-10 bg-gray-300 rounded" /> - <div className="h-2" /> - <div className="w-full h-4 bg-gray-300 rounded" /> - <div className="w-full h-4 bg-gray-300 rounded" /> - <div className="w-full h-4 bg-gray-300 rounded" /> - <div className="w-8/12 h-4 bg-gray-300 rounded" /> - <div className="h-2" /> - <div className="w-full h-4 bg-gray-300 rounded" /> - <div className="w-full h-4 bg-gray-300 rounded" /> - <div className="w-full h-4 bg-gray-300 rounded" /> - <div className="w-1/2 h-4 bg-gray-300 rounded" /> + <div className='animate-pulse grid gap-y-4'> + <div className='w-full h-10 bg-gray-300 rounded' /> + <div className='h-2' /> + <div className='w-full h-4 bg-gray-300 rounded' /> + <div className='w-full h-4 bg-gray-300 rounded' /> + <div className='w-full h-4 bg-gray-300 rounded' /> + <div className='w-8/12 h-4 bg-gray-300 rounded' /> + <div className='h-2' /> + <div className='w-full h-4 bg-gray-300 rounded' /> + <div className='w-full h-4 bg-gray-300 rounded' /> + <div className='w-full h-4 bg-gray-300 rounded' /> + <div className='w-1/2 h-4 bg-gray-300 rounded' /> </div> -) +); -export default PageContent
\ No newline at end of file +export default PageContent; diff --git a/src-migrate/modules/product-detail/components/AddToCart.tsx b/src-migrate/modules/product-detail/components/AddToCart.tsx index 097db98a..a5284637 100644 --- a/src-migrate/modules/product-detail/components/AddToCart.tsx +++ b/src-migrate/modules/product-detail/components/AddToCart.tsx @@ -1,19 +1,35 @@ -import { Button, useToast } from '@chakra-ui/react' +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 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 AddToCart = ({ variantId, quantity = 1, - source = 'add_to_cart' + source = 'add_to_cart', + products }: Props) => { const auth = getAuth() const router = useRouter() @@ -22,40 +38,65 @@ const AddToCart = ({ isClosable: true }) - const handleClick = async () => { + 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; - - toast.promise( - upsertUserCart({ - userId: auth.id, + if (status === 'success') return + setStatus('loading') + await upsertUserCart({ + userId: auth.id, type: 'product', id: variantId, qty: quantity, selected: true, source: source, qtyAppend: true - }), - { - loading: { title: 'Menambahkan ke keranjang', description: 'Mohon tunggu...' }, - success: { title: 'Menambahkan ke keranjang', description: 'Berhasil menambahkan ke keranjang belanja' }, - error: { title: 'Menambahkan ke keranjang', description: 'Gagal menambahkan ke keranjang belanja' }, - } - ) - + }) + 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/checkout?source=buy') } } + useEffect(() => { + if (status === 'success') setTimeout(() => { setStatus('idle') }, 3000) + }, [status]) const btnConfig = { 'add_to_cart': { @@ -69,10 +110,98 @@ const AddToCart = ({ } return ( - <Button onClick={handleClick} colorScheme={btnConfig[source].colorScheme} className='w-full'> - {btnConfig[source].text} - </Button> + <div 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 && ( + <> + <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 AddToCart
\ No newline at end of file +export default AddToCart
\ No newline at end of file diff --git a/src-migrate/modules/product-detail/components/PriceAction.tsx b/src-migrate/modules/product-detail/components/PriceAction.tsx index 81271f6e..9021264e 100644 --- a/src-migrate/modules/product-detail/components/PriceAction.tsx +++ b/src-migrate/modules/product-detail/components/PriceAction.tsx @@ -97,12 +97,14 @@ const PriceAction = ({ product }: Props) => { className={style['quantity-input']} /> <AddToCart + products={product} variantId={activeVariantId} quantity={Number(quantityInput)} /> {!isApproval && ( <AddToCart source='buy' + products={product} variantId={activeVariantId} quantity={Number(quantityInput)} /> diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx index fad35a7d..e4555913 100644 --- a/src-migrate/modules/product-detail/components/ProductDetail.tsx +++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx @@ -129,7 +129,7 @@ const ProductDetail = ({ product }: Props) => { )} <div className='h-4 md:h-10' /> - {!!activeVariantId && !isApproval && <ProductPromoSection productId={activeVariantId} />} + {!!activeVariantId && !isApproval && <ProductPromoSection product={product} productId={activeVariantId} />} <div className={style['section-card']}> <h2 className={style['heading']}> diff --git a/src-migrate/modules/product-promo/components/AddToCart.tsx b/src-migrate/modules/product-promo/components/AddToCart.tsx index 87017c14..7b3863f9 100644 --- a/src-migrate/modules/product-promo/components/AddToCart.tsx +++ b/src-migrate/modules/product-promo/components/AddToCart.tsx @@ -5,53 +5,71 @@ import { useEffect, useState } from 'react' import { getAuth } from '~/libs/auth' import { upsertUserCart } from '~/services/cart' -import { IPromotion } from '~/types/promotion' +import { IPromotion, IProductVariantPromo } from '~/types/promotion' import DesktopView from '../../../../src/core/components/views/DesktopView'; import MobileView from '../../../../src/core/components/views/MobileView'; - +import BottomPopup from '@/core/components/elements/Popup/BottomPopup' +import ImageNext from 'next/image'; +import Link from 'next/link' +import LazyLoad from 'react-lazy-load' +import ProductSimilar from '../../../../src/lib/product/components/ProductSimilar'; +import { IProductDetail } from '~/types/product'; +import { useProductCartContext } from '@/contexts/ProductCartContext' +import { createSlug } from '~/libs/slug' +import formatCurrency from '~/libs/formatCurrency' +import { useProductDetail } from '../../product-detail/stores/useProductDetail'; type Props = { promotion: IPromotion + product: IProductDetail + variant: IProductVariantPromo, } type Status = 'idle' | 'loading' | 'success' -const ProductPromoAddToCart = ({ promotion }: Props) => { +const ProductPromoAddToCart = ({product, promotion, variant }: Props) => { const auth = getAuth() const toast = useToast() const router = useRouter() - + const {askAdminUrl} = useProductDetail(); const [status, setStatus] = useState<Status>('idle') - + const { productCart, setRefreshCart, setProductCart, refreshCart, isLoading, setIsloading } = + useProductCartContext() + const productSimilarQuery = [ + promotion?.name, + `fq=-product_id_i:${promotion.products[0].product_id}`, + ].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 (status === 'success') return - - setStatus('loading') - await upsertUserCart({ - userId: auth.id, - type: 'promotion', - id: promotion.id, - qty: 1, - selected: true, - source: 'add_to_cart', - qtyAppend: true - }) - setStatus('idle') - - toast({ - title: 'Tambah ke keranjang', - description: 'Berhasil menambahkan barang ke keranjang belanja', - status: 'success', - duration: 3000, - isClosable: true, - position: 'top', - }) + const currentUrl = encodeURIComponent(router.asPath) + router.push(`/login?next=${currentUrl}`) + return } + if (status === 'success') return + setStatus('loading') + await upsertUserCart({ + userId: auth.id, + type: 'promotion', + id: promotion.id, + qty: 1, + selected: true, + source: 'add_to_cart', + 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', + }) +} useEffect(() => { if (status === 'success') setTimeout(() => { setStatus('idle') }, 3000) @@ -92,6 +110,97 @@ const ProductPromoAddToCart = ({ promotion }: Props) => { {status === 'success' && <span>Berhasil</span>} {status !== 'success' && <span>Keranjang</span>} </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 + ? product?.image + : variant?.image || '' + } + alt={product?.name ? product?.name : variant?.display_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 || variant?.manufacture && ( + <Link + href={createSlug('/shop/brands/', product?.manufacture?.name? product?.manufacture?.name : variant?.manufacture?.manufacture_name, product?.manufacture?.id? product?.manufacture?.id.toString() : variant?.manufacture?.manufacture_id.toString())} + className=' hover:underline text-red-500' + color={"red"} + > + {product?.manufacture?.name ? product?.manufacture?.name : variant?.manufacture?.manufacture_name} + </Link> + )} + <p className='text-ellipsis overflow-hidden'> + {product?.name ? product?.name : variant?.name} + </p> + <p> + {product?.code} + </p> + {(!!product?.lowest_price && product?.lowest_price?.price > 0) || variant?.price?.price > 0 && ( + <> + <div className='flex items-end gap-x-2'> + {(product?.lowest_price?.discount_percentage > 0) || variant?.price?.discount_percentage > 0 && ( + <> + <div className='badge-solid-red'> + {Math.floor(product?.lowest_price?.discount_percentage ? product?.lowest_price?.discount_percentage : variant?.price?.discount_percentage)}% + </div> + <div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'> + Rp {formatCurrency(product?.lowest_price?.price ? product?.lowest_price?.price || 0 : variant?.price?.price || 0)} + </div> + </> + )} + <div className='text-danger-500 font-semibold'> + Rp {formatCurrency(product?.lowest_price?.price_discount? product?.lowest_price?.price_discount || 0 : variant?.price?.price_discount)} + </div> + </div> + </> + )} + + {(!!product?.lowest_price && product?.lowest_price?.price === 0) || variant?.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> </DesktopView> </div> ) diff --git a/src-migrate/modules/product-promo/components/Card.tsx b/src-migrate/modules/product-promo/components/Card.tsx index 728d23ca..b8abe5ec 100644 --- a/src-migrate/modules/product-promo/components/Card.tsx +++ b/src-migrate/modules/product-promo/components/Card.tsx @@ -15,16 +15,16 @@ import clsxm from '~/libs/clsxm' import ProductPromoItem from './Item' import ProductPromoAddToCart from "./AddToCart" import ProductPromoCardCountdown from "./CardCountdown" - +import { IProductDetail } from '~/types/product'; import MobileView from '../../../../src/core/components/views/MobileView'; import DesktopView from '../../../../src/core/components/views/DesktopView'; type Props = { promotion: IPromotion - + product: IProductDetail } -const ProductPromoCard = ({ promotion}: Props) => { +const ProductPromoCard = ({product, promotion}: Props) => { const [products, setProducts] = useState<IProductVariantPromo[]>([]) const [freeProducts, setFreeProducts] = useState<IProductVariantPromo[]>([]) const [error, setError] = useState<string | null>(null) @@ -132,7 +132,7 @@ const ProductPromoCard = ({ promotion}: Props) => { </div> <div> - <ProductPromoAddToCart promotion={promotion} /> + <ProductPromoAddToCart product={product} promotion={promotion} variant={products[0]} /> </div> </div> @@ -189,7 +189,7 @@ const ProductPromoCard = ({ promotion}: Props) => { </div> </div> <div> - <ProductPromoAddToCart promotion={promotion} /> + <ProductPromoAddToCart product={product} promotion={promotion} variant={products[0]}/> </div> </div> diff --git a/src-migrate/modules/product-promo/components/Item.tsx b/src-migrate/modules/product-promo/components/Item.tsx index b396160f..4b345654 100644 --- a/src-migrate/modules/product-promo/components/Item.tsx +++ b/src-migrate/modules/product-promo/components/Item.tsx @@ -1,34 +1,35 @@ -import style from '../styles/item.module.css' +import style from '../styles/item.module.css'; -import { Tooltip } from '@chakra-ui/react' +import { Tooltip } from '@chakra-ui/react'; -import Image from '~/components/ui/image' -import { IProductVariantPromo } from '~/types/promotion' +import Image from '~/components/ui/image'; +import { IProductVariantPromo } from '~/types/promotion'; type Props = { - variant: IProductVariantPromo, - isFree?: boolean -} + variant: IProductVariantPromo; + isFree?: boolean; +}; -const ProductPromoItem = ({ - variant, - isFree = false -}: Props) => { +const ProductPromoItem = ({ variant, isFree = false }: Props) => { return ( <div className={style.item}> <div className={style.image}> - <Image src={variant.image || '/images/noimage.jpeg'} alt={variant.display_name} width={120} height={120} quality={100} /> + <Image + src={variant.image || '/images/noimage.jpeg'} + alt={variant.display_name} + width={120} + height={120} + quality={85} + /> <div className={style.quantity}> {variant.qty} pcs {isFree ? '(free)' : ''} </div> </div> <Tooltip label={variant.display_name} placement='top' fontSize='sm'> - <div className={style.name}> - {variant.name} - </div> + <div className={style.name}>{variant.name}</div> </Tooltip> </div> - ) -} + ); +}; -export default ProductPromoItem
\ No newline at end of file +export default ProductPromoItem; diff --git a/src-migrate/modules/product-promo/components/Modal.tsx b/src-migrate/modules/product-promo/components/Modal.tsx index 0de672c2..1722b9df 100644 --- a/src-migrate/modules/product-promo/components/Modal.tsx +++ b/src-migrate/modules/product-promo/components/Modal.tsx @@ -3,8 +3,12 @@ import { Modal } from "~/components/ui/modal" import { useModalStore } from '../stores/useModalStore' import ProductPromoCategoryTab from './CategoryTab' import ProductPromoModalContent from './ModalContent' +import { IProductDetail } from '~/types/product'; -const ProductPromoModal = () => { +type Props = { + product: IProductDetail +} +const ProductPromoModal = ({product}:Props) => { const { active, closeModal } = useModalStore() return ( @@ -17,7 +21,7 @@ const ProductPromoModal = () => { <div className='h-4' /> - <ProductPromoModalContent /> + <ProductPromoModalContent product={product} /> </Modal> ) } diff --git a/src-migrate/modules/product-promo/components/ModalContent.tsx b/src-migrate/modules/product-promo/components/ModalContent.tsx index ab5129f8..256ef61a 100644 --- a/src-migrate/modules/product-promo/components/ModalContent.tsx +++ b/src-migrate/modules/product-promo/components/ModalContent.tsx @@ -6,7 +6,11 @@ import { getVariantPromoByCategory } from "~/services/productVariant" import { useModalStore } from "../stores/useModalStore" import ProductPromoCard from "./Card" -const ProductPromoModalContent = () => { +import { IProductDetail } from '~/types/product'; +type Props = { + product: IProductDetail +} +const ProductPromoModalContent = ({product}:Props) => { const { activeTab, variantId } = useModalStore() const promotionsQuery = useQuery( @@ -24,7 +28,7 @@ const ProductPromoModalContent = () => { <Skeleton isLoaded={!promotionsQuery.isLoading} className='min-h-[70vh] max-h-[70vh]'> <div className="grid grid-cols-1 gap-y-6 pb-6"> {promotions?.data.map((promo) => ( - <ProductPromoCard key={promo.id} promotion={promo} /> + <ProductPromoCard key={promo.id} promotion={promo} product={product} /> ))} {promotions?.data.length === 0 && ( <div className="py-10 rounded-lg h-fit text-center text-body-1 font-semibold text-gray-800 bg-gray-200">Belum ada promo pada kategori ini</div> diff --git a/src-migrate/modules/product-promo/components/Section.tsx b/src-migrate/modules/product-promo/components/Section.tsx index 4e8a7dd5..e1719998 100644 --- a/src-migrate/modules/product-promo/components/Section.tsx +++ b/src-migrate/modules/product-promo/components/Section.tsx @@ -9,12 +9,14 @@ import { IPromotion } from '~/types/promotion' import { useModalStore } from "../stores/useModalStore" import ProductPromoCard from './Card' import ProductPromoModal from "./Modal" +import { IProductDetail } from '~/types/product'; type Props = { productId: number; + product: IProductDetail; } -const ProductPromoSection = ({ productId }: Props) => { +const ProductPromoSection = ({ product, productId }: Props) => { const promotionsQuery = useQuery({ queryKey: [`promotions.highlight`, productId], queryFn: async () => await fetch(`/api/product-variant/${productId}/promotion/highlight`).then((res) => res.json()) as { data: IPromotion[] } @@ -23,14 +25,13 @@ const ProductPromoSection = ({ productId }: Props) => { const promotions = promotionsQuery.data const { openModal } = useModalStore() - return ( <SmoothRender isLoaded={(promotions?.data && promotions?.data.length > 0) || false} height='450px' duration='700ms' > - <ProductPromoModal /> + <ProductPromoModal product={product}/> {promotions?.data && promotions?.data.length > 0 && ( <div className={style.titleWrapper}> @@ -50,7 +51,7 @@ const ProductPromoSection = ({ productId }: Props) => { > {promotions?.data.map((promotion) => ( <div key={promotion.id} className="min-w-[400px] max-w-[400px]"> - <ProductPromoCard promotion={promotion} /> + <ProductPromoCard product={product} promotion={promotion} /> </div> ))} </Skeleton> diff --git a/src-migrate/modules/promo/components/Hero.tsx b/src-migrate/modules/promo/components/Hero.tsx index c5f0afad..7d0aad11 100644 --- a/src-migrate/modules/promo/components/Hero.tsx +++ b/src-migrate/modules/promo/components/Hero.tsx @@ -3,34 +3,34 @@ import 'swiper/css'; import Image from 'next/image'; import { useEffect, useMemo } from 'react'; import { useQuery } from 'react-query'; -import { Swiper, SwiperProps, SwiperSlide } from 'swiper/react'; +import { Swiper, SwiperProps, SwiperSlide } from 'swiper/react'; import style from '../styles/hero.module.css'; import 'swiper/css/navigation'; import 'swiper/css/pagination'; -import { Navigation, Pagination, Autoplay } from 'swiper'; +import { Navigation, Pagination, Autoplay } from 'swiper'; import MobileView from '../../../../src/core/components/views/MobileView'; import DesktopView from '@/core/components/views/DesktopView'; -import {bannerApi} from '../../../../src/api/bannerApi' +import { bannerApi } from '../../../../src/api/bannerApi'; interface IPromotionProgram { headlineBanner: string; descriptionBanner: string; - image: string ; + image: string; name: string; } const swiperBanner: SwiperProps = { - modules:[Navigation, Pagination, Autoplay], + modules: [Navigation, Pagination, Autoplay], autoplay: { delay: 6000, - disableOnInteraction: false + disableOnInteraction: false, }, loop: true, className: 'h-[400px] w-full', slidesPerView: 1, spaceBetween: 10, - pagination:true, -} + pagination: true, +}; const swiperBannerMob = { autoplay: { delay: 6000, @@ -43,7 +43,10 @@ const swiperBannerMob = { }; const Hero = () => { - const heroBanner = useQuery('allPromo', bannerApi({ type: 'banner-semua-promo' })); + const heroBanner = useQuery( + 'allPromo', + bannerApi({ type: 'banner-semua-promo' }) + ); const banners: IPromotionProgram[] = useMemo( () => heroBanner?.data || [], @@ -54,52 +57,60 @@ const Hero = () => { ...swiperBannerMob, pagination: { dynamicBullets: false, clickable: true }, }; - + return ( <> <DesktopView> <div className={style['wrapper']}> - <Swiper {...swiperBanner}> - {banners.map((banner, index) => ( - <SwiperSlide key={index} className='flex flex-row'> - <div className={style['desc-section']}> - <div className={style['title']}>{banner.headlineBanner? banner.headlineBanner : "Pasti Hemat & Untung Selama Belanja di Indoteknik.com!"}</div> - <div className='h-4' /> - <div className={style['subtitle']}>{banner.descriptionBanner? banner.descriptionBanner : "Cari paket yang kami sediakan dengan penawaran harga & Nikmati kemudahan dalam setiap transaksi dengan fitur lengkap Pembayaran hingga barang sampai!"}</div> + <Swiper {...swiperBanner}> + {banners?.map((banner, index) => ( + <SwiperSlide key={index} className='flex flex-row'> + <div className={style['desc-section']}> + <div className={style['title']}> + {banner?.headlineBanner + ? banner?.headlineBanner + : 'Pasti Hemat & Untung Selama Belanja di Indoteknik.com!'} </div> - <div className={style['banner-section']}> - <Image - src={banner.image} - alt={banner.name} - width={666} - height={450} - quality={90} - className='w-full h-full object-fit object-center rounded-2xl' /> + <div className='h-4' /> + <div className={style['subtitle']}> + {banner?.descriptionBanner + ? banner?.descriptionBanner + : 'Cari paket yang kami sediakan dengan penawaran harga & Nikmati kemudahan dalam setiap transaksi dengan fitur lengkap Pembayaran hingga barang sampai!'} </div> - </SwiperSlide> - ))} - </Swiper> - </div> - </DesktopView> - <MobileView> - <Swiper {...swiperBannerMobile}> - {banners?.map((banner, index) => ( - <SwiperSlide key={index}> - <Image - width={439} - height={150} - quality={100} - src={banner.image} - alt={banner.name} - className='w-full h-full object-cover object-center rounded-2xl' - /> + </div> + <div className={style['banner-section']}> + <Image + src={banner.image} + alt={banner.name} + width={666} + height={450} + quality={85} + className='w-full h-full object-fit object-center rounded-2xl' + /> + </div> </SwiperSlide> ))} </Swiper> - + </div> + </DesktopView> + <MobileView> + <Swiper {...swiperBannerMobile}> + {banners?.map((banner, index) => ( + <SwiperSlide key={index}> + <Image + width={439} + height={150} + quality={85} + src={banner?.image} + alt={banner?.name} + className='w-full h-full object-cover object-center rounded-2xl' + /> + </SwiperSlide> + ))} + </Swiper> </MobileView> </> - ) -} + ); +}; -export default Hero
\ No newline at end of file +export default Hero; diff --git a/src-migrate/modules/promo/components/HeroDiskon.tsx b/src-migrate/modules/promo/components/HeroDiskon.tsx index 6d38c763..8b8edcc0 100644 --- a/src-migrate/modules/promo/components/HeroDiskon.tsx +++ b/src-migrate/modules/promo/components/HeroDiskon.tsx @@ -39,7 +39,7 @@ const Hero = () => { queryFn: () => getBanner({ type: 'banner-promotion' }) }) - const banners = useMemo(() => bannerQuery.data || [], [bannerQuery.data]); + const banners = useMemo(() => bannerQuery?.data || [], [bannerQuery?.data]); useEffect(() => { if (banners.length > 1) { @@ -56,8 +56,8 @@ const Hero = () => { {banners.map((banner, index) => ( <SwiperSlide key={index}> <Image - src={banner.image} - alt={banner.name} + src={banner?.image} + alt={banner?.name} width={666} height={480} className='w-[446px] h-[446px] object-fill object-center rounded-2xl' @@ -71,8 +71,8 @@ const Hero = () => { {banners.map((banner, index) => ( <SwiperSlide key={index}> <Image - src={banner.image} - alt={banner.name} + src={banner?.image} + alt={banner?.name} width={666} height={450} className='w-[400px] h-[217px] object-cover object-center rounded-2xl ' @@ -86,8 +86,8 @@ const Hero = () => { {banners.map((banner, index) => ( <SwiperSlide key={index}> <Image - src={banner.image} - alt={banner.name} + src={banner?.image} + alt={banner?.name} width={666} height={450} className='w-[400px] h-[217px] object-cover object-center rounded-2xl' @@ -101,8 +101,8 @@ const Hero = () => { {banners.map((banner, index) => ( <SwiperSlide key={index}> <Image - src={banner.image} - alt={banner.name} + src={banner?.image} + alt={banner?.name} width={666} height={450} className='w-[400px] h-[217px] object-cover object-center rounded-2xl' @@ -116,8 +116,8 @@ const Hero = () => { {banners.map((banner, index) => ( <SwiperSlide key={index}> <Image - src={banner.image} - alt={banner.name} + src={banner?.image} + alt={banner?.name} width={666} height={450} className='w-[400px] h-[217px] object-cover object-center rounded-2xl' diff --git a/src-migrate/modules/promo/components/PromoList.tsx b/src-migrate/modules/promo/components/PromoList.tsx index 42725034..d59d1867 100644 --- a/src-migrate/modules/promo/components/PromoList.tsx +++ b/src-migrate/modules/promo/components/PromoList.tsx @@ -59,7 +59,7 @@ const PromoList: React.FC<PromoListProps> = ({ selectedPromo }) => { const items = await fetchPromoItemsSolr(`type_value_s:${slug}`, 0, 10); setPromoItems(items); - const promoDataPromises = items.map(async (item) => { + const promoDataPromises = items?.map(async (item) => { try { const response = await fetchPromoItemsSolr(`id:${item.id}`, 0, 10); return response; @@ -69,7 +69,7 @@ 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) { @@ -114,7 +114,7 @@ const PromoList: React.FC<PromoListProps> = ({ selectedPromo }) => { {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 promotion={promotion} /> + <ProductPromoCard product={promoItems} promotion={promotion} /> </div> </SwiperSlide> ))} @@ -122,7 +122,7 @@ const PromoList: React.FC<PromoListProps> = ({ selectedPromo }) => { )} {isMobile && (promoData?.map((promotion: IPromotion) => ( <div key={promotion.id} className="min-w-[400px] max-w-[400px]"> - <ProductPromoCard promotion={promotion} /> + <ProductPromoCard product={promoItems} promotion={promotion} /> </div> )))} diff --git a/src-migrate/modules/promo/components/PromotinProgram.jsx b/src-migrate/modules/promo/components/PromotinProgram.jsx index 33839944..43e4eedf 100644 --- a/src-migrate/modules/promo/components/PromotinProgram.jsx +++ b/src-migrate/modules/promo/components/PromotinProgram.jsx @@ -1,9 +1,7 @@ import React from 'react'; import Image from 'next/image'; -import { InfoIcon } from "lucide-react"; -import MobileView from '../../../../src/core/components/views/MobileView'; -import DesktopView from '@/core/components/views/DesktopView'; -import { Swiper, SwiperProps, SwiperSlide } from 'swiper/react'; +import { InfoIcon } from 'lucide-react'; +import { Swiper, SwiperProps, SwiperSlide } from 'swiper/react'; import 'swiper/css'; import useDevice from '@/core/hooks/useDevice'; @@ -11,9 +9,12 @@ const PromotionProgram = ({ selectedPromo, onSelectPromo }) => { const { isMobile } = useDevice(); return ( <> - <div className="text-h-sm md:text-h-lg font-semibold py-4">Serba Serbi Promo</div> + <h1 className='text-h-sm md:text-h-lg font-semibold py-4'> + {' '} + Serba Serbi Promo + </h1> <div className='px-4 sm:px-0'> - {/* <div className='w-full h-full '> + {/* <div className='w-full h-full '> <div onClick={() => onSelectPromo('Diskon')} className={`border p-2 flex items-center gap-x-2 rounded-lg cursor-pointer ${selectedPromo === 'Diskon' ? 'bg-red-50 border-red-500 text-red-500' : 'border-gray-200 text-gray-900'}`} @@ -39,93 +40,147 @@ const PromotionProgram = ({ selectedPromo, onSelectPromo }) => { </div> </div> </div> */} - - <Swiper slidesPerView={isMobile ? 1.3 : 3} spaceBetween={10}> - <SwiperSlide> - <div className='w-full h-full '> - <div - onClick={() => onSelectPromo('Bundling')} - className={`border h-full p-1 flex items-center gap-x-2 rounded-lg cursor-pointer ${selectedPromo === 'Bundling' ? 'bg-red-50 border-red-500 text-red-500' : 'border-gray-200 text-gray-900'}`} - > - <div> - <Image - width={24} - height={24} - quality={100} - src='/images/icon_promo/silat.svg' - alt='' - className='h-12 w-12 rounded' - /> - </div> - <div > - <div className='flex w-full flex-row items-center justify-start'> - <h1 className={`mr-1 font-semibold text-base ${selectedPromo === 'Bundling' ? 'text-red-500' : 'text-gray-900'}`}>Paket Silat</h1> - <InfoIcon className='mt-[1px] text-red-500' size={14} /> - </div> - <p className={`text-xs md:text-sm ${selectedPromo === 'Bundling' ? 'text-red-500' : 'text-gray-500'}`}> - Pilihan bundling barang kombinasi Silat. - </p> + + <Swiper slidesPerView={isMobile ? 1.3 : 3} spaceBetween={10}> + <SwiperSlide> + <div className='w-full h-full '> + <div + onClick={() => onSelectPromo('Bundling')} + className={`border h-full p-1 flex items-center gap-x-2 rounded-lg cursor-pointer ${ + selectedPromo === 'Bundling' + ? 'bg-red-50 border-red-500 text-red-500' + : 'border-gray-200 text-gray-900' + }`} + > + <div> + <Image + width={24} + height={24} + quality={85} + src='/images/icon_promo/silat.svg' + alt='' + className='h-12 w-12 rounded' + /> + </div> + <div> + <div className='flex w-full flex-row items-center justify-start'> + <h1 + className={`mr-1 font-semibold text-base ${ + selectedPromo === 'Bundling' + ? 'text-red-500' + : 'text-gray-900' + }`} + > + Paket Silat + </h1> + <InfoIcon className='mt-[1px] text-red-500' size={14} /> </div> + <p + className={`text-xs md:text-sm ${ + selectedPromo === 'Bundling' + ? 'text-red-500' + : 'text-gray-500' + }`} + > + Pilihan bundling barang kombinasi Silat. + </p> </div> </div> - </SwiperSlide> - <SwiperSlide> - <div className='w-full h-full '> - <div - onClick={() => onSelectPromo('Loading')} - className={`border p-2 h-full flex items-center gap-x-2 rounded-lg cursor-pointer ${selectedPromo === 'Loading' ? 'bg-red-50 border-red-500 text-red-500' : 'border-gray-200 text-gray-900'}`} - > - <div> - <Image - width={24} - height={24} - quality={100} - src='/images/icon_promo/barong.svg' - alt='' - className='h-12 w-12 rounded' - /> - </div> - <div> - <div className='flex w-full flex-row items-center justify-start'> - <h1 className={`mr-1 font-semibold text-base ${selectedPromo === 'Loading' ? 'text-red-500' : 'text-gray-900'}`}>Paket Barong</h1> - <InfoIcon className='mt-[1px] text-red-500' size={14} /> - </div> - <p className={`text-xs md:text-sm ${selectedPromo === 'Loading' ? 'text-red-500' : 'text-gray-500'}`}> - Beli banyak barang/partai barang borong. - </p> + </div> + </SwiperSlide> + <SwiperSlide> + <div className='w-full h-full '> + <div + onClick={() => onSelectPromo('Loading')} + className={`border p-2 h-full flex items-center gap-x-2 rounded-lg cursor-pointer ${ + selectedPromo === 'Loading' + ? 'bg-red-50 border-red-500 text-red-500' + : 'border-gray-200 text-gray-900' + }`} + > + <div> + <Image + width={24} + height={24} + quality={85} + src='/images/icon_promo/barong.svg' + alt='' + className='h-12 w-12 rounded' + /> + </div> + <div> + <div className='flex w-full flex-row items-center justify-start'> + <h1 + className={`mr-1 font-semibold text-base ${ + selectedPromo === 'Loading' + ? 'text-red-500' + : 'text-gray-900' + }`} + > + Paket Barong + </h1> + <InfoIcon className='mt-[1px] text-red-500' size={14} /> </div> + <p + className={`text-xs md:text-sm ${ + selectedPromo === 'Loading' + ? 'text-red-500' + : 'text-gray-500' + }`} + > + Beli banyak barang/partai barang borong. + </p> </div> </div> - </SwiperSlide> - <SwiperSlide> - <div className='w-full h-full '> - <div - onClick={() => onSelectPromo('Merchandise')} - className={`border p-2 h-full flex items-center gap-x-2 rounded-lg cursor-pointer ${selectedPromo === 'Merchandise' ? 'bg-red-50 border-red-500 text-red-500' : 'border-gray-200 text-gray-900'}`} - > - <div> - <Image - width={24} - height={24} - quality={100} - src='/images/icon_promo/angklung.svg' - alt='' - className='h-12 w-12 rounded' - /> - </div> - <div > - <div className='flex w-full flex-row items-center justify-start '> - <h1 className={`mr-1 font-semibold text-base ${selectedPromo === 'Merchandise' ? 'text-red-500' : 'text-gray-900'}`}>Paket Angklung</h1> - <InfoIcon className='mt-[1px] text-red-500' size={14} /> - </div> - <p className={` m1 text-xs md:text-sm ${selectedPromo === 'Merchandise' ? 'text-red-500' : 'text-gray-500'}`}> - Gratis barang promosi/merchandise menang langsung. - </p> + </div> + </SwiperSlide> + <SwiperSlide> + <div className='w-full h-full '> + <div + onClick={() => onSelectPromo('Merchandise')} + className={`border p-2 h-full flex items-center gap-x-2 rounded-lg cursor-pointer ${ + selectedPromo === 'Merchandise' + ? 'bg-red-50 border-red-500 text-red-500' + : 'border-gray-200 text-gray-900' + }`} + > + <div> + <Image + width={24} + height={24} + quality={85} + src='/images/icon_promo/angklung.svg' + alt='' + className='h-12 w-12 rounded' + /> + </div> + <div> + <div className='flex w-full flex-row items-center justify-start '> + <h1 + className={`mr-1 font-semibold text-base ${ + selectedPromo === 'Merchandise' + ? 'text-red-500' + : 'text-gray-900' + }`} + > + Paket Angklung + </h1> + <InfoIcon className='mt-[1px] text-red-500' size={14} /> </div> + <p + className={` m1 text-xs md:text-sm ${ + selectedPromo === 'Merchandise' + ? 'text-red-500' + : 'text-gray-500' + }`} + > + Gratis barang promosi/merchandise menang langsung. + </p> </div> </div> - </SwiperSlide> - </Swiper> + </div> + </SwiperSlide> + </Swiper> </div> </> ); diff --git a/src-migrate/modules/promo/components/Voucher.tsx b/src-migrate/modules/promo/components/Voucher.tsx index e5877e51..034d13e9 100644 --- a/src-migrate/modules/promo/components/Voucher.tsx +++ b/src-migrate/modules/promo/components/Voucher.tsx @@ -55,15 +55,18 @@ const VoucherComponent = () => { slidesPerView: isMobile ? 1.2 : 3.2, spaceBetween: 2, }; - - const dataVouchers = useMemo(() => voucherQuery.data || [], [voucherQuery.data]); - const vouchers = auth?.id? listVouchers : dataVouchers; + const dataVouchers = useMemo( + () => voucherQuery?.data || [], + [voucherQuery?.data] + ); + const vouchers = auth?.id ? listVouchers : dataVouchers; const copyText = (text: string) => { if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(text) + navigator.clipboard + .writeText(text) .then(() => { toast({ title: 'Salin ke papan klip', @@ -72,7 +75,7 @@ const VoucherComponent = () => { duration: 3000, isClosable: true, position: 'top', - }) + }); }) .catch(() => { fallbackCopyTextToClipboard(text); @@ -80,10 +83,10 @@ const VoucherComponent = () => { } else { fallbackCopyTextToClipboard(text); } - } + }; const fallbackCopyTextToClipboard = (text: string) => { - const textArea = document.createElement("textarea"); + const textArea = document.createElement('textarea'); textArea.value = text; // Tambahkan style untuk menyembunyikan textArea secara visual textArea.style.position = 'fixed'; @@ -108,43 +111,65 @@ const VoucherComponent = () => { duration: 3000, isClosable: true, position: 'top', - }) + }); } catch (err) { console.error('Fallback: Oops, unable to copy', err); } document.body.removeChild(textArea); - } + }; return ( <> - <div className={style['title']}>Pakai Voucher Belanja</div> + <h1 className={style['title']}>Pakai Voucher Belanja</h1> <div className='h-6' /> - {voucherQuery.isLoading && ( + {voucherQuery?.isLoading && ( <div className='grid grid-cols-3 gap-x-4 animate-pulse'> {Array.from({ length: 3 }).map((_, index) => ( - <div key={index} className='w-full h-[160px] bg-gray-200 rounded-xl' /> + <div + key={index} + className='w-full h-[160px] bg-gray-200 rounded-xl' + /> ))} </div> )} - {!voucherQuery.isLoading && ( + {!voucherQuery?.isLoading && ( <div className={style['voucher-section']}> <Swiper {...swiperVoucher}> {vouchers?.map((voucher) => ( <SwiperSlide key={voucher.id} className='pb-2'> <div className={style['voucher-card']}> - <Image src={voucher.image} alt={voucher.name} width={128} height={128} className={style['voucher-image']} /> + <Image + src={voucher?.image} + alt={voucher?.name} + width={128} + height={128} + className={style['voucher-image']} + /> <div className={style['voucher-content']}> - <div className={style['voucher-title']}>{voucher.name}</div> - <div className={style['voucher-desc']}>{voucher.description}</div> + <div className={style['voucher-title']}> + {voucher?.name} + </div> + <div className={style['voucher-desc']}> + {voucher?.description} + </div> <div className={style['voucher-bottom']}> <div> - <div className={style['voucher-code-desc']}>Kode Promo</div> - <div className={style['voucher-code']}>{voucher.code}</div> + <div className={style['voucher-code-desc']}> + Kode Promo + </div> + <div className={style['voucher-code']}> + {voucher?.code} + </div> </div> - <button className={style['voucher-copy']} onClick={() => copyText(voucher.code)}>Salin</button> + <button + className={style['voucher-copy']} + onClick={() => copyText(voucher?.code)} + > + Salin + </button> </div> </div> </div> @@ -154,7 +179,7 @@ const VoucherComponent = () => { </div> )} </> - ) -} + ); +}; -export default VoucherComponent +export default VoucherComponent; diff --git a/src-migrate/modules/register/components/Form.tsx b/src-migrate/modules/register/components/Form.tsx index b834f97a..38e9c810 100644 --- a/src-migrate/modules/register/components/Form.tsx +++ b/src-migrate/modules/register/components/Form.tsx @@ -1,179 +1,169 @@ -import { ChangeEvent, useMemo } from "react"; -import { useMutation } from "react-query"; -import { useRegisterStore } from "../stores/useRegisterStore"; -import { RegisterProps } from "~/types/auth"; -import { registerUser } from "~/services/auth"; -import TermCondition from "./TermCondition"; -import FormCaptcha from "./FormCaptcha"; -import { useRouter } from "next/router"; -import { UseToastOptions, useToast } from "@chakra-ui/react"; -import Link from "next/link"; - -const Form = () => { - const { - form, - isCheckedTNC, - isValidCaptcha, - errors, - updateForm, - validate, - } = useRegisterStore() - - const isFormValid = useMemo(() => Object.keys(errors).length === 0, [errors]) - - const router = useRouter() - const toast = useToast() +import { ChangeEvent, useMemo, useEffect, useRef, useState } from 'react'; +import { useMutation } from 'react-query'; +import { useRegisterStore } from '../stores/useRegisterStore'; +import { RegisterProps } from '~/types/auth'; +import { registerUser } from '~/services/auth'; +import TermCondition from './TermCondition'; +import FormCaptcha from './FormCaptcha'; +import { useRouter } from 'next/router'; +import { UseToastOptions, useToast } from '@chakra-ui/react'; +import Link from 'next/link'; + +interface FormProps { + type: string; + required: boolean; + isBisnisRegist: boolean; + chekValid: boolean; + buttonSubmitClick: boolean; +} + +const Form: React.FC<FormProps> = ({ + type, + required, + isBisnisRegist = false, + chekValid, + buttonSubmitClick, +}) => { + const { form, isCheckedTNC, isValidCaptcha, errors, updateForm, validate } = + useRegisterStore(); + const isFormValid = useMemo(() => Object.keys(errors).length === 0, [errors]); + + const router = useRouter(); + const toast = useToast(); + + const emailRef = useRef<HTMLInputElement>(null); + const nameRef = useRef<HTMLInputElement>(null); + const passwordRef = useRef<HTMLInputElement>(null); + const phoneRef = useRef<HTMLInputElement>(null); const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => { const { name, value } = event.target; - updateForm(name, value) - validate() - } - - const mutation = useMutation({ - mutationFn: (data: RegisterProps) => registerUser(data) - }) - - const handleSubmit = async (e: ChangeEvent<HTMLFormElement>) => { - e.preventDefault() - - const response = await mutation.mutateAsync(form) - - if (response?.register === true) { - const urlParams = new URLSearchParams({ - activation: 'otp', - email: form.email, - redirect: (router.query?.next || '/') as string - }) - router.push(`${router.route}?${urlParams}`) - } - - const toastProps: UseToastOptions = { - duration: 5000, - isClosable: true - } - - switch (response?.reason) { - case 'EMAIL_USED': - toast({ - ...toastProps, - title: 'Email sudah digunakan', - status: 'warning' - }) - break; - case 'NOT_ACTIVE': - const activationUrl = `${router.route}?activation=email` - toast({ - ...toastProps, - title: 'Akun belum aktif', - description: <>Akun sudah terdaftar namun belum aktif. <Link href={activationUrl} className="underline">Klik untuk aktivasi akun</Link></>, - status: 'warning' - }) - break - } - } + updateForm(name, value); + validate(); + }; + useEffect(() => { + const loadIndustries = async () => { + if (!isFormValid) { + const options: ScrollIntoViewOptions = { + behavior: 'smooth', + block: 'center', + }; + + if (errors.email_partner && emailRef.current) { + emailRef.current.scrollIntoView(options); + return; + } + if (errors.name && nameRef.current) { + nameRef.current.scrollIntoView(options); + return; + } + + if (errors.password && passwordRef.current) { + passwordRef.current.scrollIntoView(options); + return; + } + + if (errors.phone && phoneRef.current) { + phoneRef.current.scrollIntoView(options); + return; + } + } + }; + loadIndustries(); + }, [buttonSubmitClick, chekValid]); return ( - <form className="mt-6 grid grid-cols-1 gap-y-4" onSubmit={handleSubmit}> + <form className='mt-6 grid grid-cols-1 gap-y-4'> <div> - <label htmlFor="company"> - Nama Perusahaan <span className='text-gray_r-11'>(opsional)</span> + <label htmlFor='name' className='text-black font-bold'> + Nama Lengkap </label> <input - type="text" - name="company" - id="company" - className="form-input mt-3" - placeholder="cth: INDOTEKNIK DOTCOM GEMILANG" - autoCapitalize="true" - value={form.company} - onChange={handleInputChange} - /> - </div> - - <div> - <label htmlFor='name'>Nama Lengkap</label> - - <input type='text' id='name' name='name' - className='form-input mt-3' + className='form-input mt-3 transition-all duration-700' placeholder='Masukan nama lengkap anda' value={form.name} + ref={nameRef} onChange={handleInputChange} - aria-invalid={!!errors.name} - /> - - {!!errors.name && <span className="form-msg-danger">{errors.name}</span>} - </div> - - <div> - <label htmlFor='phone'>No Handphone</label> - - <input - type='tel' - id='phone' - name='phone' - className='form-input mt-3' - placeholder='08xxxxxxxx' - value={form.phone} - onChange={handleInputChange} - aria-invalid={!!errors.phone} + aria-invalid={chekValid && !!errors.name} /> - {!!errors.phone && <span className="form-msg-danger">{errors.phone}</span>} + {chekValid && !!errors.name && ( + <span className='form-msg-danger'>{errors.name}</span> + )} </div> <div> - <label htmlFor='email'>Alamat Email</label> + <label htmlFor='email' className='text-black font-bold'> + Alamat Email + </label> <input type='text' id='email' name='email' - className='form-input mt-3' + className='form-input mt-3 transition-all duration-500' placeholder='Masukan alamat email anda' value={form.email} + ref={emailRef} onChange={handleInputChange} - autoComplete="username" - aria-invalid={!!errors.email} + autoComplete='username' + aria-invalid={chekValid && !!errors.email} /> - {!!errors.email && <span className="form-msg-danger">{errors.email}</span>} + {chekValid && !!errors.email && ( + <span className='form-msg-danger'>{errors.email}</span> + )} </div> <div> - <label htmlFor='password'>Kata Sandi</label> + <label htmlFor='password' className='text-black font-bold'> + Kata Sandi + </label> <input type='password' name='password' id='password' - className='form-input mt-3' + className='form-input mt-3 transition-all duration-500' placeholder='••••••••••••' value={form.password} + ref={passwordRef} onChange={handleInputChange} - autoComplete="current-password" - aria-invalid={!!errors.password} + autoComplete='current-password' + aria-invalid={chekValid && !!errors.password} /> - {!!errors.password && <span className="form-msg-danger">{errors.password}</span>} + {chekValid && !!errors.password && ( + <span className='form-msg-danger'>{errors.password}</span> + )} </div> - <FormCaptcha /> + <div> + <label htmlFor='phone' className='text-black font-bold'> + No Handphone + </label> - <TermCondition /> + <input + type='tel' + id='phone' + name='phone' + className='form-input mt-3 transition-all duration-500' + placeholder='08xxxxxxxx' + value={form.phone} + ref={phoneRef} + onChange={handleInputChange} + aria-invalid={chekValid && !!errors.phone} + /> - <button - type="submit" - className="btn-yellow w-full mt-2" - disabled={!isFormValid || !isCheckedTNC || mutation.isLoading || !isValidCaptcha} - > - {mutation.isLoading ? 'Loading...' : 'Daftar'} - </button> + {chekValid && !!errors.phone && ( + <span className='form-msg-danger'>{errors.phone}</span> + )} + </div> </form> - ) -} + ); +}; -export default Form
\ No newline at end of file +export default Form; diff --git a/src-migrate/modules/register/components/FormBisnis.tsx b/src-migrate/modules/register/components/FormBisnis.tsx new file mode 100644 index 00000000..e4cf3442 --- /dev/null +++ b/src-migrate/modules/register/components/FormBisnis.tsx @@ -0,0 +1,715 @@ +import { ChangeEvent, useEffect, useMemo, useRef, useState } from 'react'; +import { useMutation } from 'react-query'; +import { useRegisterStore } from '../stores/useRegisterStore'; +import { RegisterProps } from '~/types/auth'; +import { registerUser } from '~/services/auth'; +import { useRouter } from 'next/router'; +import { + Button, + Checkbox, + UseToastOptions, + color, + useToast, +} from '@chakra-ui/react'; +import Link from 'next/link'; +import getFileBase64 from '@/core/utils/getFileBase64'; +import { Controller, useForm } from 'react-hook-form'; +import HookFormSelect from '@/core/components/elements/Select/HookFormSelect'; +import odooApi from '~/libs/odooApi'; +import { toast } from 'react-hot-toast'; +import { EyeIcon } from '@heroicons/react/24/outline'; +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; +import Image from 'next/image'; +import useDevice from '@/core/hooks/useDevice'; +interface FormProps { + type: string; + required: boolean; + isPKP: boolean; + chekValid: boolean; + buttonSubmitClick: boolean; +} + +interface industry_id { + label: string; + value: string; + category: string; +} + +interface companyType { + value: string; + label: string; +} + +const form: React.FC<FormProps> = ({ + type, + required, + isPKP, + chekValid, + buttonSubmitClick, +}) => { + const { form, errors, updateForm, validate } = useRegisterStore(); + const { control, watch, setValue } = useForm(); + const [selectedCategory, setSelectedCategory] = useState<string>(''); + const [isChekBox, setIsChekBox] = useState<boolean>(false); + const [isExample, setIsExample] = useState<boolean>(false); + const { isDesktop, isMobile } = useDevice(); + // Inside your component + const [formattedNpwp, setFormattedNpwp] = useState<string>(''); // State for formatted NPWP + const [unformattedNpwp, setUnformattedNpwp] = useState<string>(''); // State for unformatted NPWP + + const [industries, setIndustries] = useState<industry_id[]>([]); + const [companyTypes, setCompanyTypes] = useState<companyType[]>([]); + + const router = useRouter(); + const toast = useToast(); + + const emailRef = useRef<HTMLInputElement>(null); + const businessNameRef = useRef<HTMLInputElement>(null); + const companyTypeRef = useRef<HTMLInputElement>(null); + const industryRef = useRef<HTMLDivElement>(null); + const addressRef = useRef<HTMLInputElement>(null); + const namaWajibPajakRef = useRef<HTMLInputElement>(null); + const alamatWajibPajakRef = useRef<HTMLInputElement>(null); + const npwpRef = useRef<HTMLInputElement>(null); + const sppkpRef = useRef<HTMLInputElement>(null); + const docsSppkpRef = useRef<HTMLInputElement>(null); + const docsNpwpRef = useRef<HTMLInputElement>(null); + + useEffect(() => { + const loadCompanyTypes = async () => { + const dataCompanyTypes = await odooApi( + 'GET', + '/api/v1/partner/company_type' + ); + setCompanyTypes( + dataCompanyTypes?.map((o: { id: any; name: any }) => ({ + value: o.id, + label: o.name, + })) + ); + }; + loadCompanyTypes(); + }, []); + + useEffect(() => { + const selectedCompanyType = companyTypes.find( + (company) => company.value === watch('companyType') + ); + if (selectedCompanyType) { + updateForm('company_type_id', `${selectedCompanyType?.value}`); + validate(); + } + }, [watch('companyType'), companyTypes]); + + useEffect(() => { + const selectedIndustryType = industries.find( + (industry) => industry.value === watch('industry_id') + ); + if (selectedIndustryType) { + updateForm('industry_id', `${selectedIndustryType?.value}`); + setSelectedCategory(selectedIndustryType.category); + validate(); + } + }, [watch('industry_id'), industries]); + + useEffect(() => { + const loadIndustries = async () => { + const dataIndustries = await odooApi('GET', '/api/v1/partner/industry'); + setIndustries( + dataIndustries?.map((o: { id: any; name: any; category: any }) => ({ + value: o.id, + label: o.name, + category: o.category, + })) + ); + }; + loadIndustries(); + }, []); + + const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => { + const { name, value } = event.target; + + updateForm('type_acc', 'business'); + updateForm('is_pkp', `${isPKP}`); + + // Update form dengan nilai terbaru dari input yang berubah + updateForm(name, value); + + // Jika checkbox aktif, sinkronisasi alamat_wajib_pajak dengan alamat_bisnis + if (isChekBox) { + if (name === 'alamat_wajib_pajak') { + updateForm('alamat_bisnis', value); + } else if (name === 'alamat_bisnis') { + updateForm('alamat_wajib_pajak', value); + } + } + + // Validasi setelah perubahan dilakukan + validate(); + }; + + const handleInputChangeNpwp = (event: ChangeEvent<HTMLInputElement>) => { + const { name, value } = event.target; + updateForm('type_acc', `business`); + updateForm('is_pkp', `${isPKP}`); + updateForm('npwp', value); + validate(); + }; + + const handleChange = (event: ChangeEvent<HTMLInputElement>) => { + setIsChekBox(!isChekBox); + }; + + const formatNpwp = (value: string) => { + try { + const cleaned = ('' + value).replace(/\D/g, ''); + let match; + if (cleaned.length <= 15) { + match = cleaned.match( + /(\d{0,2})?(\d{0,3})?(\d{0,3})?(\d{0,1})?(\d{0,3})?(\d{0,3})$/ + ); + } else { + match = cleaned.match( + /(\d{0,3})?(\d{0,3})?(\d{0,3})?(\d{0,1})?(\d{0,3})?(\d{0,3})$/ + ); + } + + if (match) { + return [ + match[1], + match[2] ? '.' : '', + match[2], + match[3] ? '.' : '', + match[3], + match[4] ? '.' : '', + match[4], + match[5] ? '-' : '', + match[5], + match[6] ? '.' : '', + match[6], + ].join(''); + } + + // If match is null, return the original cleaned string or handle as needed + return cleaned; + } catch (error) { + // Handle error or return a default value + console.error('Error formatting NPWP:', error); + return value; + } + }; + + useEffect(() => { + if (isChekBox) { + updateForm('isChekBox', 'true'); + updateForm('alamat_wajib_pajak', `${form.alamat_bisnis}`); + validate(); + } else { + updateForm('isChekBox', 'false'); + validate(); + } + }, [isChekBox]); + + const handleFileChange = async (event: ChangeEvent<HTMLInputElement>) => { + const toastProps: UseToastOptions = { + duration: 5000, + isClosable: true, + position: 'top', + }; + + let fileBase64 = ''; + const { name } = event.target; + const file = event.target.files?.[0]; + + // Allowed file extensions + const allowedExtensions = ['pdf', 'doc', 'docx', 'png', 'jpg', 'jpeg']; + + if (file) { + const fileExtension = file.name.split('.').pop()?.toLowerCase(); // Extract file extension + + // Check if the file extension is allowed + if (!fileExtension || !allowedExtensions.includes(fileExtension)) { + toast({ + ...toastProps, + title: + 'Format file yang diijinkan adalah .pdf, .doc, .docx, .png, .jpg, atau .jpeg', + status: 'error', + }); + event.target.value = ''; + return; + } + + // Check for file size + if (file.size > 5000000) { + toast({ + ...toastProps, + title: 'Maksimal ukuran file adalah 5MB', + status: 'error', + }); + event.target.value = ''; + return; + } + + // Convert file to Base64 + fileBase64 = await getFileBase64(file); + updateForm(name, fileBase64); // Update form with the Base64 string + validate(); // Perform form validation + } + }; + const isFormValid = useMemo(() => Object.keys(errors).length === 0, [errors]); + + useEffect(() => { + const loadIndustries = async () => { + if (!isFormValid) { + const options: ScrollIntoViewOptions = { + behavior: 'smooth', + block: 'center', + }; + if (errors.email_partner && emailRef.current) { + emailRef.current.scrollIntoView(options); + return; + } + if (errors.company_type_id && companyTypeRef.current) { + companyTypeRef.current.scrollIntoView(options); + return; + } + + if (errors.business_name && businessNameRef.current) { + businessNameRef.current.scrollIntoView(options); + return; + } + + if (errors.industry_id && industryRef.current) { + industryRef.current.scrollIntoView(options); + return; + } + + if (errors.alamat_bisnis && addressRef.current) { + addressRef.current.scrollIntoView(options); + return; + } + + if (errors.npwp && npwpRef.current) { + npwpRef.current.scrollIntoView(options); + return; + } + + if (errors.sppkp && sppkpRef.current) { + sppkpRef.current.scrollIntoView(options); + return; + } + if (errors.sppkp_document && docsSppkpRef.current) { + docsSppkpRef.current.scrollIntoView(options); + return; + } + if (errors.npwp_document && docsNpwpRef.current) { + docsNpwpRef.current.scrollIntoView(options); + return; + } + } + }; + loadIndustries(); + }, [buttonSubmitClick, chekValid]); + return ( + <> + <BottomPopup + className='' + title='Contoh SPPKP' + active={isExample} + close={() => setIsExample(false)} + > + <div className='flex p-2'> + <Image + src='/images/NO-SPPKP-FORMAT-TEMPLATE.jpg' + alt='Contoh SPPKP' + className='w-full h-full ' + width={800} + height={800} + quality={85} + /> + </div> + </BottomPopup> + <form + className={` ${ + type === 'bisnis' + ? 'mt-6 grid grid-cols-1 gap-y-4' + : 'mt-6 grid grid-cols-2 gap-x-4 gap-y-2' + }`} + > + <div> + <label htmlFor='email' className='font-bold'> + Email Bisnis{' '} + {!isPKP && !required && ( + <span className='font-normal text-gray_r-11'>(opsional)</span> + )} + </label> + + <input + type='text' + id='email_partner' + name='email_partner' + placeholder='example@email.com' + value={!required ? form.email_partner : ''} + className={`form-input max-h-11 mt-3 transition-all duration-500 ${ + required ? 'cursor-no-drop' : '' + }`} + disabled={required} + contentEditable={required} + readOnly={required} + onChange={handleInputChange} + autoComplete='username' + ref={emailRef} + aria-invalid={ + chekValid && !required && isPKP && !!errors.email_partner + } + /> + + {chekValid && !required && isPKP && !!errors.email_partner && ( + <span className='form-msg-danger'>{errors.email_partner}</span> + )} + </div> + + <div> + <label className='font-bold' htmlFor='company'> + Nama Bisnis + </label> + <div className='flex justify-between items-start gap-2 max-h-12 min-h-12 text-sm mt-3'> + <div + className='w-4/5 pr-1 max-h-11 transition-all duration-500' + ref={companyTypeRef} + > + <Controller + name='companyType' + control={control} + render={(props) => ( + <HookFormSelect + {...props} + options={companyTypes} + disabled={required} + placeholder='Badan Usaha' + /> + )} + /> + {chekValid && !required && !!errors.company_type_id && ( + <span className='form-msg-danger'> + {errors.company_type_id} + </span> + )} + </div> + <div className='w-[120%] '> + <input + type='text' + name='business_name' + id='business_name' + className='form-input max-h-11 transition-all duration-500' + placeholder='Nama Perusahaan' + autoCapitalize='true' + value={form.business_name} + ref={businessNameRef} + aria-invalid={chekValid && !!errors.business_name} + onChange={handleInputChange} + /> + + {chekValid && !!errors.business_name && ( + <span className='form-msg-danger'>{errors.business_name}</span> + )} + </div> + </div> + </div> + + <div className='mt-8 md:mt-0'> + <label className='font-bold' htmlFor='business_name'> + Klasifikasi Jenis Usaha + </label> + <div + className='max-h-10 transition-all duration-500' + ref={industryRef} + > + <Controller + name='industry_id' + control={control} + render={(props) => ( + <HookFormSelect + {...props} + options={industries} + disabled={required} + placeholder={'Select industry'} + /> + )} + /> + </div> + {selectedCategory && ( + <span className='text-gray_r-11 text-xs'> + Kategori : {selectedCategory} + </span> + )} + {chekValid && !required && !!errors.industry_id && ( + <span className='form-msg-danger'>{errors.industry_id}</span> + )} + </div> + + <div> + <label htmlFor='alamat_bisnis' className='font-bold'> + Alamat Bisnis + </label> + + <input + type='text' + id='alamat_bisnis' + name='alamat_bisnis' + placeholder='Masukan alamat bisnis anda' + value={!required ? form.alamat_bisnis : ''} + className={`form-input mt-3 max-h-11 transition-all duration-500 ${ + required ? 'cursor-no-drop' : '' + }`} + disabled={required} + contentEditable={required} + readOnly={required} + ref={addressRef} + onChange={handleInputChange} + aria-invalid={chekValid && !required && !!errors.alamat_bisnis} + /> + + {chekValid && !required && !!errors.alamat_bisnis && ( + <span className='form-msg-danger'>{errors.alamat_bisnis}</span> + )} + </div> + + <div> + <label htmlFor='nama_wajib_pajak' className='font-bold'> + Nama Wajib Pajak{' '} + {!isPKP && !required && ( + <span className='font-normal text-gray_r-11'>(opsional)</span> + )} + </label> + + <input + type='text' + id='nama_wajib_pajak' + name='nama_wajib_pajak' + placeholder='Masukan nama lengkap anda' + value={!required ? form.nama_wajib_pajak : ''} + className={`form-input mt-3 max-h-11 transition-all duration-500${ + required ? 'cursor-no-drop' : '' + }`} + disabled={required} + contentEditable={required} + readOnly={required} + onChange={handleInputChange} + ref={namaWajibPajakRef} + aria-invalid={ + chekValid && isPKP && !required && !!errors.nama_wajib_pajak + } + /> + + {chekValid && isPKP && !required && !!errors.nama_wajib_pajak && ( + <span className='form-msg-danger'>{errors.nama_wajib_pajak}</span> + )} + </div> + + <div> + <label + htmlFor='alamat_wajib_pajak' + className='font-bold flex items-center' + > + <p> + Alamat Wajib Pajak{' '} + {!isPKP && !required && ( + <span className='font-normal text-gray_r-11'>(opsional)</span> + )} + </p> + <div className='flex items-center ml-2 mt-1 '> + <Checkbox + borderColor='gray.600' + colorScheme='red' + size='md' + isChecked={isChekBox} + onChange={handleChange} + /> + <span className='text-caption-2 ml-2 font-normal italic'> + sama dengan alamat bisnis? + </span> + </div> + </label> + + <input + type='text' + id='alamat_wajib_pajak' + name='alamat_wajib_pajak' + placeholder='Masukan alamat wajib pajak anda' + value={ + !required + ? isChekBox + ? form.alamat_bisnis + : form.alamat_wajib_pajak + : '' + } + className={`form-input max-h-11 mt-3 transition-all duration-500 ${ + required ? 'cursor-no-drop' : '' + }`} + disabled={isChekBox || required} + contentEditable={required} + readOnly={required} + onChange={handleInputChange} + ref={alamatWajibPajakRef} + aria-invalid={ + chekValid && isPKP && !required && !!errors.alamat_wajib_pajak + } + /> + + {chekValid && isPKP && !required && !!errors.alamat_wajib_pajak && ( + <span className='form-msg-danger'>{errors.alamat_wajib_pajak}</span> + )} + </div> + + <div> + <label htmlFor='npwp' className='font-bold'> + Nomor NPWP{' '} + {!isPKP && !required && ( + <span className='font-normal text-gray_r-11'>(opsional)</span> + )} + </label> + + <input + type='tel' + id='npwp' + name='npwp' + className={`form-input max-h-11 mt-3 transition-all duration-500 ${ + required ? 'cursor-no-drop' : '' + }`} + disabled={required} + contentEditable={required} + readOnly={required} + ref={npwpRef} + placeholder='000.000.000.0-000.000' + value={!required ? formattedNpwp : ''} + maxLength={21} // Set maximum length to 16 characters + onChange={(e) => { + if (!required) { + const unformatted = e.target.value.replace(/\D/g, ''); // Remove all non-digit characters + const formattedValue = formatNpwp(unformatted); // Format the value + setUnformattedNpwp(unformatted); // Store unformatted value + setFormattedNpwp(formattedValue); // Store formatted value + handleInputChangeNpwp({ + ...e, + target: { ...e.target, value: unformatted }, + }); // Update form state with unformatted value + } + }} + aria-invalid={chekValid && !required && !!errors.npwp} + /> + + {chekValid && !required && !!errors.npwp && ( + <span className='form-msg-danger'>{errors.npwp}</span> + )} + </div> + + <div> + <label + htmlFor='sppkp' + className='font-bold flex flex-row items-center justify-between' + > + <div className='flex flex-row items-center'> + Nomor SPPKP{' '} + {!required && ( + <span className='ml-2 font-normal text-gray_r-11'> + (opsional){' '} + </span> + )} + </div> + { + <div + onClick={() => setIsExample(!isExample)} + className='rounded text-white p-2 flex flex-row bg-red-500 hover:cursor-pointer hover:bg-red-400' + > + <EyeIcon className={`w-4 ${isDesktop && 'mr-2'}`} /> + {isDesktop && ( + <p className='font-light text-xs'>Lihat Contoh</p> + )} + </div> + } + </label> + + <input + type='tel' + id='sppkp' + name='sppkp' + className={`form-input max-h-11 mt-3 transition-all duration-500 ${ + required ? 'cursor-no-drop' : '' + }`} + disabled={required} + contentEditable={required} + readOnly={required} + ref={sppkpRef} + placeholder='X-XXXPKP/WJPXXX/XX.XXXX/XXXX' + onChange={handleInputChange} + value={!required ? form.sppkp : ''} + aria-invalid={chekValid && !required && !!errors.sppkp} + /> + + {chekValid && !required && !!errors.sppkp && ( + <span className='form-msg-danger'>{errors.sppkp}</span> + )} + </div> + + <div> + <label htmlFor='npwp_document' className='font-bold'> + Dokumen NPWP{' '} + {!isPKP && !required && ( + <span className='font-normal text-gray_r-11'>(opsional)</span> + )} + </label> + + <input + type='file' + id='npwp_document' + name='npwp_document' + className={`form-input transition-all duration-500 ${ + type === 'bisnis' ? '' : 'border-none' + } mt-3 ${required ? 'cursor-no-drop' : ''}`} + disabled={required} + contentEditable={required} + ref={docsNpwpRef} + readOnly={required} + onChange={handleFileChange} + accept='.pdf,.doc,.docx,.png,.jpg,.jpeg' // Filter file types + /> + + {chekValid && isPKP && !required && !!errors.npwp_document && ( + <span className='form-msg-danger'>{errors.npwp_document}</span> + )} + </div> + + <div> + <label htmlFor='sppkp_document' className='font-bold'> + Dokumen SPPKP{' '} + {!isPKP && !required && ( + <span className='font-normal text-gray_r-11'>(opsional)</span> + )} + </label> + + <input + type='file' + id='sppkp_document' + name='sppkp_document' + className={`form-input transition-all duration-500 ${ + type === 'bisnis' ? '' : 'border-none' + } mt-3 ${required ? 'cursor-no-drop' : ''}`} + disabled={required} + contentEditable={required} + ref={docsSppkpRef} + readOnly={required} + onChange={handleFileChange} + accept='.pdf,.doc,.docx,.png,.jpg,.jpeg' // Filter file types + /> + + {chekValid && isPKP && !required && !!errors.sppkp_document && ( + <span className='form-msg-danger'>{errors.sppkp_document}</span> + )} + </div> + </form> + </> + ); +}; + +export default form; diff --git a/src-migrate/modules/register/components/RegistrasiBisnis.tsx b/src-migrate/modules/register/components/RegistrasiBisnis.tsx new file mode 100644 index 00000000..ce4d3972 --- /dev/null +++ b/src-migrate/modules/register/components/RegistrasiBisnis.tsx @@ -0,0 +1,197 @@ +import { ChangeEvent, useEffect, useMemo, useState } from 'react'; +import FormBisnis from './FormBisnis'; +import Form from './Form'; +import TermCondition from './TermCondition'; +import FormCaptcha from './FormCaptcha'; +import { Radio, RadioGroup, Stack, Divider, Button } from '@chakra-ui/react'; +import React from 'react'; +import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; +import { useRegisterStore } from '../stores/useRegisterStore'; +import { useMutation } from 'react-query'; +import { RegisterProps } from '~/types/auth'; +import { registerUser } from '~/services/auth'; +import router from 'next/router'; +import { useRouter } from 'next/router'; +import { UseToastOptions, useToast } from '@chakra-ui/react'; +import Link from 'next/link'; +interface FormProps { + chekValid: boolean; + buttonSubmitClick: boolean; +} +const RegistrasiBisnis: React.FC<FormProps> = ({ + chekValid, + buttonSubmitClick, +}) => { + const [isPKP, setIsPKP] = useState(true); + const [isTerdaftar, setIsTerdaftar] = useState(false); + const [isDropIndividu, setIsDropIndividu] = useState(true); + const [isBisnisClicked, setisBisnisClicked] = useState(true); + const [selectedValue, setSelectedValue] = useState('PKP'); + const [selectedValueBisnis, setSelectedValueBisnis] = useState('false'); + const { form, isCheckedTNC, isValidCaptcha, errors, validate, updateForm } = + useRegisterStore(); + const isFormValid = useMemo(() => Object.keys(errors).length === 0, [errors]); + const toast = useToast(); + const mutation = useMutation({ + mutationFn: (data: RegisterProps) => registerUser(data), + }); + + useEffect(() => { + if (selectedValue === 'PKP') { + updateForm('is_pkp', 'true'); + validate(); + } else { + updateForm('is_pkp', 'false'); + validate(); + } + }, [selectedValue]); + + useEffect(() => { + if (isTerdaftar) { + updateForm('is_terdaftar', 'true'); + validate(); + } else { + updateForm('is_terdaftar', 'false'); + validate(); + } + }, [isTerdaftar]); + + const handleChange = (value: string) => { + setSelectedValue(value); + if (value === 'PKP') { + validate(); + setIsPKP(true); + } else { + validate(); + setIsPKP(false); + setIsPKP(false); + } + }; + + const handleChangeBisnis = (value: string) => { + setSelectedValueBisnis(value); + if (value === 'true') { + validate(); + setIsTerdaftar(true); + } else { + validate(); + setIsTerdaftar(false); + } + }; + + const handleClick = () => { + setIsDropIndividu(!isDropIndividu); + }; + + const handleClickBisnis = () => { + setisBisnisClicked(!isBisnisClicked); + }; + + return ( + <> + <div className='mt-4 border'> + <div className='p-4'> + <div onClick={handleClick} className='flex justify-between'> + <p className='text-2xl font-semibold text-center md:text-left'> + Data Akun + </p> + {isDropIndividu ? ( + <div className='flex'> + <ChevronDownIcon + onClick={handleClick} + className='h-6 w-6 text-black' + /> + </div> + ) : ( + <ChevronRightIcon + onClick={handleClick} + className='h-6 w-6 text-black' + /> + )} + </div> + {isDropIndividu && ( + <div> + <Divider my={4} /> + <Form + type='bisnis' + required={true} + isBisnisRegist={true} + chekValid={chekValid} + buttonSubmitClick={buttonSubmitClick} + /> + </div> + )} + </div> + </div> + <div className='mt-4 border'> + <div className='p-4'> + <div onClick={handleClickBisnis} className='flex justify-between'> + <p className='text-2xl font-semibold text-center md:text-left'> + Data Bisnis + </p> + {isBisnisClicked ? ( + <div className='flex'> + <ChevronDownIcon + onClick={handleClickBisnis} + className='h-6 w-6 text-black' + /> + </div> + ) : ( + <ChevronRightIcon + onClick={handleClickBisnis} + className='h-6 w-6 text-black' + /> + )} + </div> + {isBisnisClicked && ( + <div> + <Divider my={4} /> + <div> + <p className='text-black font-bold mb-2'> + Bisnis Terdaftar di Indoteknik? + </p> + <RadioGroup + onChange={handleChangeBisnis} + value={selectedValueBisnis} + > + <Stack direction='row'> + <Radio colorScheme='red' value='true'> + Sudah Terdaftar + </Radio> + <Radio colorScheme='red' value='false' className='ml-2'> + Belum Terdaftar + </Radio> + </Stack> + </RadioGroup> + </div> + <div className='mt-4'> + <p className='text-black font-bold mb-2'>Tipe Bisnis</p> + <RadioGroup onChange={handleChange} value={selectedValue}> + <Stack direction='row' className='font-bold'> + <Radio colorScheme='red' value='PKP'> + PKP + </Radio> + <Radio colorScheme='red' value='Non-PKP' className='ml-4'> + Non-PKP + </Radio> + </Stack> + </RadioGroup> + </div> + <FormBisnis + type='bisnis' + required={isTerdaftar} + isPKP={isPKP} + chekValid={chekValid} + buttonSubmitClick={buttonSubmitClick} + /> + </div> + )} + </div> + </div> + + <h1 className=''></h1> + </> + ); +}; + +export default RegistrasiBisnis; diff --git a/src-migrate/modules/register/components/RegistrasiIndividu.tsx b/src-migrate/modules/register/components/RegistrasiIndividu.tsx new file mode 100644 index 00000000..84049065 --- /dev/null +++ b/src-migrate/modules/register/components/RegistrasiIndividu.tsx @@ -0,0 +1,34 @@ +import Form from './Form'; +import { useRegisterStore } from '../stores/useRegisterStore'; +import { useEffect } from 'react'; +interface FormProps { + chekValid: boolean; + buttonSubmitClick: boolean; +} +const RegistrasiIndividu: React.FC<FormProps> = ({ + chekValid, + buttonSubmitClick, +}) => { + const { form, errors, updateForm, validate } = useRegisterStore(); + + useEffect(() => { + updateForm('is_pkp', 'false'); + updateForm('is_terdaftar', 'false'); + updateForm('type_acc', 'individu'); + validate(); + }, []); + + return ( + <> + <Form + type='individu' + required={false} + isBisnisRegist={false} + chekValid={chekValid} + buttonSubmitClick={buttonSubmitClick} + /> + </> + ); +}; + +export default RegistrasiIndividu; diff --git a/src-migrate/modules/register/components/TermCondition.tsx b/src-migrate/modules/register/components/TermCondition.tsx index b7729deb..d54fe921 100644 --- a/src-migrate/modules/register/components/TermCondition.tsx +++ b/src-migrate/modules/register/components/TermCondition.tsx @@ -10,7 +10,7 @@ const TermCondition = () => { return ( <> <div className="mt-4 flex items-center gap-x-2"> - <Checkbox id='tnc' name='tnc' isChecked={isCheckedTNC} onChange={toggleCheckTNC} /> + <Checkbox id='tnc' name='tnc' colorScheme='red' isChecked={isCheckedTNC} onChange={toggleCheckTNC} /> <div> <label htmlFor="tnc" className="cursor-pointer">Dengan ini saya menyetujui</label> {' '} diff --git a/src-migrate/modules/register/index.tsx b/src-migrate/modules/register/index.tsx index 00931284..da41fd8b 100644 --- a/src-migrate/modules/register/index.tsx +++ b/src-migrate/modules/register/index.tsx @@ -1,54 +1,212 @@ -import PageContent from "~/modules/page-content" -import Form from "./components/Form" -import Link from "next/link" -import Image from "next/image" -import IndoteknikLogo from "~/images/logo.png" -import AccountActivation from "../account-activation" +import PageContent from '~/modules/page-content'; +import RegistrasiIndividu from './components/RegistrasiIndividu'; +import RegistrasiBisnis from './components/RegistrasiBisnis'; +import Link from 'next/link'; +import Image from 'next/image'; +import IndoteknikLogo from '~/images/logo.png'; +import AccountActivation from '../account-activation'; +import { useMemo, useState } from 'react'; +import { useRegisterStore } from './stores/useRegisterStore'; +import FormCaptcha from './components/FormCaptcha'; +import TermCondition from './components/TermCondition'; +import { Button } from '@chakra-ui/react'; +import { useMutation } from 'react-query'; +import { UseToastOptions, useToast } from '@chakra-ui/react'; +import { RegisterProps } from '~/types/auth'; +import { registerUser } from '~/services/auth'; +import { useRouter } from 'next/router'; const LOGO_WIDTH = 150; const LOGO_HEIGHT = LOGO_WIDTH / 3; const Register = () => { + const [isIndividuClicked, setIsIndividuClicked] = useState(true); + const [notValid, setNotValid] = useState(false); + const [buttonSubmitClick, setButtonSubmitClick] = useState(false); + const [isBisnisClicked, setIsBisnisClicked] = useState(false); + const { form, isCheckedTNC, isValidCaptcha, resetForm, errors, updateForm } = + useRegisterStore(); + + const isFormValid = useMemo(() => Object.keys(errors).length === 0, [errors]); + const toast = useToast(); + const router = useRouter(); + const mutation = useMutation({ + mutationFn: (data: RegisterProps) => registerUser(data), + }); + + const handleIndividuClick = () => { + resetForm(); + setIsIndividuClicked(true); + setIsBisnisClicked(false); + }; + + const handleBisnisClick = () => { + resetForm(); + updateForm('is_terdaftar', 'false'); + updateForm('type_acc', 'business'); + setIsIndividuClicked(false); + setIsBisnisClicked(true); + }; + const handleSubmit = async () => { + if (!isFormValid) { + setNotValid(true); + setButtonSubmitClick(!buttonSubmitClick); + return; + } else { + setButtonSubmitClick(!buttonSubmitClick); + setNotValid(false); + } + const response = await mutation.mutateAsync(form); + if (response?.register === true) { + const urlParams = new URLSearchParams({ + activation: 'otp', + email: form.email, + redirect: (router.query?.next || '/') as string, + }); + router.push(`${router.route}?${urlParams}`); + } + + const toastProps: UseToastOptions = { + duration: 5000, + isClosable: true, + position: 'top', + }; + + switch (response?.reason) { + case 'EMAIL_USED': + toast({ + ...toastProps, + title: 'Email sudah digunakan', + status: 'warning', + }); + break; + case 'NOT_ACTIVE': + const activationUrl = `${router.route}?activation=email`; + toast({ + ...toastProps, + title: 'Akun belum aktif', + description: ( + <> + Akun sudah terdaftar namun belum aktif.{' '} + <Link href={activationUrl} className='underline'> + Klik untuk aktivasi akun + </Link> + </> + ), + status: 'warning', + }); + break; + } + }; return ( - <div className="container"> - <div className="grid grid-cols-1 md:grid-cols-2 gap-x-10 pt-10 px-2 md:pt-16"> - <section> - <Link href='/' className="block md:hidden"> - <Image src={IndoteknikLogo} alt='Logo Indoteknik' width={LOGO_WIDTH} height={LOGO_HEIGHT} className="mx-auto mb-4 w-auto h-auto" priority /> - </Link> - - <h1 className="text-2xl font-semibold text-center md:text-left"> - Daftar Akun Indoteknik - </h1> - <h2 className="text-gray_r-11 mt-1 mb-10 text-center md:text-left"> - Buat akun sekarang lebih mudah dan terverifikasi - </h2> - - <Form /> - - <div className='text-gray_r-11 mt-4 text-center md:text-left'> - Sudah punya akun Indoteknik?{' '} - <Link href='/login' className='inline font-medium text-danger-500'> - Masuk + <div className='container'> + <div className='grid grid-cols-1 md:grid-cols-2 gap-x-8 pt-10 px-2 md:pt-16'> + <section className=''> + <div className='px-8 py-4 border'> + <Link href='/' className='block md:hidden'> + <Image + src={IndoteknikLogo} + alt='Logo Indoteknik' + width={LOGO_WIDTH} + height={LOGO_HEIGHT} + className='mx-auto mb-4 w-auto h-auto' + priority + /> </Link> - </div> - <div className='text-gray_r-11 mt-4 text-center md:text-left'> - Akun anda belum aktif?{' '} - <Link href='/register?activation=email' className='inline font-medium text-danger-500'> - Aktivasi - </Link> + <h1 className='text-2xl font-semibold text-center md:text-left'> + Daftar Akun Indoteknik + </h1> + <h2 className='text-gray_r-11 mt-1 mb-4 text-center md:text-left'> + Buat akun sekarang lebih mudah dan terverifikasi + </h2> + + <label htmlFor='name' className='text-black font-bold'> + Tipe Akun + </label> + <div className='grid grid-cols-2 gap-x-3 mt-2 h-14 font-bold text-black hover:cursor-pointer'> + <div + className={` border rounded-md flex justify-center items-center transition-colors duration-300 ease-in-out ${ + isIndividuClicked ? 'bg-red-500 text-white' : '' + }`} + onClick={handleIndividuClick} + > + <p>Individu</p> + </div> + <div + className={` border rounded-md flex justify-center items-center transition-colors duration-300 ease-in-out ${ + isBisnisClicked ? 'bg-red-500 text-white' : '' + }`} + onClick={handleBisnisClick} + > + <p>Bisnis</p> + </div> + </div> + <div className='transition-opacity duration-300 ease-in-out'> + {isIndividuClicked && ( + <div className='opacity-100'> + <RegistrasiIndividu + chekValid={notValid} + buttonSubmitClick={buttonSubmitClick} + /> + </div> + )} + {isBisnisClicked && ( + <div className='opacity-100'> + <RegistrasiBisnis + chekValid={notValid} + buttonSubmitClick={buttonSubmitClick} + /> + </div> + )} + </div> + <section className='mt-2'> + <FormCaptcha /> + <TermCondition /> + <Button + type='submit' + colorScheme='red' + className='w-full mt-2' + size='lg' + onClick={handleSubmit} + isDisabled={ + !isCheckedTNC || mutation.isLoading || !isValidCaptcha + } + > + {mutation.isLoading ? 'Loading...' : 'Daftar'} + </Button> + </section> + <section className='flex justify-center items-center flex-col'> + <div className='text-gray_r-11 mt-4 text-center md:text-left'> + Sudah punya akun Indoteknik?{' '} + <Link + href='/login' + className='inline font-medium text-danger-500' + > + Masuk + </Link> + </div> + <div className='text-gray_r-11 mt-4 text-center md:text-left'> + Akun anda belum aktif?{' '} + <Link + href='/register?activation=email' + className='inline font-medium text-danger-500' + > + Aktivasi + </Link> + </div> + </section> </div> </section> - <section className="my-10 md:my-0"> - <PageContent path="/register" /> + <section className='my-10 md:my-0'> + <PageContent path='/register' /> </section> </div> <AccountActivation /> </div> - ) -} + ); +}; -export default Register
\ No newline at end of file +export default Register; diff --git a/src-migrate/modules/register/stores/useRegisterStore.ts b/src-migrate/modules/register/stores/useRegisterStore.ts index d8abf52b..273472be 100644 --- a/src-migrate/modules/register/stores/useRegisterStore.ts +++ b/src-migrate/modules/register/stores/useRegisterStore.ts @@ -1,7 +1,7 @@ import { create } from 'zustand'; import { RegisterProps } from '~/types/auth'; import { registerSchema } from '~/validations/auth'; -import { ZodError } from 'zod'; +import { boolean, ZodError } from 'zod'; type State = { form: RegisterProps; @@ -20,18 +20,33 @@ type Action = { openTNC: () => void; closeTNC: () => void; validate: () => void; + resetForm: () => void; }; export const useRegisterStore = create<State & Action>((set, get) => ({ form: { - company: '', + company_type_id: '', + business_name: '', name: '', + nama_wajib_pajak : '', email: '', + email_partner: '', password: '', phone: '', + sppkp_document: '', + npwp_document: '', + industry_id: '', + npwp: '', + sppkp: '', + is_pkp: '', + type_acc:'', + is_terdaftar:'', + alamat_bisnis:'', + alamat_wajib_pajak:'', }, updateForm: (name, value) => set((state) => ({ form: { ...state.form, [name]: value } })), + errors: {}, validate: () => { @@ -48,6 +63,7 @@ export const useRegisterStore = create<State & Action>((set, get) => ({ } } }, + isCheckedTNC: false, toggleCheckTNC: () => set((state) => ({ isCheckedTNC: !state.isCheckedTNC })), @@ -57,4 +73,27 @@ export const useRegisterStore = create<State & Action>((set, get) => ({ isValidCaptcha: false, updateValidCaptcha: (value) => set(() => ({ isValidCaptcha: value })), + + resetForm: () => set({ + form: { + company_type_id: '', + business_name: '', + name: '', + nama_wajib_pajak : '', + email: '', + email_partner: '', + password: '', + phone: '', + sppkp_document: '', + npwp_document: '', + industry_id: '', + npwp: '', + sppkp: '', + is_pkp: '', + type_acc:'', + is_terdaftar:'', + alamat_bisnis:'', + alamat_wajib_pajak:'', + }, + }), })); |
