diff options
| author | it-fixcomart <it@fixcomart.co.id> | 2025-07-29 09:46:05 +0700 |
|---|---|---|
| committer | it-fixcomart <it@fixcomart.co.id> | 2025-07-29 09:46:05 +0700 |
| commit | 077467cf53b46d8049df8b812577cd1a03011eba (patch) | |
| tree | 0dc641a9acb1237a3caca3f7f8a157a3e938c0b8 /src/lib/home | |
| parent | 0d28dc8ff5fb8c5399e356ed6ecae4fce2019ca6 (diff) | |
| parent | dc31efb2fec4c7b79917324d922ae820c4b5bb50 (diff) | |
<hafid> merging new release
Diffstat (limited to 'src/lib/home')
| -rw-r--r-- | src/lib/home/components/BannerSection.jsx | 158 | ||||
| -rw-r--r-- | src/lib/home/components/CategoryHomeId.jsx | 2 | ||||
| -rw-r--r-- | src/lib/home/components/PopupBannerPromotion.jsx | 260 |
3 files changed, 372 insertions, 48 deletions
diff --git a/src/lib/home/components/BannerSection.jsx b/src/lib/home/components/BannerSection.jsx index 898f1bf5..1eac9592 100644 --- a/src/lib/home/components/BannerSection.jsx +++ b/src/lib/home/components/BannerSection.jsx @@ -1,62 +1,126 @@ import Link from '@/core/components/elements/Link/Link'; -import Image from 'next/image'; import { useEffect, useState } from 'react'; -import { bannerApi } from '../../../api/bannerApi'; - -const { useQuery } = require('react-query'); -const { default: bannerSectionApi } = require('../api/bannerSectionApi'); +import useDevice from '@/core/hooks/useDevice'; +import { Swiper, SwiperSlide } from 'swiper/react'; const BannerSection = () => { - const [data, setData] = useState(null); - const [shouldFetch, setShouldFetch] = useState(false); - + const [privateBrandData, setPrivateBrandData] = useState([]); + const [homeBannerData, setHomeBannerData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const { isMobile, isDesktop } = useDevice(); useEffect(() => { - const fetchCategoryData = async () => { - const res = await fetch('/api/banner-section'); - const { data } = await res.json(); - if (data) { - setData(data); + const fetchAllBanners = async () => { + try { + // Fetch private brand banners + const privateBrandRes = await fetch( + '/api/banner-section?type=private-brand' + ); + if (privateBrandRes.ok) { + const privateBrandResult = await privateBrandRes.json(); + setPrivateBrandData(privateBrandResult.data || []); + } + + // Fetch home banners + const homeBannerRes = await fetch( + '/api/banner-section?type=home-banner' + ); + if (homeBannerRes.ok) { + const homeBannerResult = await homeBannerRes.json(); + setHomeBannerData(homeBannerResult.data || []); + } + } catch (err) { + setError('Network error'); + } finally { + setLoading(false); } }; - fetchCategoryData(); + fetchAllBanners(); }, []); - // 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); - } - }, - } - ); + // if (loading) return <div>Loading...</div>; + // if (error) return <div>Error: {error}</div>; - const bannerSection = data; return ( - bannerSection && - bannerSection?.length > 0 && ( - <div className='grid grid-cols-1 sm:grid-cols-2 gap-4'> - {bannerSection?.map((banner) => ( - <Link key={banner.id} href={banner.url}> - <Image - width={1024} - height={512} - quality={85} - src={banner.image} - alt={banner.name} - className='h-auto w-full rounded' - loading='eager' - /> - </Link> - ))} - </div> - ) + <div className='space-y-12'> + {/* Private Brand Section */} + {privateBrandData && privateBrandData.length > 0 && ( + <div className='px-4 sm:px-0'> + <div + className='text-black font-semibold sm:text-h-lg mb-6' + id='private-brand' + > + Private Brand + </div> + + {/* Desktop Grid View */} + {isDesktop && ( + <div className='grid grid-cols-3 sm:grid-cols-3 gap-4 rounded-md'> + {privateBrandData.map((banner, index) => ( + <Link key={banner.id || index} href={banner.url || '#'}> + <img + width={439} + height={150} + quality={85} + src={banner.image} + alt={banner.name || `Private Brand Banner ${index + 1}`} + className='rounded hover:scale-105 transition duration-500 ease-in-out' + loading='eager' + /> + </Link> + ))} + </div> + )} + + {/* Mobile Swiper View */} + {isMobile && ( + <Swiper slidesPerView={1.1} spaceBetween={8} freeMode> + {privateBrandData.map((banner, index) => ( + <SwiperSlide key={banner.id || index}> + <Link href={banner.url || '#'}> + <img + width={350} + height={100} + quality={70} + src={banner.image} + alt={banner.name || `Private Brand Banner ${index + 1}`} + className='rounded' + loading='eager' + /> + </Link> + </SwiperSlide> + ))} + </Swiper> + )} + </div> + )} + + {/* Home Banner Section */} + {homeBannerData && homeBannerData.length > 0 && ( + <div className='grid grid-cols-1 sm:grid-cols-2 gap-4'> + {homeBannerData.map((banner, index) => ( + <div key={banner.id || index} className='relative'> + <Link href={banner.url || '#'}> + <img + width={1024} + height={512} + quality={85} + src={banner.image} + alt={banner.name || `Home Banner ${index + 1}`} + // className='h-40 w-full rounded object-cover transition-transform hover:scale-105' + className='h-auto w-full rounded' + loading='eager' + onError={(e) => { + e.target.style.display = 'none'; + }} + /> + </Link> + </div> + ))} + </div> + )} + </div> ); }; diff --git a/src/lib/home/components/CategoryHomeId.jsx b/src/lib/home/components/CategoryHomeId.jsx index 9f436dac..db473a1c 100644 --- a/src/lib/home/components/CategoryHomeId.jsx +++ b/src/lib/home/components/CategoryHomeId.jsx @@ -8,7 +8,7 @@ const CategoryHomeId = () => { return ( <div> <h1 className='font-semibold text-[14px] sm:text-h-lg mb-6 px-4 sm:px-0'> - Kategori Pilihan + Paket Bundling / Paket UMKM </h1> <div className='flex flex-col gap-y-10'> {categoryHomeIds.data?.map((id) => ( diff --git a/src/lib/home/components/PopupBannerPromotion.jsx b/src/lib/home/components/PopupBannerPromotion.jsx new file mode 100644 index 00000000..538f35e6 --- /dev/null +++ b/src/lib/home/components/PopupBannerPromotion.jsx @@ -0,0 +1,260 @@ +import { useRouter } from 'next/router'; +import { useEffect, useState, useRef } from 'react'; +import Image from 'next/image'; +import Link from 'next/link'; +import { X } from 'lucide-react'; +import { getAuth } from '~/libs/auth'; +import useDevice from '@/core/hooks/useDevice'; +import { createPortal } from 'react-dom'; + +const PagePopupInformation = () => { + const router = useRouter(); + const isHomePage = router.pathname === '/'; + const auth = getAuth(); + const { isDesktop } = useDevice(); + + const [active, setActive] = useState(false); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + const popupRef = useRef(null); + const [position, setPosition] = useState({ x: 20, y: window.innerHeight - 170 }); + const dragStartPos = useRef({ x: 0, y: 0 }); + const isDragging = useRef(false); + const isTouching = useRef(false); + const [isSnapping, setIsSnapping] = useState(false); + const [containerLeft, setContainerLeft] = useState(0); + + const [isClient, setIsClient] = useState(false); + + useEffect(() => { + setIsClient(true); + }, []); + + useEffect(() => { + if (isHomePage && !auth) { + setActive(true); + fetchData(); + } + }, [isHomePage, auth]); + + const fetchData = async () => { + try { + const res = await fetch(`/api/hero-banner?type=dragable-banner`); + const { data } = await res.json(); + if (Array.isArray(data) && data[0]?.image) { + setData(data[0]); + } else { + setActive(false); + } + } catch (error) { + console.error('Failed to fetch popup banner:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + const handleResizeOrZoom = () => { + if (!popupRef.current) return; + + const popupWidth = popupRef.current.offsetWidth || 85; + const popupHeight = popupRef.current.offsetHeight || 85; + + setPosition({ + x: 20, + y: window.innerHeight - popupHeight - 20, + }); + }; + + window.addEventListener('resize', handleResizeOrZoom); + + return () => { + window.removeEventListener('resize', handleResizeOrZoom); + }; + }, []); + + + const updateContainerLeft = () => { + const container = document.querySelector('.container'); + if (container) { + setContainerLeft(container.getBoundingClientRect().left); + } + }; + + useEffect(() => { + updateContainerLeft(); + window.addEventListener('resize', updateContainerLeft); + window.addEventListener('scroll', updateContainerLeft); + return () => { + window.removeEventListener('resize', updateContainerLeft); + window.removeEventListener('scroll', updateContainerLeft); + }; + }, []); + + useEffect(() => { + if (isDesktop && containerLeft) { + const popupWidth = popupRef.current?.offsetWidth || 85; + setPosition({ + x: containerLeft - popupWidth - 20, + y: window.innerHeight - 130, + }); + } + }, [isDesktop, containerLeft]); + + const startDrag = (clientX, clientY) => { + dragStartPos.current = { x: clientX - position.x, y: clientY - position.y }; + isDragging.current = false; + setIsSnapping(false); + }; + + const moveDrag = (clientX, clientY) => { + isDragging.current = true; + const popupWidth = popupRef.current?.offsetWidth || 85; + const popupHeight = popupRef.current?.offsetHeight || 85; + const maxX = window.innerWidth - popupWidth - 20; + + const topPadding = isDesktop ? 0 : 130; + const bottomPadding = isDesktop ? 0 : 80; + const maxY = window.innerHeight - popupHeight - bottomPadding; + + const minX = 0; + + let newX = clientX - dragStartPos.current.x; + let newY = clientY - dragStartPos.current.y; + + newX = Math.max(minX, Math.min(newX, maxX)); + newY = Math.max(topPadding, Math.min(newY, maxY)); + + setPosition({ x: newX, y: newY }); + }; + + const endDrag = () => { + if (isDragging.current) { + setIsSnapping(true); + + if (isDesktop) { + const popupWidth = popupRef.current?.offsetWidth || 85; + setPosition({ + x: containerLeft - popupWidth - 20, + y: window.innerHeight - 130, + }); + } else { + const popupWidth = popupRef.current?.offsetWidth || 85; + const screenMiddle = window.innerWidth / 2; + + setPosition(pos => ({ + x: pos.x + popupWidth / 2 < screenMiddle ? 20 : window.innerWidth - popupWidth - 20, + y: pos.y, + })); + } + } + isDragging.current = false; + isTouching.current = false; + }; + + + + const handleMouseDown = e => { + e.preventDefault(); + startDrag(e.clientX, e.clientY); + window.addEventListener('mousemove', handleMouseMove); + window.addEventListener('mouseup', handleMouseUp); + }; + + const handleMouseMove = e => moveDrag(e.clientX, e.clientY); + + const handleMouseUp = () => { + endDrag(); + window.removeEventListener('mousemove', handleMouseMove); + window.removeEventListener('mouseup', handleMouseUp); + }; + + const handleTouchStart = e => { + if (e.touches.length === 1) { + isTouching.current = true; + startDrag(e.touches[0].clientX, e.touches[0].clientY); + } + }; + + useEffect(() => { + const handleTouchMove = e => { + if (isTouching.current) { + e.preventDefault(); + moveDrag(e.touches[0].clientX, e.touches[0].clientY); + } + }; + + const handleTouchEnd = () => endDrag(); + + window.addEventListener('touchmove', handleTouchMove, { passive: false }); + window.addEventListener('touchend', handleTouchEnd); + return () => { + window.removeEventListener('touchmove', handleTouchMove); + window.removeEventListener('touchend', handleTouchEnd); + }; + }, []); + + if (!active || !data || loading) return null; + + const popupContent = ( + <div + ref={popupRef} + className={`relative pointer-events-auto ${isSnapping ? 'transition-transform duration-300 ease-out' : ''}`} + style={{ + transform: `translate(${position.x}px, ${position.y}px)`, + cursor: 'grab', + width: '85px', + }} + onMouseDown={handleMouseDown} + onTouchStart={handleTouchStart} + > + <Link + href={typeof data.url === 'boolean' && data.url === false ? '/' : data.url} + onClick={e => { + if (isDragging.current) { + e.preventDefault(); + isDragging.current = false; + } else { + setActive(false); + } + }} + draggable="false" + > + <Image + src={data.image} + alt={data.name || 'popup'} + width={85} + height={85} + className="w-full h-auto select-none" + draggable="false" + /> + </Link> + + <button + onClick={() => setActive(false)} + className="absolute -top-2 -right-2 z-10 p-1 bg-red-500 rounded-full hover:bg-red-600 transition-colors" + aria-label="Close popup" + > + <X className="w-3 h-3 text-white" /> + </button> + </div> + ); + + if (isDesktop && isClient) { + return createPortal( + <div className="fixed z-[9999] pointer-events-none top-0 left-0 w-screen h-screen"> + {popupContent} + </div>, + document.body + ); + } + + return ( + <div className="fixed z-[9999] pointer-events-none top-[40px] left-0"> + {popupContent} + </div> + ); +}; + +export default PagePopupInformation; |
