diff options
| author | trisusilo48 <tri.susilo@altama.co.id> | 2024-11-18 11:35:58 +0700 |
|---|---|---|
| committer | trisusilo48 <tri.susilo@altama.co.id> | 2024-11-18 11:35:58 +0700 |
| commit | fb58a58715a7f5530a60479487457e5e0a1a41ec (patch) | |
| tree | 733f0332f127a4d6bc931330ec1cd103968dbb7d | |
| parent | 0d4278bd482d2ec2563b29cb3597eb8c7227a2d7 (diff) | |
| parent | bde516b6b39cccfe8ac3248cd7f85592e6298d7a (diff) | |
Merge branch 'new-release' into feature/integrasi_biteship
32 files changed, 901 insertions, 570 deletions
diff --git a/package.json b/package.json index 30b46ac9..31b9f589 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "react-query": "^3.39.3", "react-select": "^5.8.0", "react-web-share": "^2.0.2", + "redis": "^4.7.0", "snakecase-keys": "^5.5.0", "swiper": "^8.4.4", "tw-merge": "^0.0.1-alpha.3", diff --git a/src-migrate/modules/page-content/index.tsx b/src-migrate/modules/page-content/index.tsx index 3423ca8b..54ee0a04 100644 --- a/src-migrate/modules/page-content/index.tsx +++ b/src-migrate/modules/page-content/index.tsx @@ -10,28 +10,21 @@ type Props = { const PageContent = ({ path }: Props) => { const [localData, setData] = useState<PageContentProps>(); const [shouldFetch, setShouldFetch] = useState(false); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { - const localData = localStorage.getItem(`page-content:${path}`); - if (localData) { - setData(JSON.parse(localData)); - }else{ - setShouldFetch(true); - } - },[]) - - const { data, isLoading } = useQuery<PageContentProps>( - `page-content:${path}`, - async () => await getPageContent({ path }), { - enabled: shouldFetch, - onSuccess: (data) => { - if (data) { - localStorage.setItem(`page-content:${path}`, JSON.stringify(data)); - setData(data); - } - }, - } - ); + 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 (!localData) return ''; 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/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 ec606423..b9f4be91 100644 --- a/src-migrate/modules/product-detail/components/Information.tsx +++ b/src-migrate/modules/product-detail/components/Information.tsx @@ -35,7 +35,7 @@ const Information = ({ product }: Props) => { 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[]>( @@ -68,6 +68,7 @@ const Information = ({ product }: Props) => { }, [selectedVariant]); const handleOnChange = (vals: any) => { + setDisableFilter(true); let code = vals.replace(/\s-\s.*$/, '').trim(); let variant = variantOptions.find((item) => item.code === code); setSelectedVariant(variant); @@ -85,6 +86,7 @@ const Information = ({ product }: Props) => { }; const handleOnKeyUp = (e: any) => { + setDisableFilter(false); setInputValue(e.target.value); }; @@ -98,6 +100,7 @@ const Information = ({ product }: Props) => { </span>{' '} </label> <AutoComplete + disableFilter={disableFilter} openOnFocus className='form-input' onChange={(vals) => handleOnChange(vals)} @@ -107,8 +110,9 @@ const Information = ({ product }: Props) => { ref={inputRef} value={inputValue as string} onChange={(e) => handleOnKeyUp(e)} + onFocus={() => setDisableFilter(true)} /> - <InputRightElement> + <InputRightElement className='mr-4'> <ChevronDownIcon className='h-6 w-6 text-gray-500' onClick={() => inputRef?.current?.focus()} diff --git a/src-migrate/types/product.ts b/src-migrate/types/product.ts index fb9c888c..85ea702a 100644 --- a/src-migrate/types/product.ts +++ b/src-migrate/types/product.ts @@ -3,6 +3,7 @@ import { IProductVariantDetail } from './productVariant'; export interface IProduct { id: number; image: string; + image_mobile: string; code: string; display_name: string; name: string; diff --git a/src/components/ui/HeroBanner.jsx b/src/components/ui/HeroBanner.jsx index 64838b85..2eea5915 100644 --- a/src/components/ui/HeroBanner.jsx +++ b/src/components/ui/HeroBanner.jsx @@ -6,7 +6,7 @@ import 'swiper/css/pagination'; import { Swiper, SwiperSlide } from 'swiper/react'; import Image from 'next/image'; -import { useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useQuery } from 'react-query'; import { bannerApi } from '@/api/bannerApi'; @@ -27,7 +27,20 @@ const swiperBanner = { }; const HeroBanner = () => { - const heroBanner = useQuery('heroBanner', bannerApi({ type: 'index-a-1' })); + // const heroBanner = useQuery('heroBanner', bannerApi({ type: 'index-a-1' })); + const [data, setData] = useState(null); + useEffect(() => { + const fetchData = async () => { + const res = await fetch(`/api/hero-banner?type=index-a-1`); + const { data } = await res.json(); + if (data) { + setData(data); + } + }; + + fetchData(); + }, []); + const heroBanner = data; const swiperBannerMobile = { ...swiperBanner, @@ -44,9 +57,9 @@ const HeroBanner = () => { }; const BannerComponent = useMemo(() => { - if (!heroBanner.data) return null; + if (!heroBanner) return null; - return heroBanner.data.map((banner, index) => ( + return heroBanner.map((banner, index) => ( <SwiperSlide key={index}> <Link href={banner.url} className='w-full h-auto'> <Image @@ -56,22 +69,22 @@ const HeroBanner = () => { width={1152} height={768} className='w-full h-auto' - priority={index === 0} - loading={index === 0 ? 'eager' : 'lazy'} - placeholder="blur" - blurDataURL="/images/indoteknik-placeholder.png" - sizes="(max-width: 768px) 100vw, 50vw" + priority={index === 0} + loading={index === 0 ? 'eager' : 'lazy'} + placeholder='blur' + blurDataURL='/images/indoteknik-placeholder.png' + sizes='(max-width: 768px) 100vw, 50vw' /> </Link> </SwiperSlide> )); - }, [heroBanner.data]); + }, [heroBanner]); return ( <> <MobileView> <SmoothRender - isLoaded={heroBanner.data?.length > 0} + isLoaded={heroBanner?.length > 0} height='68vw' duration='750ms' delay='100ms' @@ -81,7 +94,7 @@ const HeroBanner = () => { </MobileView> <DesktopView> - {heroBanner.data?.length > 0 && ( + {heroBanner?.length > 0 && ( <Swiper {...swiperBannerDesktop}>{BannerComponent}</Swiper> )} </DesktopView> diff --git a/src/components/ui/HeroBannerSecondary.jsx b/src/components/ui/HeroBannerSecondary.jsx index a7b32a4a..6074c9a6 100644 --- a/src/components/ui/HeroBannerSecondary.jsx +++ b/src/components/ui/HeroBannerSecondary.jsx @@ -1,39 +1,58 @@ -import Link from '@/core/components/elements/Link/Link' -import { getRandomInt } from '@/utils/getRandomInt' -import Image from 'next/image' -import { useMemo } from 'react' -import { useQuery } from 'react-query' -import { HeroBannerSkeleton } from '../skeleton/BannerSkeleton' -import { bannerApi } from '@/api/bannerApi' +import Link from '@/core/components/elements/Link/Link'; +import { getRandomInt } from '@/utils/getRandomInt'; +import Image from 'next/image'; +import { useMemo, useEffect, useState } from 'react'; +import { useQuery } from 'react-query'; +import { HeroBannerSkeleton } from '../skeleton/BannerSkeleton'; +import { bannerApi } from '@/api/bannerApi'; const HeroBannerSecondary = () => { - const heroBannerSecondary = useQuery('heroBannerSecondary', bannerApi({ type: 'index-a-2' })) + const [heroBannerSecondary, setHeroBannerSecondary] = useState([]); + const [isLoading, setIsLoading] = useState(false); + // const heroBannerSecondary = useQuery( + // 'heroBannerSecondary', + // bannerApi({ type: 'index-a-2' }) + // ); - const randomIndex = useMemo(() => { - if (!heroBannerSecondary.data) return null - const length = heroBannerSecondary.data?.length - return getRandomInt(length) - }, [heroBannerSecondary.data]) + useEffect(() => { + const fetchData = async () => { + setIsLoading(true); + const res = await fetch(`/api/hero-banner?type=index-a-2`); + const { data } = await res.json(); + if (data) { + setHeroBannerSecondary(data); + } + setIsLoading(false); + }; + + fetchData(); + }, []); - if (heroBannerSecondary.isLoading) return <HeroBannerSkeleton /> + const randomIndex = useMemo(() => { + if (!heroBannerSecondary) return null; + const length = heroBannerSecondary?.length; + return getRandomInt(length); + }, [heroBannerSecondary]); + if (isLoading) return <HeroBannerSkeleton />; return ( - heroBannerSecondary.data && randomIndex !== null && ( - <Link href={heroBannerSecondary.data[randomIndex].url} className="h-full"> + heroBannerSecondary && + randomIndex !== null && ( + <Link href={heroBannerSecondary[randomIndex]?.url} className='h-full'> <Image - src={heroBannerSecondary.data[randomIndex].image} + src={heroBannerSecondary[randomIndex]?.image} width={512} height={1024} - alt={heroBannerSecondary.data[randomIndex].name} - className="object-cover object-center h-full" - loading="lazy" - placeholder="blur" - blurDataURL="/images/indoteknik-placeholder.png" - sizes="(max-width: 768px) 100vw, 50vw" + alt={heroBannerSecondary[randomIndex]?.name} + className='object-cover object-center h-full' + loading='lazy' + placeholder='blur' + blurDataURL='/images/indoteknik-placeholder.png' + sizes='(max-width: 768px) 100vw, 50vw' /> </Link> ) ); -} +}; -export default HeroBannerSecondary +export default HeroBannerSecondary; diff --git a/src/core/components/elements/Footer/BasicFooter.jsx b/src/core/components/elements/Footer/BasicFooter.jsx index 4688b15b..05dc4d8c 100644 --- a/src/core/components/elements/Footer/BasicFooter.jsx +++ b/src/core/components/elements/Footer/BasicFooter.jsx @@ -264,7 +264,7 @@ const InformationCenter = () => ( <li className='text-gray_r-12/80 flex items-center'> <PhoneArrowUpRightIcon className='w-[18px] mr-2' /> <a href='tel:02129338828' target='_blank' rel='noreferrer'> - (021) 2933-8828 + (021) 29338828 </a> </li> <li className='text-gray_r-12/80 flex items-center'> diff --git a/src/core/components/elements/Footer/SimpleFooter.jsx b/src/core/components/elements/Footer/SimpleFooter.jsx index 371b1652..1f5e13e7 100644 --- a/src/core/components/elements/Footer/SimpleFooter.jsx +++ b/src/core/components/elements/Footer/SimpleFooter.jsx @@ -13,7 +13,7 @@ const SimpleFooter = () => ( <ul className='flex flex-col gap-y-2'> <li className='text-gray_r-12/80 flex items-center'> <PhoneArrowUpRightIcon className='w-[18px] mr-2' /> - <a href='tel:02129338828'>(021) 2933-8828 / 29</a> + <a href='tel:02129338828'>(021) 29338828</a> </li> <li className='text-gray_r-12/80 flex items-center'> <EnvelopeIcon className='w-[18px] mr-2' /> diff --git a/src/core/components/elements/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx index 04cf76d1..fa3df5bf 100644 --- a/src/core/components/elements/Navbar/NavbarDesktop.jsx +++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx @@ -391,7 +391,7 @@ const SocialMedias = () => ( > <NextImage src='/images/socials/youtube.webp' - alt='Youtube - Indoteknik.com' + // alt='Youtube - Indoteknik.com' width={24} height={24} /> @@ -403,7 +403,7 @@ const SocialMedias = () => ( > <NextImage src='/images/socials/tiktok.png' - alt='TikTok - Indoteknik.com' + // alt='TikTok - Indoteknik.com' width={24} height={24} /> @@ -423,7 +423,7 @@ const SocialMedias = () => ( > <NextImage src='/images/socials/Facebook.png' - alt='Facebook - Indoteknik.com' + // alt='Facebook - Indoteknik.com' width={24} height={24} /> @@ -435,7 +435,7 @@ const SocialMedias = () => ( > <NextImage src='/images/socials/Instagram.png' - alt='Instagram - Indoteknik.com' + // alt='Instagram - Indoteknik.com' width={24} height={24} /> @@ -447,7 +447,7 @@ const SocialMedias = () => ( > <NextImage src='/images/socials/Linkedin.png' - alt='Linkedin - Indoteknik.com' + // alt='Linkedin - Indoteknik.com' width={24} height={24} /> @@ -459,7 +459,7 @@ const SocialMedias = () => ( > <NextImage src='/images/socials/g_maps.png' - alt='Maps - Indoteknik.com' + // alt='Maps - Indoteknik.com' width={24} height={24} /> diff --git a/src/core/components/elements/Navbar/TopBanner.jsx b/src/core/components/elements/Navbar/TopBanner.jsx index f438ae67..709495ce 100644 --- a/src/core/components/elements/Navbar/TopBanner.jsx +++ b/src/core/components/elements/Navbar/TopBanner.jsx @@ -1,22 +1,37 @@ import Image from 'next/image'; -import { useQuery } from 'react-query';import useDevice from '@/core/hooks/useDevice' +import { useQuery } from 'react-query'; +import useDevice from '@/core/hooks/useDevice'; import odooApi from '@/core/api/odooApi'; import SmoothRender from '~/components/ui/smooth-render'; import Link from '../Link/Link'; import { background } from '@chakra-ui/react'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; const TopBanner = ({ onLoad = () => {} }) => { - const { isDesktop, isMobile } = useDevice() - const topBanner = useQuery({ - queryKey: 'topBanner', - queryFn: async () => await odooApi('GET', '/api/v1/banner?type=top-banner'), - refetchOnWindowFocus: false, - }); + const [topBanner, setTopBanner] = useState([]); + const { isDesktop, isMobile } = useDevice(); + + useEffect(() => { + const fetchData = async () => { + const res = await fetch(`/api/hero-banner?type=top-banner`); + const { data } = await res.json(); + if (data) { + setTopBanner(data); + } + }; + + fetchData(); + }, []); + + // const topBanner = useQuery({ + // queryKey: 'topBanner', + // queryFn: async () => await odooApi('GET', '/api/v1/banner?type=top-banner'), + // refetchOnWindowFocus: false, + // }); // const backgroundColor = topBanner.data?.[0]?.backgroundColor || 'transparent'; - const hasData = topBanner.data?.length > 0; - const data = topBanner.data?.[0] || null; + const hasData = topBanner?.length > 0; + const data = topBanner?.[0] || null; useEffect(() => { if (hasData) { @@ -31,17 +46,15 @@ const TopBanner = ({ onLoad = () => {} }) => { duration='700ms' delay='300ms' className='h-auto' - > + > <Link - href={data?.url} - className="block bg-cover bg-center h-3 md:h-6 lg:h-[36px]" - style={{ - backgroundImage: `url('${data?.image}')`, - }} - > - </Link> - - </SmoothRender> + href={data?.url} + className='block bg-cover bg-center h-3 md:h-6 lg:h-[36px]' + style={{ + backgroundImage: `url('${data?.image}')`, + }} + ></Link> + </SmoothRender> ); }; diff --git a/src/core/utils/whatsappUrl.js b/src/core/utils/whatsappUrl.js index 7a129aa6..c840e105 100644 --- a/src/core/utils/whatsappUrl.js +++ b/src/core/utils/whatsappUrl.js @@ -2,28 +2,31 @@ import { getAuth } from "./auth" const whatsappUrl = (template = 'default', payload, urlPath = null) => { let user = getAuth() - if(!user){ - if(urlPath) return `/login?next=${urlPath}` - if(!urlPath) return '/login' - } + // if(!user){ + // if(urlPath) return `/login?next=${urlPath}` + // if(!urlPath) return '/login' + // } let parentName = user.parentName || '-' let url = 'https://wa.me/6281717181922' let text = 'Hallo Indoteknik.com,' + if(user){ + text += `Saya ${user.name}, Saya dari ${parentName}` + } switch (template) { case 'product': - text += ` Saya ${user.name} , Saya dari ${parentName} Saya mencari barang dibawah ini\n\n: Brand = ${payload?.manufacture}\n\n Item Name = ${payload?.name}\n\nLink : ${payload?.url}` + text += ` Saya mencari barang dibawah ini\n\n: Brand = ${payload?.manufacture}\n\n Item Name = ${payload?.name}\n\nLink : ${payload?.url}` break case 'productWeight': - text += ` Saya ${user.name} , Saya dari ${parentName} Saya mencari barang dibawah ini\n\n: Brand = ${payload?.manufacture}\n\n Item Name = ${payload?.name}\n\nLink : ${payload?.url}` + text += ` Saya mencari barang dibawah ini\n\n: Brand = ${payload?.manufacture}\n\n Item Name = ${payload?.name}\n\nLink : ${payload?.url}` break case 'productSearch': - text += `Saya lagi cari-cari produk ${payload?.name}, bisa bantu saya cari produknya?` + text += ` Saya lagi cari-cari produk ${payload?.name}, bisa bantu saya cari produknya?` break case null: - text += `Saya ${user.name}, Saya dari ${parentName} Bisa tolong bantu kebutuhan saya?` + text += ` Bisa tolong bantu kebutuhan saya?` break; default: - text += `Saya ${user.name}, Saya dari ${parentName} Bisa tolong bantu kebutuhan saya?` + text += ` Bisa tolong bantu kebutuhan saya?` break } if (text) url += `?text=${encodeURI(text)}` diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 4dd715d7..33cfaa87 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -40,6 +40,18 @@ const SELF_PICKUP_ID = 32; const { checkoutApi } = require('../api/checkoutApi'); const { getProductsCheckout } = require('../api/checkoutApi'); +function convertToInternational(number) { + if (typeof number !== 'string') { + throw new Error("Input harus berupa string"); + } + + if (number.startsWith('08')) { + return '+62' + number.slice(2); + } + + return number; +} + const Checkout = () => { const router = useRouter(); const query = router.query.source ?? null; @@ -454,6 +466,13 @@ const Checkout = () => { transaction_id: isCheckouted.id, }); + gtag('set', 'user_data', { + email: auth.email, + phone_number: convertToInternational(auth.mobile) ?? convertToInternational(auth.phone), + }); + + gtag('config', 'AW-954540379', { ' allow_enhanced_conversions':true } ) ; + for (const product of products) deleteItemCart({ productId: product.id }); if (grandTotal > 0) { const payment = await axios.post( diff --git a/src/lib/flashSale/components/FlashSale.jsx b/src/lib/flashSale/components/FlashSale.jsx index 89c46de4..8be1d7a6 100644 --- a/src/lib/flashSale/components/FlashSale.jsx +++ b/src/lib/flashSale/components/FlashSale.jsx @@ -69,7 +69,7 @@ const FlashSaleProduct = ({ flashSaleId }) => { useEffect(() => { const loadProducts = async () => { const dataProducts = await productSearchApi({ - query: `fq=flashsale_id_i:${flashSaleId}&fq=flashsale_price_f:[1 TO *]&limit=500&orderBy=flashsale-price-asc`, + query: `fq=flashsale_id_i:${flashSaleId}&fq=flashsale_price_f:[1 TO *]&limit=500&orderBy=flashsale-price-asc&source=similar`, operation: 'AND', }); setProducts(dataProducts.response); diff --git a/src/lib/flashSale/components/FlashSaleNonDisplay.jsx b/src/lib/flashSale/components/FlashSaleNonDisplay.jsx index c91de2be..4b420fac 100644 --- a/src/lib/flashSale/components/FlashSaleNonDisplay.jsx +++ b/src/lib/flashSale/components/FlashSaleNonDisplay.jsx @@ -56,7 +56,7 @@ const FlashSaleProduct = ({ flashSaleId }) => { useEffect(() => { const loadProducts = async () => { const dataProducts = await productSearchApi({ - query: `fq=-flashsale_id_i:${flashSaleId}&fq=flashsale_price_f:[1 TO *]&limit=25&orderBy=flashsale-discount-desc`, + query: `fq=-flashsale_id_i:${flashSaleId}&fq=flashsale_price_f:[1 TO *]&limit=25&orderBy=flashsale-discount-desc&source=similar`, operation: 'AND', }); setProducts(dataProducts.response); diff --git a/src/lib/home/components/BannerSection.jsx b/src/lib/home/components/BannerSection.jsx index 60d38f8f..303b5c4b 100644 --- a/src/lib/home/components/BannerSection.jsx +++ b/src/lib/home/components/BannerSection.jsx @@ -11,27 +11,33 @@ const BannerSection = () => { const [shouldFetch, setShouldFetch] = useState(false); useEffect(() => { - const localData = localStorage.getItem('Homepage_bannerSection'); - if (localData) { - setData(JSON.parse(localData)); - }else{ - setShouldFetch(true); - } - }, []); - - // const fetchBannerSection = async () => await bannerSectionApi(); - const getBannerSection = useQuery('bannerSection', bannerApi({ type: 'home-banner' }), { - enabled: shouldFetch, - onSuccess: (data) => { + const fetchCategoryData = async () => { + const res = await fetch('/api/banner-section'); + const { data } = await res.json(); if (data) { - localStorage.setItem('Homepage_bannerSection', JSON.stringify(data)); setData(data); } - }, - }); + }; - const bannerSection = data; + fetchCategoryData(); + }, []); + // const fetchBannerSection = async () => await bannerSectionApi(); + const getBannerSection = useQuery( + 'bannerSection', + bannerApi({ type: 'home-banner' }), + { + enabled: shouldFetch, + onSuccess: (data) => { + if (data) { + localStorage.setItem('Homepage_bannerSection', JSON.stringify(data)); + setData(data); + } + }, + } + ); + + const bannerSection = data; return ( bannerSection && bannerSection?.length > 0 && ( diff --git a/src/lib/home/components/CategoryDynamic.jsx b/src/lib/home/components/CategoryDynamic.jsx index e62575f7..cc4f42b7 100644 --- a/src/lib/home/components/CategoryDynamic.jsx +++ b/src/lib/home/components/CategoryDynamic.jsx @@ -1,81 +1,33 @@ -import React, { useEffect, useState, useCallback } from 'react'; -import { - fetchCategoryManagementSolr, - fetchCategoryManagementVersion, -} from '../api/categoryManagementApi'; +import React, { useEffect, useState } from 'react'; +import { fetchCategoryManagementSolr } from '../api/categoryManagementApi'; +import { Skeleton } from '@chakra-ui/react'; import NextImage from 'next/image'; import Link from 'next/link'; import { createSlug } from '@/core/utils/slug'; -import { Skeleton } from '@chakra-ui/react'; import { Swiper, SwiperSlide } from 'swiper/react'; import 'swiper/css'; import 'swiper/css/navigation'; import 'swiper/css/pagination'; import { Pagination } from 'swiper'; -const saveToLocalStorage = (key, data, version) => { - const now = new Date(); - const item = { - value: data, - version: version, - lastFetchedTime: now.getTime(), - }; - localStorage.setItem(key, JSON.stringify(item)); -}; - -const getFromLocalStorage = (key) => { - const itemStr = localStorage.getItem(key); - if (!itemStr) return null; - - const item = JSON.parse(itemStr); - return item; -}; - -const getElapsedTime = (lastFetchedTime) => { - const now = new Date(); - return now.getTime() - lastFetchedTime; -}; - const CategoryDynamic = () => { const [categoryManagement, setCategoryManagement] = useState([]); - const [isLoading, setIsLoading] = useState(false); - - const loadBrand = useCallback(async () => { - const cachedData = getFromLocalStorage('homepage_categoryDynamic'); - - if (cachedData) { - // Hitung selisih waktu antara saat ini dengan waktu terakhir data di-fetch - const elapsedTime = getElapsedTime(cachedData.lastFetchedTime); - - if (elapsedTime < 24 * 60 * 60 * 1000) { - setCategoryManagement(cachedData.value); - return; - } - } + const [isLoading, setIsLoading] = useState(true); - const latestVersion = await fetchCategoryManagementVersion(); - if (cachedData && cachedData.version === latestVersion) { - // perbarui waktu - saveToLocalStorage( - 'homepage_categoryDynamic', - cachedData.value, - latestVersion - ); - setCategoryManagement(cachedData.value); - } else { + useEffect(() => { + const fetchCategoryData = async () => { setIsLoading(true); - const items = await fetchCategoryManagementSolr(); + const res = await fetch('/api/category-management'); + const { data } = await res.json(); + if (data) { + setCategoryManagement(data); + } setIsLoading(false); + }; - saveToLocalStorage('homepage_categoryDynamic', items, latestVersion); - setCategoryManagement(items); - } + fetchCategoryData(); }, []); - useEffect(() => { - loadBrand(); - }, [loadBrand]); - const swiperBanner = { modules: [Pagination], classNames: 'mySwiper', @@ -90,104 +42,99 @@ const CategoryDynamic = () => { return ( <div> {categoryManagement && - categoryManagement?.map((category) => { - return ( - <Skeleton key={category.id} isLoaded={!isLoading}> - <div key={category.id}> - <div className='bagian-judul flex flex-row justify-start items-center gap-3 mb-4 mt-4'> - <h1 className='font-semibold text-[14px] sm:text-h-lg mr-2'> - {category.name} - </h1> - <Link - href={createSlug( - '/shop/category/', - category?.name, - category?.category_id - )} - className='!text-red-500 font-semibold' - > - Lihat Semua - </Link> - </div> + categoryManagement.map((category) => ( + <Skeleton key={category.id} isLoaded={!isLoading}> + <div key={category.id}> + <div className='bagian-judul flex flex-row justify-start items-center gap-3 mb-4 mt-4'> + <h1 className='font-semibold text-[14px] sm:text-h-lg mr-2'> + {category.name} + </h1> + <Link + href={createSlug( + '/shop/category/', + category?.name, + category?.category_id + )} + className='!text-red-500 font-semibold' + > + Lihat Semua + </Link> + </div> - {/* Swiper for SubCategories */} - <Swiper {...swiperBanner}> - {category.categories.map((subCategory) => { - return ( - <SwiperSlide key={subCategory.id}> - <div className='border rounded justify-start items-start '> - <div className='p-3'> - <div className='flex flex-row border rounded mb-2 justify-start items-center'> - <NextImage - src={ - subCategory.image - ? subCategory.image - : '/images/noimage.jpeg' - } - alt={subCategory.name} - width={90} - height={30} - className='object-fit p-4' - /> - <div className='bagian-judul flex flex-col justify-center items-start gap-2 ml-2'> - <h2 className='font-semibold text-lg mr-2'> - {subCategory?.name} - </h2> + <Swiper {...swiperBanner}> + {category?.categories?.map((subCategory) => ( + <SwiperSlide key={subCategory.id}> + <div className='border rounded justify-start items-start '> + <div className='p-3'> + <div className='flex flex-row border rounded mb-2 justify-start items-center'> + <NextImage + src={ + subCategory.image + ? subCategory.image + : '/images/noimage.jpeg' + } + alt={subCategory.name} + width={90} + height={30} + className='object-fit p-4' + /> + <div className='bagian-judul flex flex-col justify-center items-start gap-2 ml-2'> + <h2 className='font-semibold text-lg mr-2'> + {subCategory?.name} + </h2> + <Link + href={createSlug( + '/shop/category/', + subCategory?.name, + subCategory?.id_level_2 + )} + className='!text-red-500 font-semibold' + > + Lihat Semua + </Link> + </div> + </div> + <div className='grid grid-cols-2 gap-2 overflow-y-auto max-h-[240px] min-h-[240px] content-start'> + {subCategory.child_frontend_id_i.map( + (childCategory) => ( + <div key={childCategory.id} className=''> <Link href={createSlug( '/shop/category/', - subCategory?.name, - subCategory?.id_level_2 + childCategory?.name, + childCategory?.id_level_3 )} - className='!text-red-500 font-semibold' + className='flex flex-row gap-2 border rounded group hover:border-red-500' > - Lihat Semua + <NextImage + src={ + childCategory.image + ? childCategory.image + : '/images/noimage.jpeg' + } + alt={childCategory.name} + className='p-2 ml-1' + width={40} + height={40} + /> + <div className='bagian-judul flex flex-col justify-center items-center gap-2 break-words line-clamp-2 group-hover:text-red-500'> + <h3 className='font-semibold line-clamp-2 group-hover:text-red-500 text-sm mr-2'> + {childCategory.name} + </h3> + </div> </Link> </div> - </div> - <div className='grid grid-cols-2 gap-2 overflow-y-auto max-h-[240px] min-h-[240px] content-start'> - {subCategory.child_frontend_id_i.map( - (childCategory) => ( - <div key={childCategory.id} className=''> - <Link - href={createSlug( - '/shop/category/', - childCategory?.name, - childCategory?.id_level_3 - )} - className='flex flex-row gap-2 border rounded group hover:border-red-500' - > - <NextImage - src={ - childCategory.image - ? childCategory.image - : '/images/noimage.jpeg' - } - alt={childCategory.name} - className='p-2 ml-1' - width={40} - height={40} - /> - <div className='bagian-judul flex flex-col justify-center items-center gap-2 break-words line-clamp-2 group-hover:text-red-500'> - <h3 className='font-semibold line-clamp-2 group-hover:text-red-500 text-sm mr-2'> - {childCategory.name} - </h3> - </div> - </Link> - </div> - ) - )} - </div> - </div> + ) + )} </div> - </SwiperSlide> - ); - })} - </Swiper> - </div> - </Skeleton> - ); - })} + </div> + </div> + </SwiperSlide> + ))} + </Swiper> + </div> + </Skeleton> + ))} </div> ); }; diff --git a/src/lib/home/components/CategoryDynamicMobile.jsx b/src/lib/home/components/CategoryDynamicMobile.jsx index 55654b0e..67ae6f5f 100644 --- a/src/lib/home/components/CategoryDynamicMobile.jsx +++ b/src/lib/home/components/CategoryDynamicMobile.jsx @@ -9,71 +9,26 @@ import { fetchCategoryManagementVersion, } from '../api/categoryManagementApi'; -const saveToLocalStorage = (key, data, version) => { - const now = new Date(); - const item = { - value: data, - version: version, - lastFetchedTime: now.getTime(), - }; - localStorage.setItem(key, JSON.stringify(item)); -}; - -const getFromLocalStorage = (key) => { - const itemStr = localStorage.getItem(key); - if (!itemStr) return null; - - const item = JSON.parse(itemStr); - return item; -}; - -const getElapsedTime = (lastFetchedTime) => { - const now = new Date(); - return now.getTime() - lastFetchedTime; -}; - const CategoryDynamicMobile = () => { const [selectedCategory, setSelectedCategory] = useState({}); const [categoryManagement, setCategoryManagement] = useState([]); const [isLoading, setIsLoading] = useState(false); - const loadCategoryManagement = useCallback(async () => { - const cachedData = getFromLocalStorage('homepage_categoryDynamic'); - - if (cachedData) { - // Hitung selisih waktu antara saat ini dengan waktu terakhir data di-fetch - const elapsedTime = getElapsedTime(cachedData.lastFetchedTime); - - if (elapsedTime < 24 * 60 * 60 * 1000) { - setCategoryManagement(cachedData.value); - return; - } - } - - const latestVersion = await fetchCategoryManagementVersion(); - if (cachedData && cachedData.version === latestVersion) { - // perbarui waktu - saveToLocalStorage( - 'homepage_categoryDynamic', - cachedData.value, - latestVersion - ); - setCategoryManagement(cachedData.value); - } else { + useEffect(() => { + const fetchCategoryData = async () => { setIsLoading(true); - const items = await fetchCategoryManagementSolr(); + const res = await fetch('/api/category-management'); + const { data } = await res.json(); + if (data) { + setCategoryManagement(data); + } setIsLoading(false); + }; - saveToLocalStorage('homepage_categoryDynamic', items, latestVersion); - setCategoryManagement(items); - } + fetchCategoryData(); }, []); useEffect(() => { - loadCategoryManagement(); - }, [loadCategoryManagement]); - - useEffect(() => { if (categoryManagement?.length > 0) { const initialSelections = categoryManagement.reduce((acc, category) => { if (category.categories.length > 0) { diff --git a/src/lib/home/components/PreferredBrand.jsx b/src/lib/home/components/PreferredBrand.jsx index eefced60..b7a30503 100644 --- a/src/lib/home/components/PreferredBrand.jsx +++ b/src/lib/home/components/PreferredBrand.jsx @@ -1,49 +1,50 @@ -import { Swiper, SwiperSlide } from 'swiper/react' -import { Navigation, Pagination, Autoplay } from 'swiper'; -import { useCallback, useEffect, useState } from 'react' -import usePreferredBrand from '../hooks/usePreferredBrand' -import PreferredBrandSkeleton from './Skeleton/PreferredBrandSkeleton' -import BrandCard from '@/lib/brand/components/BrandCard' -import useDevice from '@/core/hooks/useDevice' -import Link from '@/core/components/elements/Link/Link' -import axios from 'axios' +import { Swiper, SwiperSlide } from 'swiper/react'; +import { Navigation, Pagination, Autoplay } from 'swiper'; +import { useCallback, useEffect, useState } from 'react'; +import usePreferredBrand from '../hooks/usePreferredBrand'; +import PreferredBrandSkeleton from './Skeleton/PreferredBrandSkeleton'; +import BrandCard from '@/lib/brand/components/BrandCard'; +import useDevice from '@/core/hooks/useDevice'; +import Link from '@/core/components/elements/Link/Link'; +import axios from 'axios'; const PreferredBrand = () => { - let query = '' - let params = 'prioritas' - const [isLoading, setIsLoading] = useState(true) - const [startWith, setStartWith] = useState(null) - const [manufactures, setManufactures] = useState([]) + let query = ''; + let params = 'prioritas'; + const [isLoading, setIsLoading] = useState(true); + const [startWith, setStartWith] = useState(null); + const [manufactures, setManufactures] = useState([]); const loadBrand = useCallback(async () => { - setIsLoading(true) - const name = startWith ? `${startWith}*` : '' - const result = await axios(`${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/brands?rows=20`) - - setIsLoading(false) - setManufactures((manufactures) => [...result.data]) - }, [startWith]) + setIsLoading(true); + const name = startWith ? `${startWith}*` : ''; + const result = await axios( + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/preferredBrand?rows=20` + ); + setIsLoading(false); + setManufactures((manufactures) => [...result.data]); + }, [startWith]); const toggleStartWith = (alphabet) => { - setManufactures([]) + setManufactures([]); if (alphabet == startWith) { - setStartWith(null) - return + setStartWith(null); + return; } - setStartWith(alphabet) - } + setStartWith(alphabet); + }; useEffect(() => { - loadBrand() - }, []) + loadBrand(); + }, []); // const { preferredBrands } = usePreferredBrand(query) - const { isMobile, isDesktop } = useDevice() + const { isMobile, isDesktop } = useDevice(); const swiperBanner = { - modules:[Navigation, Pagination, Autoplay], + modules: [Navigation, Pagination, Autoplay], autoplay: { delay: 4000, - disableOnInteraction: false + disableOnInteraction: false, }, loop: true, className: 'h-[70px] md:h-[100px] w-full', @@ -53,13 +54,17 @@ const PreferredBrand = () => { dynamicBullets: true, dynamicMainBullets: isMobile ? 6 : 8, clickable: true, - } - } - const preferredBrandsData = manufactures ? manufactures.slice(0, 20) : [] + }, + }; + const preferredBrandsData = manufactures ? manufactures.slice(0, 20) : []; return ( <div className='px-4 sm:px-0'> <div className='flex justify-between items-center mb-4'> - <h1 className='font-semibold text-[14px] sm:text-h-lg'><Link href='/shop/brands' className='!text-black font-semibold'>Brand Pilihan</Link></h1> + <h1 className='font-semibold text-[14px] sm:text-h-lg'> + <Link href='/shop/brands' className='!text-black font-semibold'> + Brand Pilihan + </Link> + </h1> {isDesktop && ( <Link href='/shop/brands' className='!text-red-500 font-semibold'> Lihat Semua @@ -79,7 +84,7 @@ const PreferredBrand = () => { )} </div> </div> - ) -} + ); +}; -export default PreferredBrand
\ No newline at end of file +export default PreferredBrand; diff --git a/src/lib/home/components/PromotionProgram.jsx b/src/lib/home/components/PromotionProgram.jsx index 7433e7f0..562fa138 100644 --- a/src/lib/home/components/PromotionProgram.jsx +++ b/src/lib/home/components/PromotionProgram.jsx @@ -10,32 +10,50 @@ const BannerSection = () => { const { isMobile, isDesktop } = useDevice(); const [data, setData] = useState(null); const [shouldFetch, setShouldFetch] = useState(false); - useEffect(() => { - const localData = localStorage.getItem('Homepage_promotionProgram'); - if (localData) { - setData(JSON.parse(localData)); - }else{ - setShouldFetch(true); - } - },[]) - - const getPromotionProgram = useQuery( - 'promotionProgram', - bannerApi({ type: 'banner-promotion' }),{ - enabled: shouldFetch, - onSuccess: (data) => { - if (data) { - localStorage.setItem('Homepage_promotionProgram', JSON.stringify(data)); - setData(data); - } + const fetchData = async () => { + const res = await fetch(`/api/hero-banner?type=banner-promotion`); + const { data } = await res.json(); + if (data) { + setData(data); } - } - ); + }; + + fetchData(); + }, []); + + // useEffect(() => { + // const localData = localStorage.getItem('Homepage_promotionProgram'); + // if (localData) { + // setData(JSON.parse(localData)); + // } else { + // setShouldFetch(true); + // } + // }, []); - const promotionProgram = data + // const getPromotionProgram = useQuery( + // 'promotionProgram', + // bannerApi({ type: 'banner-promotion' }), + // { + // enabled: shouldFetch, + // onSuccess: (data) => { + // if (data) { + // localStorage.setItem( + // 'Homepage_promotionProgram', + // JSON.stringify(data) + // ); + // setData(data); + // } + // }, + // } + // ); - if (getPromotionProgram?.isLoading && !data) { + const promotionProgram = data; + + // if (getPromotionProgram?.isLoading && !data) { + // return <BannerPromoSkeleton />; + // } + if (!data) { return <BannerPromoSkeleton />; } @@ -62,24 +80,22 @@ const BannerSection = () => { </Link> )} </div> - {isDesktop && - promotionProgram && - promotionProgram?.length > 0 && ( - <div className='grid grid-cols-3 sm:grid-cols-3 gap-4 rounded-md'> - {promotionProgram?.map((banner) => ( - <Link key={banner.id} href={banner.url}> - <Image - width={439} - height={150} - quality={85} - src={banner.image} - alt={banner.name} - className='h-auto w-full rounded hover:scale-105 transition duration-500 ease-in-out' - /> - </Link> - ))} - </div> - )} + {isDesktop && promotionProgram && promotionProgram?.length > 0 && ( + <div className='grid grid-cols-3 sm:grid-cols-3 gap-4 rounded-md'> + {promotionProgram?.map((banner) => ( + <Link key={banner.id} href={banner.url}> + <Image + width={439} + height={150} + quality={85} + src={banner.image} + alt={banner.name} + className='h-auto w-full rounded hover:scale-105 transition duration-500 ease-in-out' + /> + </Link> + ))} + </div> + )} {isMobile && ( <Swiper slidesPerView={1.1} spaceBetween={8} freeMode> diff --git a/src/lib/product/components/Product/ProductDesktopVariant.jsx b/src/lib/product/components/Product/ProductDesktopVariant.jsx index f4569574..5dfd452b 100644 --- a/src/lib/product/components/Product/ProductDesktopVariant.jsx +++ b/src/lib/product/components/Product/ProductDesktopVariant.jsx @@ -264,11 +264,17 @@ const ProductDesktopVariant = ({ product.manufacture.id.toString() )} > - <Image - width={100} - src={product.manufacture.logo} - alt={product.manufacture.name} - /> + {product?.manufacture.logo ? ( + <Image + width={100} + src={product.manufacture.logo} + alt={product.manufacture.name} + /> + ) : ( + <p className='font-bold text-red-500'> + {product.manufacture.name} + </p> + )} </Link> </div> </div> @@ -434,7 +440,7 @@ const ProductDesktopVariant = ({ </div> <div> <Skeleton - isLoaded={sla} + isLoaded={!isLoadingSLA} h='21px' // w={16} className={ diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx index d3b50302..3e6a6913 100644 --- a/src/lib/product/components/ProductCard.jsx +++ b/src/lib/product/components/ProductCard.jsx @@ -10,12 +10,13 @@ import { sellingProductFormat } from '@/core/utils/formatValue'; import { createSlug } from '@/core/utils/slug'; import whatsappUrl from '@/core/utils/whatsappUrl'; import useUtmSource from '~/hooks/useUtmSource'; +import useDevice from '@/core/hooks/useDevice'; const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { const router = useRouter(); const utmSource = useUtmSource(); const [discount, setDiscount] = useState(0); - + const { isDesktop, isMobile } = useDevice(); let voucherPastiHemat = 0; voucherPastiHemat = product?.newVoucherPastiHemat[0]; @@ -26,9 +27,13 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { }); 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]); const URL = { product: @@ -143,7 +148,7 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { <div className='p-2 sm:p-3 pb-3 text-caption-2 sm:text-body-2 leading-5'> <div className='flex justify-between '> {product?.manufacture?.name ? ( - <Link href={URL.manufacture} className='mb-1 mt-1'> + <Link href={URL.manufacture} className='mb-1 mt-1 truncate'> {product.manufacture.name} </Link> ) : ( diff --git a/src/pages/_document.jsx b/src/pages/_document.jsx index 6af6294f..4b67c3f9 100644 --- a/src/pages/_document.jsx +++ b/src/pages/_document.jsx @@ -115,6 +115,19 @@ export default function MyDocument() { }} /> + <Script + async + id='gtag-config' + strategy='afterInteractive' + dangerouslySetInnerHTML={{ + __html: ` + gtag('config', 'AW-954540379/fCU8CI3Y8OoZENvClMcD', { + 'phone_conversion_number': '(021) 29338828' + }); + `, + }} + /> + {/* <Script id='tawk-script-tag' strategy='afterInteractive' diff --git a/src/pages/api/banner-section.js b/src/pages/api/banner-section.js new file mode 100644 index 00000000..7d7040c0 --- /dev/null +++ b/src/pages/api/banner-section.js @@ -0,0 +1,44 @@ +import odooApi from '@/core/api/odooApi'; +import { createClient } from 'redis'; + +const client = createClient(); + +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; + +export default async function handler(req, res) { + try { + await connectRedis(); + const cacheKey = 'hero-banner'; + // await client.del(cacheKey); + let cachedData = await client.get(cacheKey); + + if (cachedData) { + const data = JSON.parse(cachedData); + return res.status(200).json({ data }); + } else { + const dataBannerSections = await odooApi( + 'GET', + '/api/v1/banner?type=home-banner' + ); + + // Simpan hasil fetch ke Redis dengan masa kadaluarsa 3 hari (259200 detik) + await client.set( + cacheKey, + JSON.stringify(dataBannerSections), + 'EX', + 259200 + ); + + return res.status(200).json({ data: dataBannerSections }); + } + } catch (error) { + console.error('Error interacting with Redis or fetching data:', error); + return res.status(500).json({ error: 'Internal Server Error' }); + } +} diff --git a/src/pages/api/category-management.js b/src/pages/api/category-management.js new file mode 100644 index 00000000..f05d8644 --- /dev/null +++ b/src/pages/api/category-management.js @@ -0,0 +1,85 @@ +import { createClient } from 'redis'; +// import { fetchCategoryManagementSolr } from '../../lib/home/api/categoryManagementApi'; +const client = createClient(); +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; + +export default async function handler(req, res) { + try { + await connectRedis(); + // await client.del('homepage_categoryDynamic'); + + let cachedData; + if (req.method === 'GET') { + cachedData = await client.get('homepage_categoryDynamic'); + + if (!cachedData) { + const items = await fetchCategoryManagementSolr(); + await client.set( + 'homepage_categoryDynamic', + JSON.stringify(items), + 'EX', + 259200 // Expiry 3 hari + ); + cachedData = await client.get('homepage_categoryDynamic'); + } + const data = cachedData ? JSON.parse(cachedData) : null; + res.status(200).json({ data }); + } else { + res.setHeader('Allow', ['GET']); + res.status(405).end(`Method ${req.method} Not Allowed`); + } + } catch (error) { + console.error('Error interacting with Redis:', error); + res.status(500).json({ error: 'Error interacting with Redis' }); + } +} + +const fetchCategoryManagementSolr = async () => { + let sort = 'sort=sequence_i asc'; + try { + const response = await fetch( + `http://34.101.189.218:8983/solr/category_management/query?q=*:*&q.op=OR&indent=true&${sort}&&rows=20` + ); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + const promotions = await map(data.response.docs); + return promotions; + } catch (error) { + console.error('Error fetching promotion data:', error); + return []; + } +}; +const map = async (promotions) => { + return promotions.map((promotion) => { + let parsedCategories = promotion.categories.map((category) => { + // Parse string JSON utama + let parsedCategory = JSON.parse(category); + + // Parse setiap elemen di child_frontend_id_i jika ada + if (parsedCategory.child_frontend_id_i) { + parsedCategory.child_frontend_id_i = + parsedCategory.child_frontend_id_i.map((child) => JSON.parse(child)); + } + + return parsedCategory; + }); + let productMapped = { + id: promotion.id, + name: promotion.name_s, + image: promotion.image_s, + sequence: promotion.sequence_i, + numFound: promotion.numFound_i, + categories: parsedCategories, + category_id: promotion.category_id_i, + }; + return productMapped; + }); +}; diff --git a/src/pages/api/hero-banner.js b/src/pages/api/hero-banner.js new file mode 100644 index 00000000..7a348cfa --- /dev/null +++ b/src/pages/api/hero-banner.js @@ -0,0 +1,45 @@ +import odooApi from '@/core/api/odooApi'; +import { createClient } from 'redis'; + +const client = createClient(); + +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; + +export default async function handler(req, res) { + const { type } = req.query; + try { + await connectRedis(); + const cacheKey = `homepage_bannerSection_${type}`; + // await client.del(cacheKey); + let cachedData = await client.get(cacheKey); + + if (cachedData) { + const data = JSON.parse(cachedData); + return res.status(200).json({ data }); + } else { + const dataBannerSections = await odooApi( + 'GET', + `/api/v1/banner?type=${type}` + ); + + // Simpan hasil fetch ke Redis dengan masa kadaluarsa 3 hari (259200 detik) + await client.set( + cacheKey, + JSON.stringify(dataBannerSections), + 'EX', + 259200 + ); + cachedData = await client.get(cacheKey); + return res.status(200).json({ data: cachedData }); + } + } catch (error) { + console.error('Error interacting with Redis or fetching data:', error); + return res.status(500).json({ error: 'Internal Server Error' }); + } +} diff --git a/src/pages/api/page-content.js b/src/pages/api/page-content.js new file mode 100644 index 00000000..3cb8a237 --- /dev/null +++ b/src/pages/api/page-content.js @@ -0,0 +1,44 @@ +import { createClient } from 'redis'; +import { getPageContent } from '~/services/pageContent'; +// import { fetchCategoryManagementSolr } from '../../lib/home/api/categoryManagementApi'; +const client = createClient(); +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; + +export default async function handler(req, res) { + const { path } = req.query; + try { + await connectRedis(); + // await client.del('onbording-popup'); + + let cachedData; + if (req.method === 'GET') { + cachedData = await client.get(`page-content:${path}`); + + if (!cachedData) { + const items = await getPageContent({ path }); + console.log('items', items); + await client.set( + `page-content:${path}`, + JSON.stringify(items), + 'EX', + 604800 // Expiry 1 minggu + ); + cachedData = await client.get(`page-content:${path}`); + } + const data = cachedData ? JSON.parse(cachedData) : null; + res.status(200).json({ data }); + } else { + res.setHeader('Allow', ['GET']); + res.status(405).end(`Method ${req.method} Not Allowed`); + } + } catch (error) { + console.error('Error interacting with Redis:', error); + res.status(500).json({ error: 'Error interacting with Redis' }); + } +} diff --git a/src/pages/api/shop/brands.js b/src/pages/api/shop/brands.js index 9c2824b3..d56e4b13 100644 --- a/src/pages/api/shop/brands.js +++ b/src/pages/api/shop/brands.js @@ -1,8 +1,20 @@ import axios from 'axios'; +import { createClient } from 'redis'; const SOLR_HOST = process.env.SOLR_HOST; +const client = createClient(); + +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; export default async function handler(req, res) { + await connectRedis(); + try { let params = '*:*'; let sort = @@ -11,12 +23,12 @@ export default async function handler(req, res) { if (req.query.params) { rows = 100; - switch (req?.query?.params) { + switch (req.query.params) { case 'level_s': params = 'level_s:prioritas'; break; case 'search': - params = `name_s:"${req?.query?.q.toLowerCase()}"`; + params = `name_s:"${req.query.q.toLowerCase()}"`; sort = ''; rows = 1; break; @@ -24,11 +36,11 @@ export default async function handler(req, res) { params = `name_s:${req.query.params}`.toLowerCase(); } } - if(req.query.rows) rows = req.query.rows; - + if (req.query.rows) rows = req.query.rows; + const url = `${SOLR_HOST}/solr/brands/select?q=${params}&q.op=OR&indent=true&rows=${rows}&${sort}`; - let brands = await axios(url); - let dataBrands = responseMap(brands.data.response.docs); + const brands = await axios(url); + const dataBrands = responseMap(brands.data.response.docs); res.status(200).json(dataBrands); } catch (error) { @@ -39,13 +51,11 @@ export default async function handler(req, res) { const responseMap = (brands) => { return brands.map((brand) => { - let brandMapping = { + return { id: brand.id, name: brand.display_name_s, logo: brand.image_s || '', - sequance: brand.sequence_i || '', + sequence: brand.sequence_i || '', }; - - return brandMapping; }); }; diff --git a/src/pages/api/shop/preferredBrand.js b/src/pages/api/shop/preferredBrand.js new file mode 100644 index 00000000..4cb35c84 --- /dev/null +++ b/src/pages/api/shop/preferredBrand.js @@ -0,0 +1,61 @@ +import axios from 'axios'; +import { createClient } from 'redis'; + +const SOLR_HOST = process.env.SOLR_HOST; +const client = createClient(); + +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; + +export default async function handler(req, res) { + await connectRedis(); + + try { + let params = '*:*'; + let sort = + 'sort=if(exists(sequence_i),0,1) asc,sequence_i asc, if(exists(image_s),0,1) asc '; + let rows = 20; + + if (req.query.params) { + rows = 20; + switch (req.query.params) { + case 'level_s': + params = 'level_s:prioritas'; + break; + case 'search': + params = `name_s:"${req.query.q.toLowerCase()}"`; + sort = ''; + rows = 1; + break; + default: + params = `name_s:${req.query.params}`.toLowerCase(); + } + } + if (req.query.rows) rows = req.query.rows; + + const url = `${SOLR_HOST}/solr/brands/select?q=${params}&q.op=OR&indent=true&rows=${rows}&${sort}`; + const brands = await axios(url); + const dataBrands = responseMap(brands.data.response.docs); + + res.status(200).json(dataBrands); + } catch (error) { + console.error('Error fetching data from Solr:', error); + res.status(500).json({ error: 'Internal Server Error' }); + } +} + +const responseMap = (brands) => { + return brands.map((brand) => { + return { + id: brand.id, + name: brand.display_name_s, + logo: brand.image_s || '', + sequence: brand.sequence_i || '', + }; + }); +}; diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index ace281f7..65927bbc 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -19,6 +19,10 @@ export default async function handler(req, res) { source = '', } = req.query; + + + console.log('fq new', fq); + let { stock = '' } = req.query; let paramOrderBy = ''; @@ -73,8 +77,9 @@ export default async function handler(req, res) { const formattedQuery = `(${newQ .split(' ') - .map((term) => `${term}*`) + .map((term) => (term.length < 2 ? term : `${term}*`)) // Tambahkan '*' hanya jika panjang kata >= 2 .join(' ')})`; + const mm = checkQ.length > 2 ? checkQ.length > 5 @@ -88,11 +93,24 @@ export default async function handler(req, res) { 'price_tier1_v2_f:[1 TO *]', ]; - if (fq && source != 'similar') { - filterQueries.push(fq); + + if (fq && source != 'similar' && typeof fq != 'string') { + // filterQueries.push(fq); + fq.push(...filterQueries); } const fq_ = filterQueries.join(' AND '); + let keywords = newQ; + if (source === 'similar' || checkQ.length < 3) { + if (checkQ.length < 2 || checkQ[1].length < 2) { + keywords = newQ ; + } else { + keywords = newQ + '*'; + } + } else { + keywords = formattedQuery; + } + let offset = (page - 1) * limit; let parameter = [ 'facet.field=manufacture_name_s', @@ -101,13 +119,7 @@ export default async function handler(req, res) { 'indent=true', `facet.query=${escapeSolrQuery(q)}`, `q.op=OR`, - `q=${ - source == 'similar' || checkQ.length < 3 - ? checkQ.length < 2 - ? newQ - : newQ + '*' - : formattedQuery - }`, + `q=${keywords}`, `defType=edismax`, 'qf=name_s description_clean_t category_name manufacture_name_s variants_code_t variants_name_t category_id_ids default_code_s manufacture_id_i category_id_i ', `start=${parseInt(offset)}`, @@ -152,7 +164,7 @@ export default async function handler(req, res) { if (stock) parameter.push(`fq=stock_total_f:{1 TO *}`); // Single fq in url params - // if (typeof fq === 'string') parameter.push(`fq=${encodeURIComponent(fq)}`); + if (typeof fq === 'string') parameter.push(`fq=${encodeURIComponent(fq)}`); // Multi fq in url params if (Array.isArray(fq)) parameter = parameter.concat( diff --git a/src/utils/solrMapping.js b/src/utils/solrMapping.js index f896a6a8..01a8587c 100644 --- a/src/utils/solrMapping.js +++ b/src/utils/solrMapping.js @@ -43,6 +43,7 @@ export const productMappingSolr = (products, pricelist) => { let productMapped = { id: product.product_id_i || '', image: product.image_s || '', + imageMobile: product.image_mobile_s || '', code: product.default_code_s || '', description: product.description_t || '', displayName: product.display_name_s || '', |
