diff options
| -rw-r--r-- | src/api/productApi.js | 21 | ||||
| -rw-r--r-- | src/components/ui/PopularProduct.jsx | 110 | ||||
| -rw-r--r-- | src/core/components/layouts/BasicLayout.jsx | 44 | ||||
| -rw-r--r-- | src/pages/index.jsx | 248 |
4 files changed, 179 insertions, 244 deletions
diff --git a/src/api/productApi.js b/src/api/productApi.js index b5f47bcf..dc96a77e 100644 --- a/src/api/productApi.js +++ b/src/api/productApi.js @@ -1,6 +1,5 @@ import axios from 'axios'; -// CLIENT MODE → untuk useQuery export const popularProductApi = () => { return async () => { const today = new Date(); @@ -8,25 +7,9 @@ export const popularProductApi = () => { (today - new Date(today.getFullYear(), 0, 0)) / 86400000 ); const page = (dayOfYear % 24) + 1; - - const res = await axios( + const dataPopularProducts = await axios( `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?q=*&page=${page}&orderBy=stock&priceFrom=1` ); - return res.data.response; + return dataPopularProducts.data.response; }; }; - -// SERVER MODE → untuk SSR -export async function popularProductApiSSR() { - const today = new Date(); - const dayOfYear = Math.floor( - (today - new Date(today.getFullYear(), 0, 0)) / 86400000 - ); - const page = (dayOfYear % 24) + 1; - - const res = await axios( - `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?q=*&page=${page}&orderBy=stock&priceFrom=1` - ); - - return res.data.response || null; -} diff --git a/src/components/ui/PopularProduct.jsx b/src/components/ui/PopularProduct.jsx index 0740798d..d3ae9e27 100644 --- a/src/components/ui/PopularProduct.jsx +++ b/src/components/ui/PopularProduct.jsx @@ -1,63 +1,61 @@ -import MobileView from '@/core/components/views/MobileView'; -import DesktopView from '@/core/components/views/DesktopView'; -import ProductSlider from '@/lib/product/components/ProductSlider'; -import ProductCard from '@/lib/product/components/ProductCard'; -import Link from '@/core/components/elements/Link/Link'; -import { PopularProductSkeleton } from '../skeleton/PopularProductSkeleton'; -import { useQuery } from 'react-query'; -import { popularProductApi } from '@/api/productApi'; +import { popularProductApi } from '@/api/productApi' +import MobileView from '@/core/components/views/MobileView' +import ProductSlider from '@/lib/product/components/ProductSlider' +import { useQuery } from 'react-query' +import { PopularProductSkeleton } from '../skeleton/PopularProductSkeleton' +import DesktopView from '@/core/components/views/DesktopView' +import ProductCard from '@/lib/product/components/ProductCard' +import Link from '@/core/components/elements/Link/Link' -export default function PopularProduct({ initialData }) { - const query = useQuery( - 'popularProducts', - popularProductApi(), - { - initialData: initialData ? { products: initialData.products } : undefined, - refetchOnMount: false - } - ); +const PopularProduct = () => { + const popularProduct = useQuery('popularProduct', popularProductApi()) - if (query.isLoading) return <PopularProductSkeleton />; - - const data = query.data; - - if (!data) return null; + if (popularProduct.isLoading) return <PopularProductSkeleton /> return ( - <> - {/* Mobile */} - <MobileView> - <div className="px-4"> - <div className="font-semibold mb-4 flex justify-between items-center"> - <p>Produk Ready Stock</p> - <Link href="/shop/search?orderBy=stock"> - <p className="text-danger-500 font-semibold">Lihat Semua</p> - </Link> + popularProduct.data && ( + <> + <MobileView> + <div className='px-4'> + <div className='font-semibold mb-4 flex justify-between items-center'><p> + Produk Ready Stock + </p> + <Link + href='/shop/search?orderBy=stock' + className='' + > + <p className='text-danger-500 font-semibold'>Lihat Semua</p> + </Link></div> + <ProductSlider products={popularProduct.data} simpleTitle /> </div> - - <ProductSlider products={data.products} simpleTitle /> - </div> - </MobileView> - - {/* Desktop */} - <DesktopView> - <div className="border border-gray_r-6 h-full overflow-auto"> - <div className="font-semibold text-center p-4 bg-gray_r-1 border-b border-gray_r-6 sticky top-0 z-10 flex justify-between items-center"> - <p>Produk Ready Stock</p> - <Link href="/shop/search?orderBy=stock"> - <p className="text-danger-500 font-semibold">Lihat Semua</p> - </Link> + </MobileView> + + <DesktopView> + <div className='border border-gray_r-6 h-full overflow-auto'> + <div className='font-semibold text-center p-4 bg-gray_r-1 border-b border-gray_r-6 sticky top-0 z-10 flex justify-between items-center'> + <p> + Produk Ready Stock + </p> + <Link + href='/shop/search?orderBy=stock' + className='' + > + <p className='text-danger-500 font-semibold'>Lihat Semua</p> + </Link> + </div> + <div className='h-full divide-y divide-gray_r-6'> + {popularProduct.data && + popularProduct.data.products.map((product) => ( + <div className='py-2' key={product.id}> + <ProductCard product={product} variant='horizontal' /> + </div> + ))} + </div> </div> - - <div className="h-full divide-y divide-gray_r-6"> - {data.products.map((product) => ( - <div className="py-2" key={product.id}> - <ProductCard product={product} variant="horizontal" /> - </div> - ))} - </div> - </div> - </DesktopView> - </> - ); + </DesktopView> + </> + ) + ) } + +export default PopularProduct diff --git a/src/core/components/layouts/BasicLayout.jsx b/src/core/components/layouts/BasicLayout.jsx index b13e807d..19e2c580 100644 --- a/src/core/components/layouts/BasicLayout.jsx +++ b/src/core/components/layouts/BasicLayout.jsx @@ -7,15 +7,12 @@ import { useProductContext } from '@/contexts/ProductContext'; import odooApi from '@/core/api/odooApi'; import whatsappUrl from '@/core/utils/whatsappUrl'; import Navbar from '../elements/Navbar/Navbar'; -import styles from './BasicLayout.module.css'; // Import modul CSS +import styles from './BasicLayout.module.css'; import useDevice from '@/core/hooks/useDevice'; -const AnimationLayout = dynamic(() => import('./AnimationLayout'), { - ssr: false, -}); -const BasicFooter = dynamic(() => import('../elements/Footer/BasicFooter'), { - ssr: false, -}); +// ❌ JANGAN dynamic UNTUK LAYOUT +import AnimationLayout from './AnimationLayout'; +import BasicFooter from '../elements/Footer/BasicFooter'; const BasicLayout = ({ children }) => { const [templateWA, setTemplateWA] = useState(null); @@ -27,7 +24,6 @@ const BasicLayout = ({ children }) => { const [isProductPage, setIsProductPage] = useState(false); const { isDesktop, isMobile } = useDevice(); - const router = useRouter(); const buttonRef = useRef(null); @@ -44,11 +40,10 @@ const BasicLayout = ({ children }) => { ) { setPayloadWa({ name: product?.name, - manufacture: product?.manufacture.name, + manufacture: product?.manufacture?.name, url: process.env.NEXT_PUBLIC_SELF_HOST + router.asPath, }); setTemplateWA('product'); - setUrlPath(router.asPath); } if (router.pathname.includes('/shop/product/')) { @@ -69,25 +64,17 @@ const BasicLayout = ({ children }) => { }; window.addEventListener('mouseout', handleMouseOut); - - return () => { - window.removeEventListener('mouseout', handleMouseOut); - }; + return () => window.removeEventListener('mouseout', handleMouseOut); }, []); useEffect(() => { if (highlight) { - // Set wobble animation after overlay highlight animation completes - const timer = setTimeout(() => setWobble(true), 1000); // Adjust timing if needed + const timer = setTimeout(() => setWobble(true), 1000); return () => clearTimeout(timer); } }, [highlight]); - const recordActivity = async (pathname) => { - const ONLY_ON_PATH = false; - const recordedPath = []; - if (ONLY_ON_PATH && !recordedPath.includes(pathname)) return; - + const recordActivity = async () => { const ip = await odooApi('GET', '/api/ip-address'); const data = new URLSearchParams({ page_title: document.title, @@ -117,9 +104,12 @@ const BasicLayout = ({ children }) => { onAnimationEnd={() => setHighlight(false)} /> )} + <Navbar isMobile={isMobile} /> + <AnimationLayout> {children} + {(!isProductPage || hasPrice) && ( <div className={`fixed ${ @@ -142,6 +132,7 @@ const BasicLayout = ({ children }) => { {isDesktop && 'Whatsapp'} </span> </a> + <a href={whatsappUrl(templateWA, payloadWA, urlPath)} className='elemen-whatsapp p-4 rounded-full bg-[#4FB84A] border border-green-300 flex items-center' @@ -152,24 +143,15 @@ const BasicLayout = ({ children }) => { <Image src='/images/socials/WHATSAPP.svg' alt='Whatsapp' - className='block sm:hidden' - width={36} - height={36} - loading='eager' - /> - <Image - src='/images/socials/WHATSAPP.svg' - alt='Whatsapp' - className='hidden sm:block' width={44} height={44} - loading='eager' /> </a> </div> </div> )} </AnimationLayout> + <BasicFooter /> </> ); diff --git a/src/pages/index.jsx b/src/pages/index.jsx index fd960e8a..ba8c44af 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -1,6 +1,7 @@ import { HeroBannerSkeleton } from '@/components/skeleton/BannerSkeleton'; import { PopularProductSkeleton } from '@/components/skeleton/PopularProductSkeleton'; import Seo from '@/core/components/Seo'; +import DelayRender from '@/core/components/elements/DelayRender/DelayRender'; import DesktopView from '@/core/components/views/DesktopView'; import MobileView from '@/core/components/views/MobileView'; import PreferredBrandSkeleton from '@/lib/home/components/Skeleton/PreferredBrandSkeleton'; @@ -9,118 +10,77 @@ import dynamic from 'next/dynamic'; import { useRef } from 'react'; import { getAuth } from '~/libs/auth'; import MediaNews from '../lib/home/components/MediaNews'; -import { popularProductApi, popularProductApiSSR } from '@/api/productApi'; -/* ============================ - * DYNAMIC IMPORT SETTINGS - * ============================ */ - -// Layout const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout') ); -// Popup -const PagePopupIformation = dynamic( - () => import('~/modules/popup-information'), - { ssr: false } -); - -// CATEGORY (SSR TRUE) -const CategoryPilihan = dynamic( - () => import('../lib/home/components/CategoryPilihan'), - { ssr: true } +const PagePopupIformation = dynamic(() => + import('~/modules/popup-information'), { + ssr: false + } ); -const CategoryDynamic = dynamic( - () => import('@/lib/home/components/CategoryDynamic'), - { ssr: true } +const CategoryPilihan = dynamic(() => + import('../lib/home/components/CategoryPilihan') ); - -const CategoryDynamicMobile = dynamic( - () => import('@/lib/home/components/CategoryDynamicMobile'), - { ssr: true } -); - -const CategoryHomeId = dynamic( - () => import('@/lib/home/components/CategoryHomeId'), - { ssr: true } -); - -// PRODUCT (SSR TRUE) -const PopularProduct = dynamic( - () => import('@/components/ui/PopularProduct'), - { loading: () => <PopularProductSkeleton />, ssr: true } +const HeroBanner = dynamic(() => import('@/components/ui/HeroBanner'), { + loading: () => <HeroBannerSkeleton />, +}); +const HeroBannerSecondary = dynamic( + () => import('@/components/ui/HeroBannerSecondary'), + { + loading: () => <HeroBannerSkeleton />, + } ); +const PopularProduct = dynamic(() => import('@/components/ui/PopularProduct'), { + loading: () => <PopularProductSkeleton />, +}); const PreferredBrand = dynamic( () => import('@/lib/home/components/PreferredBrand'), - { loading: () => <PreferredBrandSkeleton />, ssr: true } + { + loading: () => <PreferredBrandSkeleton />, + } ); const FlashSale = dynamic( () => import('@/lib/flashSale/components/FlashSale'), - { ssr: true } ); const ProgramPromotion = dynamic( - () => import('@/lib/home/components/PromotionProgram'), - { ssr: true } + () => import('@/lib/home/components/PromotionProgram') ); -// CSR ONLY COMPONENTS -const HeroBanner = dynamic( - () => import('@/components/ui/HeroBanner'), - { loading: () => <HeroBannerSkeleton />, ssr: false } +const BannerSection = dynamic(() => + import('@/lib/home/components/BannerSection') +); +const CategoryHomeId = dynamic( + () => import('@/lib/home/components/CategoryHomeId') ); -const HeroBannerSecondary = dynamic( - () => import('@/components/ui/HeroBannerSecondary'), - { loading: () => <HeroBannerSkeleton />, ssr: false } +const CategoryDynamic = dynamic(() => + import('@/lib/home/components/CategoryDynamic') ); -const BannerSection = dynamic( - () => import('@/lib/home/components/BannerSection'), - { ssr: false } +const CategoryDynamicMobile = dynamic(() => + import('@/lib/home/components/CategoryDynamicMobile') ); const CustomerReviews = dynamic( () => import('@/lib/review/components/CustomerReviews'), { ssr: false } -); - -const ServiceList = dynamic( - () => import('@/lib/home/components/ServiceList'), - { ssr: false } -); - - -/* ===================================== - * SERVER SIDE PROPS (HARUS DI LUAR) - * ===================================== */ +); // need to ssr:false +const ServiceList = dynamic(() => import('@/lib/home/components/ServiceList'), { + ssr: false, +}); // need to ssr: false - export async function getServerSideProps(context) { - const auth = getAuth(context.req); - - const popularProducts = await popularProductApiSSR(); - - return { - props: { - auth, - popularProducts - } - }; - } - - -/* ============================ - * MAIN PAGE COMPONENT - * ============================ */ - -export default function Home({ auth, popularProducts }) { +export default function Home({ categoryId }) { const bannerRef = useRef(null); const wrapperRef = useRef(null); + const auth = getAuth(); + const handleOnLoad = () => { wrapperRef.current.style.height = bannerRef.current?.querySelector(':first-child')?.clientHeight + 'px'; @@ -130,109 +90,121 @@ export default function Home({ auth, popularProducts }) { <> <BasicLayout> <Seo - title="Indoteknik.com: B2B Industrial Supply & Solution" - description="Temukan pilihan produk B2B Industri & Alat Teknik untuk Perusahaan, UMKM & Pemerintah dengan lengkap, mudah dan transparan." + title='Indoteknik.com: B2B Industrial Supply & Solution' + description='Temukan pilihan produk B2B Industri & Alat Teknik untuk Perusahaan, UMKM & Pemerintah dengan lengkap, mudah dan transparan.' additionalMetaTags={[ { - name: "keywords", + name: 'keywords', content: - "indoteknik, indoteknik.com, toko teknik, toko perkakas, jual genset, jual fogging, jual krisbow, harga krisbow, harga alat safety, harga pompa air" - } + 'indoteknik, indoteknik.com, toko teknik, toko perkakas, jual genset, jual fogging, jual krisbow, harga krisbow, harga alat safety, harga pompa air', + }, ]} - openGraph={{ - title: "Indoteknik.com: B2B Industrial Supply & Solution", - description: - "Temukan pilihan produk B2B Industri & Alat Teknik untuk Perusahaan, UMKM & Pemerintah dengan lengkap, mudah dan transparan.", - images: [ - { - url: "https://indoteknik.com/icon.jpg", - width: 800, - height: 600, - alt: "indoteknik.com" - } - ] - }} + openGraph={ + { + title : 'Indoteknik.com: B2B Industrial Supply & Solution', + description : 'Temukan pilihan produk B2B Industri & Alat Teknik untuk Perusahaan, UMKM & Pemerintah dengan lengkap, mudah dan transparan.', + images: [ + { + url: 'https://indoteknik.com/icon.jpg', + width: 800, + height: 600, + alt: 'indoteknik.com', + }, + ], + } + } /> <PagePopupIformation /> - {/* DESKTOP */} <DesktopView> <PagePopupInformation /> - - <div className="container mx-auto"> + <div className='container mx-auto'> <div - className="flex min-h-[400px] h-[460px]" + className='flex min-h-[400px] h-[460px]' ref={wrapperRef} onLoad={handleOnLoad} > - <div className="w-2/12"> + <div className='w-2/12'> <HeroBannerSecondary /> </div> - - <div className="w-7/12 px-1" ref={bannerRef}> + <div className='w-7/12 px-1' ref={bannerRef}> <HeroBanner /> </div> - - <div className="w-3/12"> - <PopularProduct initialData={popularProducts} /> + <div className='w-3/12'> + <DelayRender renderAfter={200}> + <PopularProduct /> + </DelayRender> </div> </div> - <div className="my-16 flex flex-col gap-y-8"> + <div className='my-16 flex flex-col gap-y-8'> <ServiceList /> <MediaNews /> - - <div id="flashsale"> + <div id='flashsale'> <PreferredBrand /> </div> - {!auth?.feature?.soApproval && ( <> - <ProgramPromotion /> - <FlashSale /> + <DelayRender renderAfter={200}> + <ProgramPromotion /> + </DelayRender> + <DelayRender renderAfter={200}> + <FlashSale /> + </DelayRender> </> )} - + {/* <PromotinProgram /> */} <CategoryPilihan /> <CategoryDynamic /> <CategoryHomeId /> - <BannerSection /> <CustomerReviews /> </div> </div> </DesktopView> - - {/* MOBILE */} <MobileView> <PagePopupInformation /> - <HeroBanner /> - - <div className="flex flex-col gap-y-4 my-6"> - <ServiceList /> - <MediaNews /> - - <div id="flashsale"> - <PreferredBrand /> - </div> - + <DelayRender renderAfter={200}> + <HeroBanner /> + </DelayRender> + <div className='flex flex-col gap-y-4 my-6'> + <DelayRender renderAfter={400}> + <ServiceList /> + <MediaNews /> + </DelayRender> + <DelayRender renderAfter={400}> + <div id='flashsale'> + <PreferredBrand /> + </div> + </DelayRender> {!auth?.feature?.soApproval && ( <> - <ProgramPromotion /> - <FlashSale /> + <DelayRender renderAfter={400}> + <ProgramPromotion /> + </DelayRender> + <DelayRender renderAfter={600}> + <FlashSale /> + </DelayRender> </> )} - - <CategoryPilihan /> - <CategoryDynamicMobile /> - - <PopularProduct initialData={popularProducts} /> - - <CategoryHomeId /> - <BannerSection /> - - <CustomerReviews /> + <DelayRender renderAfter={600}> + {/* <PromotinProgram /> */} + </DelayRender> + <DelayRender renderAfter={600}> + <CategoryPilihan /> + <CategoryDynamicMobile /> + </DelayRender> + <DelayRender renderAfter={800}> + <PopularProduct /> + </DelayRender> + <DelayRender renderAfter={1000}> + <CategoryHomeId /> + <BannerSection /> + </DelayRender> + <DelayRender renderAfter={1200}> + <CustomerReviews /> + </DelayRender> </div> </MobileView> </BasicLayout> |
