diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/core/components/elements/Footer/BasicFooter.jsx | 2 | ||||
| -rw-r--r-- | src/core/components/elements/Navbar/NavbarDesktop.jsx | 76 | ||||
| -rw-r--r-- | src/core/components/elements/Navbar/TopBanner.jsx | 9 | ||||
| -rw-r--r-- | src/lib/category/api/popularProduct.js | 31 | ||||
| -rw-r--r-- | src/lib/category/components/Category.jsx | 35 | ||||
| -rw-r--r-- | src/lib/category/components/PopularBrand.jsx | 83 | ||||
| -rw-r--r-- | src/lib/flashSale/components/FlashSaleNonDisplay.jsx | 66 | ||||
| -rw-r--r-- | src/lib/home/components/CategoryDynamic.jsx | 3 | ||||
| -rw-r--r-- | src/lib/product/components/ProductCard.jsx | 4 | ||||
| -rw-r--r-- | src/lib/product/components/ProductSearch.jsx | 5 | ||||
| -rw-r--r-- | src/pages/api/shop/search.js | 10 |
11 files changed, 274 insertions, 50 deletions
diff --git a/src/core/components/elements/Footer/BasicFooter.jsx b/src/core/components/elements/Footer/BasicFooter.jsx index 6129143d..4beea604 100644 --- a/src/core/components/elements/Footer/BasicFooter.jsx +++ b/src/core/components/elements/Footer/BasicFooter.jsx @@ -259,7 +259,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 / 29 + (021) 2933-8828 </a> </li> <li className='text-gray_r-12/80 flex items-center'> diff --git a/src/core/components/elements/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx index 2e8c982e..b23b256e 100644 --- a/src/core/components/elements/Navbar/NavbarDesktop.jsx +++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx @@ -12,11 +12,12 @@ import { ChevronDownIcon, DocumentCheckIcon, HeartIcon, + ArrowUpRightIcon, } from '@heroicons/react/24/outline'; import dynamic from 'next/dynamic'; import Image from 'next/image'; import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import DesktopView from '../../views/DesktopView'; import Link from '../Link/Link'; import NavbarUserDropdown from './NavbarUserDropdown'; @@ -62,6 +63,46 @@ const NavbarDesktop = () => { (transaction) => transaction.status === 'draft' ); + const [showPopup, setShowPopup] = useState(false); + const [isTop, setIsTop] = useState(true); + + const handleTopBannerLoad = useCallback(() => { + const showTimer = setTimeout(() => { + setShowPopup(true); + }, 500); + + const hideTimer = setTimeout(() => { + // setShowPopup(false); + }, 9500); + + return () => { + clearTimeout(showTimer); + clearTimeout(hideTimer); + }; + }, []); + + useEffect(() => { + const handleScroll = () => { + setIsTop(window.scrollY < 100); + }; + + window.addEventListener('scroll', handleScroll); + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, []); + + useEffect(() => { + const handleScroll = () => { + setIsTop(window.scrollY < 100); + }; + + window.addEventListener('scroll', handleScroll); + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, []); + useEffect(() => { setPendingTransactions(data); }, [transactions.data]); @@ -115,7 +156,7 @@ const NavbarDesktop = () => { return ( <DesktopView> - <TopBanner /> + <TopBanner onLoad={handleTopBannerLoad} /> <div className='py-2 bg-warning-400' id='desktop-nav-top'> <div className='container mx-auto flex justify-between'> <div className='flex items-start gap-5'> @@ -230,6 +271,7 @@ const NavbarDesktop = () => { </div> </button> <div className='w-6/12 flex px-1 divide-x divide-gray_r-6'> + <Link href="/shop/promo" className={`${ @@ -238,19 +280,29 @@ const NavbarDesktop = () => { target="_blank" rel="noreferrer" > - <p className="absolute inset-0 flex justify-center items-center group-hover:scale-105 group-hover:text-red-500 transition-transform duration-200 z-10">Semua Promo</p> - {/* <div className='w-full h-full flex justify-end items-start'> - <Image - src='/images/ICON PROMO DISKON.svg' - alt='promo' - width={100} - height={100} + {showPopup && ( + <div className='w-full h-full relative justify-end items-start'> + <Image + src='/images/penawaran-terbatas.jpg' + alt='penawaran terbatas' + width={1440} + height={160} quality={100} - className={`inline-block z-20`} + // className={`fixed ${isTop ? 'md:top-[145px] lg:top-[160px] ' : 'lg:top-[85px] top-[80px]'} rounded-3xl md:left-1/4 lg:left-1/4 xl:left-1/4 left-2/3 w-40 h-12 p-2 z-50 transition-all duration-300 animate-pulse`} + className={`inline-block relative -top-8 transition-all duration-300 z-20 animate-pulse`} /> - </div> */} + </div> + )} + <p className="absolute inset-0 flex justify-center items-center group-hover:scale-105 group-hover:text-red-500 transition-transform duration-200 z-10">Semua Promo</p> </Link> - + {/* {showPopup && router.pathname === '/' && ( + <div className={`fixed ${isTop ? 'top-[170px]' : 'top-[90px]'} rounded-3xl left-[700px] w-fit object-center bg-green-50 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20 text-center p-2 z-50 transition-all duration-300`}> + <p className='w-36 h-3'> + Penawaran Terbatas + </p> + </div> + )} */} + <Link href='/shop/brands' diff --git a/src/core/components/elements/Navbar/TopBanner.jsx b/src/core/components/elements/Navbar/TopBanner.jsx index df47e87d..265dfb5e 100644 --- a/src/core/components/elements/Navbar/TopBanner.jsx +++ b/src/core/components/elements/Navbar/TopBanner.jsx @@ -3,9 +3,10 @@ import { useQuery } from 'react-query';import useDevice from '@/core/hooks/useDe import odooApi from '@/core/api/odooApi'; import SmoothRender from '~/components/ui/smooth-render'; import Link from '../Link/Link'; +import { useEffect } from 'react'; import { background } from '@chakra-ui/react'; -const TopBanner = () => { +const TopBanner = ({ onLoad }) => { const { isDesktop, isMobile } = useDevice() const topBanner = useQuery({ queryKey: 'topBanner', @@ -17,6 +18,12 @@ const TopBanner = () => { const hasData = topBanner.data?.length > 0; const data = topBanner.data?.[0] || null; + useEffect(() => { + if (hasData) { + onLoad(); + } + }, [hasData, onLoad]); + return ( <SmoothRender isLoaded={hasData} diff --git a/src/lib/category/api/popularProduct.js b/src/lib/category/api/popularProduct.js new file mode 100644 index 00000000..e17e0ae5 --- /dev/null +++ b/src/lib/category/api/popularProduct.js @@ -0,0 +1,31 @@ + +export const fetchPromoItemsSolr = async (category_id_ids) => { + let sort ='sort=qty_sold_f desc'; + try { + const queryParams = new URLSearchParams({ q: category_id_ids }); + const response = await fetch(`/solr/product/select?${queryParams.toString()}&rows=2000&fl=manufacture_name_s,manufacture_id_i,id,display_name_s&${sort}`); + 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) => { + const result = []; + for (const promotion of promotions) { + const data = { + id: promotion.id, + name: promotion.display_name_s, + manufacture_name: promotion.manufacture_name_s, + manufacture_id: promotion.manufacture_id_i, + }; + result.push(data); + } + return result; + };
\ No newline at end of file diff --git a/src/lib/category/components/Category.jsx b/src/lib/category/components/Category.jsx index c147a3b3..ff958378 100644 --- a/src/lib/category/components/Category.jsx +++ b/src/lib/category/components/Category.jsx @@ -5,6 +5,7 @@ import { createSlug } from '@/core/utils/slug' import { ChevronRightIcon } from '@heroicons/react/24/outline' import Image from 'next/image' import { useEffect, useState } from 'react' +import PopularBrand from './PopularBrand' const Category = () => { const [categories, setCategories] = useState([]) @@ -30,6 +31,7 @@ const Category = () => { } loadCategories() }, []) + // console.log("categories",categories) return ( <DesktopView> @@ -38,8 +40,11 @@ const Category = () => { <div key={category.id} className='flex'> <Link href={createSlug('/shop/category/', category.name, category.id)} - className='category-mega-box__parent' + className='category-mega-box__parent flex items-center' > + <div className='w-6 h-6 border mr-2 rounded-full flex justify-center items-center'> + <Image src={category.image} alt='' width={16} height={16} /> + </div> {category.name} </Link> <div className='category-mega-box__child-wrapper'> @@ -80,33 +85,7 @@ const Category = () => { ))} </div> <div className='category-mega-box__child-wrapper !w-[260px] !flex !flex-col !gap-4'> - <div className='flex flex-col'> - <div className='grid grid-cols-2 max-h-full w-full gap-2'> - {category.childs.map((brand, index) => ( - (index < 8 ) && ( - <div key={brand.id} className='w-full flex items-center justify-center pb-2'> - <Link - href={createSlug('/shop/category/', brand.name, brand.id)} - className='category-mega-box__child-one w-fit h-full flex items-center justify-center ' - > - <Image src='https://erp.indoteknik.com/api/image/x_manufactures/x_logo_manufacture/661' alt='' width={104} height={44} objectFit='cover' /> - </Link> - </div> - ) - ))} - </div> - {category.childs.length > 8 && ( - <div className='flex hover:bg-gray_r-8/35 rounded-10'> - <Link - href={createSlug('/shop/category/', category.name, category.id)} - className='category-mega-box__child-one flex items-center gap-4 font-bold hover:ml-4' - > - <p className='mt-2 mb-0 text-danger-500 font-semibold'>Lihat Semua Brand</p> - <ChevronRightIcon className='w-4 text-danger-500 font-bold' /> - </Link> - </div> - )} - </div> + <PopularBrand category={category} /> <div className='flex w-60 h-20 object-cover'> <Image src='https://erp.indoteknik.com/api/image/x_banner.banner/x_banner_image/397' alt='' width={275} height={4} /> </div> diff --git a/src/lib/category/components/PopularBrand.jsx b/src/lib/category/components/PopularBrand.jsx new file mode 100644 index 00000000..dca731e8 --- /dev/null +++ b/src/lib/category/components/PopularBrand.jsx @@ -0,0 +1,83 @@ +import odooApi from '@/core/api/odooApi' +import React, { useEffect, useState } from 'react' +import axios from 'axios'; +import { useQuery } from 'react-query' +import Link from '@/core/components/elements/Link/Link' +import { createSlug } from '@/core/utils/slug' +import Image from 'next/image' +import { ChevronRightIcon } from '@heroicons/react/24/outline' +import useProductSearch from '../../../lib/product/hooks/useProductSearch'; +import { SolrResponse } from "~/types/solr"; +import { fetchPromoItemsSolr } from '../api/popularProduct' + +const SOLR_HOST = process.env.SOLR_HOST + +const PopularBrand = ({ category }) => { + const [topBrands, setTopBrands] = useState([]); + + const fetchTopBrands = async () => { + try { + const items = await fetchPromoItemsSolr(`category_id_ids:(${category.categoryDataIds.join(' OR ')})`); + // console.log("id",items) + // Fungsi untuk deduplikasi dan mengambil 12 nama brand teratas + const getTop12UniqueBrands = (prod) => { + const brandSet = new Set(); + const topBrands = []; + + for (const product of prod) { + if (!brandSet.has(product.manufacture_name)) { + brandSet.add(product.manufacture_name); + topBrands.push({ name: product.manufacture_name, id: product.manufacture_id }); + }else{ + } + if (topBrands.length === 18) break; + } + return topBrands; + } + + // Menggunakan hasil pencarian produk + const products = items; + const top12UniqueBrands = getTop12UniqueBrands(products); + + // console.log('top12UniqueBrands', top12UniqueBrands); + setTopBrands(top12UniqueBrands); + } catch (error) { + console.error("Error fetching data from Solr", error); + throw error; + } + } + + useEffect(() => { + fetchTopBrands(); + }, [category]); + + return ( + <div className='flex flex-col'> + <div className='grid grid-cols-3 max-h-full w-full gap-2'> + {topBrands.map((brand, index) => ( + <div key={index} className='w-full flex items-center justify-center pb-2'> + <Link + href={createSlug('/shop/brands/', brand.name, brand.id)} + className='category-mega-box__child-one w-8 h-full flex items-center justify-center ' + > + <Image src={`https://erp.indoteknik.com/api/image/x_manufactures/x_logo_manufacture/${brand.id}` } alt={`${brand.name}`} width={104} height={44} objectFit='cover' /> + </Link> + </div> + ))} + </div> + {/* {topBrands.length > 8 && ( + <div className='flex hover:bg-gray_r-8/35 rounded-10'> + <Link + href={createSlug('/shop/category/', category.name, category.id)} + className='category-mega-box__child-one flex items-center gap-4 font-bold hover:ml-4' + > + <p className='mt-2 mb-0 text-danger-500 font-semibold'>Lihat Semua Brand</p> + <ChevronRightIcon className='w-4 text-danger-500 font-bold' /> + </Link> + </div> + )} */} + </div> + ) +} + +export default PopularBrand; diff --git a/src/lib/flashSale/components/FlashSaleNonDisplay.jsx b/src/lib/flashSale/components/FlashSaleNonDisplay.jsx new file mode 100644 index 00000000..8dc15b05 --- /dev/null +++ b/src/lib/flashSale/components/FlashSaleNonDisplay.jsx @@ -0,0 +1,66 @@ +import Image from 'next/image'; +import { useEffect, useState } from 'react'; + +import CountDown from '@/core/components/elements/CountDown/CountDown'; +import productSearchApi from '@/lib/product/api/productSearchApi'; +import ProductSlider from '@/lib/product/components/ProductSlider'; + +import flashSaleApi from '../api/flashSaleApi'; +import { FlashSaleSkeleton } from '../skeleton/FlashSaleSkeleton'; + +const FlashSaleNonDisplay = () => { + const [flashSales, setFlashSales] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const loadFlashSales = async () => { + const dataFlashSales = await flashSaleApi(); + setFlashSales(dataFlashSales); + setIsLoading(false); + }; + loadFlashSales(); + }, []); + + if (isLoading) { + return <FlashSaleSkeleton />; + } + + return ( + flashSales?.length > 0 && ( + <div className='px-4 sm:px-0 grid grid-cols-1 gap-y-8'> + {flashSales.map((flashSale, index) => ( + <div key={index}> + <div className='flex gap-x-3 mb-4 justify-between sm:justify-start'> + <div className='font-medium sm:text-h-lg mt-1.5'> + Penawaran Terbatas + </div> + </div> + + <div className='relative'> + <FlashSaleProduct flashSaleId={flashSale.pricelistId} /> + </div> + </div> + ))} + </div> + ) + ); +}; + +const FlashSaleProduct = ({ flashSaleId }) => { + const [products, setProducts] = useState(null); + + useEffect(() => { + const loadProducts = async () => { + const dataProducts = await productSearchApi({ + query: `fq=-flashsale_id_i:${flashSaleId}&fq=flashsale_price_f:[1 TO *]&limit=500&orderBy=flashsale-discount-desc`, + operation: 'AND', + }); + setProducts(dataProducts.response); + }; + loadProducts(); + }, [flashSaleId]); + + return <ProductSlider products={products} />; +}; + +export default FlashSaleNonDisplay; diff --git a/src/lib/home/components/CategoryDynamic.jsx b/src/lib/home/components/CategoryDynamic.jsx index f2d1a16f..0cc43d91 100644 --- a/src/lib/home/components/CategoryDynamic.jsx +++ b/src/lib/home/components/CategoryDynamic.jsx @@ -33,9 +33,8 @@ const CategoryDynamic = () => { }; fetchCategoryData(); - }, [categoryManagement, categoryData]); + }, [categoryManagement.isLoading]); - return ( <div> {categoryManagement && categoryManagement.data?.map((category) => { diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx index 94db144d..35e2a665 100644 --- a/src/lib/product/components/ProductCard.jsx +++ b/src/lib/product/components/ProductCard.jsx @@ -17,8 +17,8 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { const [discount, setDiscount] = useState(0); let voucherPastiHemat = 0; - - if (product?.voucherPastiHemat?.length > 0) { + + if (product?.voucherPastiHemat ? product?.voucherPastiHemat.length : voucherPastiHemat > 0) { const stringVoucher = product?.voucherPastiHemat[0]; const validJsonString = stringVoucher.replace(/'/g, '"'); voucherPastiHemat = JSON.parse(validJsonString); diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx index 09534a5d..a83e5e1e 100644 --- a/src/lib/product/components/ProductSearch.jsx +++ b/src/lib/product/components/ProductSearch.jsx @@ -41,8 +41,6 @@ const ProductSearch = ({ const { page = 1 } = query; const [q, setQ] = useState(query?.q || '*'); const [search, setSearch] = useState(query?.q || '*'); - const [limit, setLimit] = useState(router.query?.limit || 30); - const [orderBy, setOrderBy] = useState(router.query?.orderBy || 'popular'); const [finalQuery, setFinalQuery] = useState({}); const [queryFinal, setQueryFinal] = useState({}); const [dataCategoriesProduct, setDataCategoriesProduct] = useState([]) @@ -50,6 +48,8 @@ const ProductSearch = ({ const categoryId = getIdFromSlug(prefixUrl) const [data, setData] = useState([]) const [dataLob, setDataLob] = useState([]); + const [limit, setLimit] = useState(query?.limit || 30); + const [orderBy, setOrderBy] = useState(router.query?.orderBy); if (defaultBrand) query.brand = defaultBrand.toLowerCase(); const dataIdCategories = [] useEffect(() => { @@ -267,6 +267,7 @@ const ProductSearch = ({ const orderOptions = [ + { value: '', label: 'Pilih Filter' }, { value: 'price-asc', label: 'Harga Terendah' }, { value: 'price-desc', label: 'Harga Tertinggi' }, { value: 'popular', label: 'Populer' }, diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index b6b8c795..87d7c580 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -3,6 +3,7 @@ import axios from 'axios'; import camelcaseObjectDeep from 'camelcase-object-deep'; export default async function handler(req, res) { + const { q = '*', page = 1, @@ -20,6 +21,9 @@ export default async function handler(req, res) { let paramOrderBy = ''; switch (orderBy) { + case 'flashsale-discount-desc': + paramOrderBy += 'flashsale_discount_f DESC'; + break; case 'price-asc': paramOrderBy += 'price_tier1_v2_f ASC'; break; @@ -39,7 +43,7 @@ export default async function handler(req, res) { paramOrderBy += 'flashsale_price_f ASC'; break; default: - paramOrderBy += 'product_rating_f DESC, price_discount_f DESC'; + paramOrderBy += ''; break; } @@ -56,9 +60,10 @@ export default async function handler(req, res) { `start=${parseInt(offset)}`, `rows=${limit}`, `sort=${paramOrderBy}`, - `fq=-publish_b:false`, + `fq=-publish_b:false, product_rating_f:[13 TO *], discount_tier1_v2_f:[1 TO *]`, ]; + if (priceFrom > 0 || priceTo > 0) { parameter.push( `fq=price_tier1_v2_f:[${priceFrom == '' ? '*' : priceFrom} TO ${ @@ -99,6 +104,7 @@ export default async function handler(req, res) { let result = await axios( process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&') ); + try { result.data.response.products = productMappingSolr( result.data.response.docs, |
