diff options
| author | IT Fixcomart <it@fixcomart.co.id> | 2024-06-28 03:26:38 +0000 |
|---|---|---|
| committer | trisusilo <tri.susilo@altama.co.id> | 2024-06-28 03:26:38 +0000 |
| commit | def7fc2f1201286c44545e1305ca9f11eb16f3d0 (patch) | |
| tree | 4c65db77b9aa28f3dd53ce514de9d918440c2887 /src | |
| parent | 98d4f9564bfba66687848b335026bddb6f4dc5b6 (diff) | |
| parent | 936f7f9fb1c6951e02dd8a5f7e85fb8d39cc7e6a (diff) | |
Merged in promotion-program (pull request #140)
Promotion program
Approved-by: trisusilo
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/bannerApi.js | 3 | ||||
| -rw-r--r-- | src/api/promoApi.js | 70 | ||||
| -rw-r--r-- | src/lib/home/components/PromotionProgram.jsx | 43 | ||||
| -rw-r--r-- | src/lib/product/components/ProductFilterDesktop.jsx | 10 | ||||
| -rw-r--r-- | src/lib/product/components/ProductFilterDesktopPromotion.jsx | 132 | ||||
| -rw-r--r-- | src/lib/product/components/ProductSearch.jsx | 2 | ||||
| -rw-r--r-- | src/lib/promo/components/Promocrumb.jsx | 40 | ||||
| -rw-r--r-- | src/pages/index.jsx | 9 | ||||
| -rw-r--r-- | src/pages/shop/promo/[slug].tsx | 523 | ||||
| -rw-r--r-- | src/pages/shop/promo/index.tsx | 186 |
10 files changed, 1016 insertions, 2 deletions
diff --git a/src/api/bannerApi.js b/src/api/bannerApi.js index 8bae131d..431225a5 100644 --- a/src/api/bannerApi.js +++ b/src/api/bannerApi.js @@ -3,3 +3,6 @@ import odooApi from '@/core/api/odooApi' export const bannerApi = ({ type }) => { return async () => await odooApi('GET', `/api/v1/banner?type=${type}`) } + +// ubah ke SOLR + diff --git a/src/api/promoApi.js b/src/api/promoApi.js new file mode 100644 index 00000000..4c386fba --- /dev/null +++ b/src/api/promoApi.js @@ -0,0 +1,70 @@ +// src/api/promoApi.js +import odooApi from '@/core/api/odooApi'; +import { type } from 'os'; +// import { SolrResponse } from "../../../../src-migrate/types/solr.ts"; + +export const fetchPromoItems = async (type) => { + try { + const response = await odooApi('GET', `/api/v1/program-line?type=${type}&limit=3`); + return response.map((item) => ({ value: item.id, label: item.name, product: item.products ,price:item.price})); + } catch (error) { + console.error('Error fetching promo items:', error); + return []; + } +}; + +export const fetchPromoItemsSolr = async (type) => { + // let query = type ? `type_value_s:${type}` : '*:*'; + let start = 0 + let rows = 100 + try { + const queryParams = new URLSearchParams({ q: type }); + const response = await fetch(`/solr/promotion_program_lines/select?${queryParams.toString()}&rows=${rows}&start=${start}`); + 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 []; + } +}; + +export const fetchVariantSolr = async(data)=>{ + try { + const queryParams = new URLSearchParams({ q: data }); + const response = await fetch(`/solr/variants/select?${queryParams.toString()}`); + const responseData = await response.json(); + return responseData; + } 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, + program_id: promotion.program_id_i, + name: promotion.name_s, + type: { + value: promotion.type_value_s, + label: promotion.type_label_s, + }, + limit: promotion.package_limit_i, + limit_user: promotion.package_limit_user_i, + limit_trx: promotion.package_limit_trx_i, + price: promotion.price_f, + total_qty: promotion.total_qty_i, + products: JSON.parse(promotion.products_s), + product_id: promotion.product_ids[0], + free_products: JSON.parse(promotion.free_products_s), + }; + result.push(data); + } + return result; +};
\ No newline at end of file diff --git a/src/lib/home/components/PromotionProgram.jsx b/src/lib/home/components/PromotionProgram.jsx new file mode 100644 index 00000000..66216d19 --- /dev/null +++ b/src/lib/home/components/PromotionProgram.jsx @@ -0,0 +1,43 @@ +import Link from '@/core/components/elements/Link/Link' +import Image from 'next/image' +import { bannerApi } from '@/api/bannerApi'; +import useDevice from '@/core/hooks/useDevice' +const { useQuery } = require('react-query') +const BannerSection = () => { + const promotionProgram = useQuery('promotionProgram', bannerApi({ type: 'banner-promotion' })); + const { isMobile, isDesktop } = useDevice() + + return ( + <div className='px-4 sm:px-0'> + <div className='flex justify-between items-center mb-4 '> + <div className='font-semibold sm:text-h-lg'>Promo Tersedia</div> + {isDesktop && ( + <Link href='/shop/promo' className='!text-red-500 font-semibold'> + Lihat Semua + </Link> + )} + </div> + {promotionProgram.data && + promotionProgram.data?.length > 0 && ( + <div className='grid grid-cols-3 sm:grid-cols-3 gap-4 rounded-md'> + {promotionProgram.data?.map((banner) => ( + <Link key={banner.id} href={banner.url}> + <Image + width={439} + height={150} + quality={100} + src={banner.image} + alt={banner.name} + className='h-auto w-full rounded hover:scale-105 transition duration-500 ease-in-out' + /> + </Link> + ))} + </div> + + )} + </div> + + ) +} + +export default BannerSection diff --git a/src/lib/product/components/ProductFilterDesktop.jsx b/src/lib/product/components/ProductFilterDesktop.jsx index e4a62abb..a8073036 100644 --- a/src/lib/product/components/ProductFilterDesktop.jsx +++ b/src/lib/product/components/ProductFilterDesktop.jsx @@ -21,6 +21,7 @@ import Image from '@/core/components/elements/Image/Image' import { formatCurrency } from '@/core/utils/formatValue' const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = null }) => { + const router = useRouter() const { query } = router const [order, setOrder] = useState(query?.orderBy) @@ -102,7 +103,14 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu } params = _.pickBy(params, _.identity) params = toQuery(params) - router.push(`${prefixUrl}?${params}`) + + const slug = Array.isArray(router.query.slug) ? router.query.slug[0] : router.query.slug; + + if (slug) { + router.push(`${prefixUrl}/${slug}?${params}`) + } else { + router.push(`${prefixUrl}?${params}`) + } } diff --git a/src/lib/product/components/ProductFilterDesktopPromotion.jsx b/src/lib/product/components/ProductFilterDesktopPromotion.jsx new file mode 100644 index 00000000..0815b881 --- /dev/null +++ b/src/lib/product/components/ProductFilterDesktopPromotion.jsx @@ -0,0 +1,132 @@ +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import _ from 'lodash'; +import { toQuery } from 'lodash-contrib'; +import { Button } from '@chakra-ui/react'; +import { MultiSelect } from 'react-multi-select-component'; + +const ProductFilterDesktop = ({ brands, categories, prefixUrl }) => { + const router = useRouter(); + const { query } = router; + const [order, setOrder] = useState(query?.orderBy); + const [brandValues, setBrand] = useState([]); + const [categoryValues, setCategory] = useState([]); + const [priceFrom, setPriceFrom] = useState(query?.priceFrom); + const [priceTo, setPriceTo] = useState(query?.priceTo); + const [stock, setStock] = useState(query?.stock); + const [activeRange, setActiveRange] = useState(null); + const [isBrandDropdownClicked, setIsBrandDropdownClicked] = useState(false); + const [isCategoryDropdownClicked, setIsCategoryDropdownClicked] = useState(false); + + // Effect to set brandValues from query parameter 'brand' + useEffect(() => { + const brandParam = query?.brand; + if (brandParam) { + const brandsArray = brandParam.split(',').map((b) => ({ + label: b, + value: b, + })); + setBrand(brandsArray); + } + + }, [query.brand]); // Trigger effect whenever query.brand changes + + useEffect(() => { + const categoryParam = query?.category; + if (categoryParam) { + const categoriesArray = categoryParam.split(',').map((c) => ({ + label: c, + value: c, + })); + setCategory(categoriesArray); + } + }, [query.category]); // Trigger effect whenever query.category changes + + const handleSubmit = () => { + let params = { + q: router.query.q, + orderBy: order, + brand: brandValues.map((b) => b.value).join(','), + category: categoryValues.map((c) => c.value).join(','), + priceFrom, + priceTo, + stock: stock, + }; + params = _.pickBy(params, _.identity); + params = toQuery(params); + + const slug = Array.isArray(router.query.slug) + ? router.query.slug[0] + : router.query.slug; + + if (slug) { + router.push(`${prefixUrl}/${slug}?${params}`); + } else { + router.push(`${prefixUrl}?${params}`); + } + }; + + + const brandOptions = brands.map((brand) => ({ + label: `${brand.brand} (${brand.qty})`, + value: brand.brand, + })); + + const categoryOptions = categories.map((category) => ({ + label: `${category.name} (${category.qty})`, + value: category.name, + })); + + return ( + <> + <div className='flex h-full w-[100%] justify-end '> + {/* Brand MultiSelect */} + <div className='mb-[20px] mr-4 w-64 h-full flex justify-start '> + <div className='relative'> + <label>Brand</label> + <div className='h-auto z-50 w-64 '> + <MultiSelect + options={brandOptions} + value={brandValues} + onChange={setBrand} + labelledBy='Select Brand' + onMenuToggle={(isOpen) => setIsBrandDropdownClicked(isOpen)} + hasSelectAll={false} + /> + </div> + </div> + </div> + + {/* Category MultiSelect */} + <div className='mb-[20px] mr-4 w-64 h-full flex justify-start '> + <div className='relative'> + <label>Kategori</label> + <div className=' h-auto w-64'> + <MultiSelect + options={categoryOptions} + value={categoryValues} + onChange={setCategory} + labelledBy='Select Kategori' + onMenuToggle={() => + setIsCategoryDropdownClicked(!isCategoryDropdownClicked) + } + hasSelectAll={false} + /> + </div> + </div> + </div> + + {/* Apply Button */} + <div className='TOMBOL mb-1 h-24 flex justify-center items-center w-24'> + <div className=' bottom-1 pb-1 left-0 right-0 flex justify-center rounded' > + <Button colorScheme='red' width={"full"} onClick={handleSubmit}> + Terapkan + </Button> + </div> + </div> + </div> + </> + ); +}; + +export default ProductFilterDesktop; diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx index 08b64c13..b1a5d409 100644 --- a/src/lib/product/components/ProductSearch.jsx +++ b/src/lib/product/components/ProductSearch.jsx @@ -130,6 +130,7 @@ const ProductSearch = ({ brands.push({ brand, qty }); } } + const categories = []; for ( @@ -144,6 +145,7 @@ const ProductSearch = ({ categories.push({ name, qty }); } } + const orderOptions = [ { value: 'price-asc', label: 'Harga Terendah' }, diff --git a/src/lib/promo/components/Promocrumb.jsx b/src/lib/promo/components/Promocrumb.jsx new file mode 100644 index 00000000..a96ec6b4 --- /dev/null +++ b/src/lib/promo/components/Promocrumb.jsx @@ -0,0 +1,40 @@ +import { Breadcrumb as ChakraBreadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react' +import Link from 'next/link' +import React from 'react' + +/** + * Renders a breadcrumb component with links to navigate through different pages. + * + * @param {Object} props - The props object containing the brand name. + * @param {string} props.brandName - The name of the brand to display in the breadcrumb. + * @return {JSX.Element} The rendered breadcrumb component. + */ +const Breadcrumb = ({ brandName }) => { + return ( + <div className='container mx-auto py-4 md:py-6'> + <ChakraBreadcrumb> + <BreadcrumbItem> + <BreadcrumbLink as={Link} href='/' className='!text-danger-500 whitespace-nowrap'> + Shop + </BreadcrumbLink> + </BreadcrumbItem> + + <BreadcrumbItem> + <BreadcrumbLink + as={Link} + href='/shop/promo' + className='!text-danger-500 whitespace-nowrap' + > + Promo + </BreadcrumbLink> + </BreadcrumbItem> + + <BreadcrumbItem isCurrentPage> + <BreadcrumbLink className='whitespace-nowrap'>{brandName}</BreadcrumbLink> + </BreadcrumbItem> + </ChakraBreadcrumb> + </div> + ) +} + +export default Breadcrumb diff --git a/src/pages/index.jsx b/src/pages/index.jsx index c097530c..80e9ef0e 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -41,6 +41,11 @@ const FlashSale = dynamic( loading: () => <FlashSaleSkeleton />, } ); + +const ProgramPromotion = dynamic(() => + import('@/lib/home/components/PromotionProgram') +); + const BannerSection = dynamic(() => import('@/lib/home/components/BannerSection') ); @@ -97,11 +102,12 @@ export default function Home() { </div> </div> - <div className='my-16 flex flex-col gap-y-16'> + <div className='my-16 flex flex-col gap-y-8'> <ServiceList /> <div id='flashsale'> <PreferredBrand /> </div> + <ProgramPromotion/> <FlashSale /> <PromotinProgram /> <CategoryHomeId /> @@ -126,6 +132,7 @@ export default function Home() { </DelayRender> <DelayRender renderAfter={600}> <FlashSale /> + <ProgramPromotion/> </DelayRender> <DelayRender renderAfter={600}> <PromotinProgram /> diff --git a/src/pages/shop/promo/[slug].tsx b/src/pages/shop/promo/[slug].tsx new file mode 100644 index 00000000..bd69c071 --- /dev/null +++ b/src/pages/shop/promo/[slug].tsx @@ -0,0 +1,523 @@ +import dynamic from 'next/dynamic' +import NextImage from 'next/image'; +import { useEffect, useState } from 'react' +import { useRouter } from 'next/router' +import Seo from '../../../core/components/Seo' +import Promocrumb from '../../../lib/promo/components/Promocrumb' +import { fetchPromoItemsSolr, fetchVariantSolr } from '../../../api/promoApi' +import LogoSpinner from '../../../core/components/elements/Spinner/LogoSpinner.jsx' +import ProductPromoCard from '../../../../src-migrate/modules/product-promo/components/Card' +import { IPromotion } from '../../../../src-migrate/types/promotion' +import React from 'react' +import { SolrResponse } from "../../../../src-migrate/types/solr.ts"; +import DesktopView from '../../../core/components/views/DesktopView'; +import MobileView from '../../../core/components/views/MobileView'; +import 'swiper/swiper-bundle.css'; +import useDevice from '../../../core/hooks/useDevice' +import ProductFilterDesktop from '../../../lib/product/components/ProductFilterDesktopPromotion'; +import ProductFilter from '../../../lib/product/components/ProductFilter'; +import { HStack, Image, Tag, TagCloseButton, TagLabel } from '@chakra-ui/react'; +import { formatCurrency } from '../../../core/utils/formatValue'; +import Pagination from '../../../core/components/elements/Pagination/Pagination'; +import SideBanner from '../../../../src-migrate/modules/side-banner'; +import whatsappUrl from '../../../core/utils/whatsappUrl'; +import { cons, toQuery } from 'lodash-contrib'; +import _ from 'lodash'; +import useActive from '../../../core/hooks/useActive'; + +const BasicLayout = dynamic(() => import('../../../core/components/layouts/BasicLayout')) + +export default function PromoDetail() { + const router = useRouter() + const { slug = '', brand ='', category='', priceFrom = '', priceTo = '', page = '1' } = router.query + const [promoItems, setPromoItems] = useState<any[]>([]) + const [promoData, setPromoData] = useState<IPromotion[] | null>(null) + const [currentPage, setCurrentPage] = useState(parseInt(page as string, 10) || 1); + const itemsPerPage = 12; // Jumlah item yang ingin ditampilkan per halaman + const [loading, setLoading] = useState(true); + const { isMobile, isDesktop } = useDevice() + const [brands, setBrands] = useState<Brand[]>([]); + const [categories, setCategories] = useState<Category[]>([]); + const [brandValues, setBrandValues] = useState<string[]>([]); + const [categoryValues, setCategoryValues] = useState<string[]>([]); + const [orderBy, setOrderBy] = useState(router.query?.orderBy || 'popular'); + const popup = useActive(); + const prefixUrl = `/shop/promo/${slug}` + + useEffect(() => { + if (router.query.brand) { + let brandsArray: string[] = []; + if (Array.isArray(router.query.brand)) { + brandsArray = router.query.brand; + } else if (typeof router.query.brand === 'string') { + brandsArray = router.query.brand.split(',').map((brand) => brand.trim()); + } + setBrandValues(brandsArray); + } else { + setBrandValues([]); + } + + if (router.query.category) { + let categoriesArray: string[] = []; + + if (Array.isArray(router.query.category)) { + categoriesArray = router.query.category; + } else if (typeof router.query.category === 'string') { + categoriesArray = router.query.category.split(',').map((category) => category.trim()); + } + setCategoryValues(categoriesArray); + } else { + setCategoryValues([]); + } + }, [router.query.brand, router.query.category]); + + interface Brand { + brand: string; + qty: number; + } + + interface Category { + name: string; + qty: number; + } + + useEffect(() => { + const loadPromo = async () => { + setLoading(true); + const brandsData: Brand[] = []; + const categoriesData: Category[] = []; + + const pageNumber = Array.isArray(page) ? parseInt(page[0], 10) : parseInt(page, 10); + setCurrentPage(pageNumber) + + try { + const items = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug}`); + setPromoItems(items); + + if (items.length === 0) { + setPromoData([]) + setLoading(false); + return; + } + + const brandArray = Array.isArray(brand) ? brand : brand.split(','); + const categoryArray = Array.isArray(category) ? category : category.split(','); + + const promoDataPromises = items.map(async (item) => { + + try { + let brandQuery = ''; + if (brand) { + brandQuery = brandArray.map(b => `manufacture_name_s:${b}`).join(' OR '); + brandQuery = `(${brandQuery})`; + } + + let categoryQuery = ''; + if (category) { + categoryQuery = categoryArray.map(c => `category_name:${c}`).join(' OR '); + categoryQuery = `(${categoryQuery})`; + } + + let priceQuery = ''; + if (priceFrom && priceTo) { + priceQuery = `price_f:[${priceFrom} TO ${priceTo}]`; + } else if (priceFrom) { + priceQuery = `price_f:[${priceFrom} TO *]`; + } else if (priceTo) { + priceQuery = `price_f:[* TO ${priceTo}]`; + } + + let combinedQuery = ''; + let combinedQueryPrice = `${priceQuery}`; + if (brand && category && priceFrom || priceTo) { + combinedQuery = `${brandQuery} AND ${categoryQuery} `; + } else if (brand && category) { + combinedQuery = `${brandQuery} AND ${categoryQuery}`; + } else if (brand && priceFrom || priceTo) { + combinedQuery = `${brandQuery}`; + } else if (category && priceFrom || priceTo) { + combinedQuery = `${categoryQuery}`; + } else if (brand) { + combinedQuery = brandQuery; + } else if (category) { + combinedQuery = categoryQuery; + } + + if (combinedQuery && priceFrom || priceTo) { + const response = await fetchVariantSolr(`id:${item.product_id} AND ${combinedQuery}`); + const product = response.response.docs[0]; + const product_id = product.id; + const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} AND ${combinedQueryPrice}`); + return response2; + }else if(combinedQuery){ + const response = await fetchVariantSolr(`id:${item.product_id} AND ${combinedQuery}`); + const product = response.response.docs[0]; + const product_id = product.id; + const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} `); + return response2; + } else { + const response = await fetchPromoItemsSolr(`id:${item.id}`); + return response; + } + } catch (fetchError) { + return []; + } + }); + + const promoDataArray = await Promise.all(promoDataPromises); + const mergedPromoData = promoDataArray.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []); + setPromoData(mergedPromoData); + + const dataBrandCategoryPromises = promoDataArray.map(async (promoData) => { + if (promoData) { + const dataBrandCategory = promoData.map(async (item) => { + let response; + if(category){ + const categoryQuery = categoryArray.map(c => `category_name:${c}`).join(' OR '); + response = await fetchVariantSolr(`id:${item.products[0].product_id} AND (${categoryQuery})`); + }else{ + response = await fetchVariantSolr(`id:${item.products[0].product_id}`) + } + + + if (response.response?.docs?.length > 0) { + const product = response.response.docs[0]; + const manufactureNameS = product.manufacture_name; + if (Array.isArray(manufactureNameS)) { + for (let i = 0; i < manufactureNameS.length; i += 2) { + const brand = manufactureNameS[i]; + const qty = 1; + const existingBrandIndex = brandsData.findIndex(b => b.brand === brand); + if (existingBrandIndex !== -1) { + brandsData[existingBrandIndex].qty += qty; + } else { + brandsData.push({ brand, qty }); + } + } + } + + const categoryNameS = product.category_name; + if (Array.isArray(categoryNameS)) { + for (let i = 0; i < categoryNameS.length; i += 2) { + const name = categoryNameS[i]; + const qty = 1; + const existingCategoryIndex = categoriesData.findIndex(c => c.name === name); + if (existingCategoryIndex !== -1) { + categoriesData[existingCategoryIndex].qty += qty; + } else { + categoriesData.push({ name, qty }); + } + } + } + } + }); + + return Promise.all(dataBrandCategory); + } + }); + + await Promise.all(dataBrandCategoryPromises); + setBrands(brandsData); + setCategories(categoriesData); + setLoading(false); + + } catch (loadError) { + // console.error("Error loading promo items:", loadError) + setLoading(false); + } + } + + if (slug) { + loadPromo() + } + },[slug, brand, category, priceFrom, priceTo, currentPage]); + + + function capitalizeFirstLetter(string) { + string = string.replace(/_/g, ' '); + return string.replace(/(^\w|\s\w)/g, function(match) { + return match.toUpperCase(); + }); + } + + const handleDeleteFilter = async (source, value) => { + let params = { + q: router.query.q, + orderBy: '', + brand: brandValues.join(','), + category: categoryValues.join(','), + priceFrom: priceFrom || '', + priceTo: priceTo || '', + }; + + let brands = brandValues; + let catagories = categoryValues; + switch (source) { + case 'brands': + brands = brandValues.filter((item) => item !== value); + params.brand = brands.join(','); + await setBrandValues(brands); + break; + case 'category': + catagories = categoryValues.filter((item) => item !== value); + params.category = catagories.join(','); + await setCategoryValues(catagories); + break; + case 'price': + params.priceFrom = ''; + params.priceTo = ''; + break; + case 'delete': + params = { + q: router.query.q, + orderBy: '', + brand: '', + category: '', + priceFrom: '', + priceTo: '', + }; + break; + } + + handleSubmitFilter(params); + }; + const handleSubmitFilter = (params) => { + params = _.pickBy(params, _.identity); + params = toQuery(params); + router.push(`${slug}?${params}`); + }; + + const visiblePromotions = promoData?.slice( (currentPage-1) * itemsPerPage, currentPage * 12) + + const toQuery = (obj) => { + const str = Object.keys(obj) + .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`) + .join('&') + return str + } + + const whatPromo = capitalizeFirstLetter(slug) + const queryWithoutSlug = _.omit(router.query, ['slug']) + const queryString = toQuery(queryWithoutSlug) + + return ( + <BasicLayout> + <Seo + title={`Promo ${Array.isArray(slug) ? slug[0] : slug} Terkini`} + description='B2B Marketplace MRO & Industri dengan Layanan Pembayaran Tempo, Faktur Pajak, Online Quotation, Garansi Resmi & Harga Kompetitif' + /> + <Promocrumb brandName={whatPromo} /> + <MobileView> + <div className='p-4 pt-0'> + <h1 className='mb-2 font-semibold text-h-sm'>Promo {whatPromo}</h1> + + <FilterChoicesComponent + brandValues={brandValues} + categoryValues={categoryValues} + priceFrom={priceFrom} + priceTo={priceTo} + handleDeleteFilter={handleDeleteFilter} + /> + {promoItems.length >= 1 && ( + <div className='flex items-center gap-x-2 mb-5 justify-between'> + <div> + <button + className='btn-light py-2 px-5 h-[40px]' + onClick={popup.activate} + > + Filter + </button> + </div> + </div> + )} + + {loading ? ( + <div className='container flex justify-center my-4'> + <LogoSpinner width={48} height={48} /> + </div> + ) : promoData && promoItems.length >= 1 ? ( + <> + <div className='grid grid-cols-1 gap-x-1 gap-y-1'> + {visiblePromotions?.map((promotion) => ( + <div key={promotion.id} className="min-w-36 max-w-[400px] mb-[20px] sm:w-full md:w-1/2 lg:w-1/3 xl:w-1/4 "> + <ProductPromoCard promotion={promotion}/> + </div> + ))} + </div> + </> + ) : ( + <div className="text-center my-8"> + <p>Belum ada promo pada kategori ini</p> + </div> + )} + + <Pagination + pageCount={Math.ceil((promoData?.length ?? 0) / itemsPerPage)} + currentPage={currentPage} + url={`${prefixUrl}?${toQuery(_.omit(queryWithoutSlug, ['page']))}`} + className='mt-6 mb-2' + /> + <ProductFilter + active={popup.active} + close={popup.deactivate} + brands={brands || []} + categories={categories || []} + prefixUrl={router.asPath.includes('?') ? `${router.asPath}` : `${router.asPath}?`} + defaultBrand={null} + /> + </div> + + </MobileView> + <DesktopView> + <div className='container mx-auto flex mb-3 flex-col'> + <div className='w-full pl-6'> + <h1 className='text-2xl mb-2 font-semibold'>Promo {whatPromo}</h1> + <div className=' w-full h-full flex flex-row items-center '> + + <div className='detail-filter w-1/2 flex justify-start items-center mt-4'> + + <FilterChoicesComponent + brandValues={brandValues} + categoryValues={categoryValues} + priceFrom={priceFrom} + priceTo={priceTo} + handleDeleteFilter={handleDeleteFilter} + /> + </div> + <div className='Filter w-1/2 flex flex-col'> + + <ProductFilterDesktop + brands={brands || []} + categories={categories || []} + prefixUrl={'/shop/promo'} + // defaultBrand={null} + /> + </div> + </div> + {loading ? ( + <div className='container flex justify-center my-4'> + <LogoSpinner width={48} height={48} /> + </div> + ) : promoData && promoItems.length >= 1 ? ( + <> + <div className='grid grid-cols-1 gap-x-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3'> + {visiblePromotions?.map((promotion) => ( + <div key={promotion.id} className="min-w-[400px] max-w-[400px] mb-[20px] sm:min-w-[350px] md:min-w-[380px] lg:min-w-[400px] xl:min-w-[400px] "> + <ProductPromoCard promotion={promotion}/> + </div> + ))} + </div> + </> + ) : ( + <div className="text-center my-8"> + <p>Belum ada promo pada kategori ini</p> + </div> + )} + <div className='flex justify-between items-center mt-6 mb-2'> + <div className='pt-2 pb-6 flex items-center gap-x-3'> + <NextImage + src='/images/logo-question.png' + alt='Logo Question Indoteknik' + width={60} + height={60} + /> + <div className='text-gray_r-12/90'> + <span> + Barang yang anda cari tidak ada?{' '} + <a + href={ + router.query?.q + ? whatsappUrl('productSearch', { + name: router.query.q, + }) + : whatsappUrl() + } + className='text-danger-500' + > + Hubungi Kami + </a> + </span> + </div> + </div> + + + + <Pagination + pageCount={Math.ceil((promoData?.length ?? 0) / itemsPerPage)} + currentPage={currentPage} + url={`${prefixUrl}?${toQuery(_.omit(queryWithoutSlug, ['page']))}`} + className='mt-6 mb-2' + /> + </div> + + </div> + </div> + </DesktopView> + </BasicLayout> + ) + } + +const FilterChoicesComponent = ({ + brandValues, + categoryValues, + priceFrom, + priceTo, + handleDeleteFilter, + }) => ( + <div className='flex items-center mb-4'> + <HStack spacing={2} className='flex-wrap'> + {brandValues?.map((value, index) => ( + <Tag + size='lg' + key={index} + borderRadius='lg' + variant='outline' + colorScheme='gray' + > + <TagLabel>{value}</TagLabel> + <TagCloseButton onClick={() => handleDeleteFilter('brands', value)} /> + </Tag> + ))} + + {categoryValues?.map((value, index) => ( + <Tag + size='lg' + key={index} + borderRadius='lg' + variant='outline' + colorScheme='gray' + > + <TagLabel>{value}</TagLabel> + <TagCloseButton + onClick={() => handleDeleteFilter('category', value)} + /> + </Tag> + ))} + {priceFrom && priceTo && ( + <Tag size='lg' borderRadius='lg' variant='outline' colorScheme='gray'> + <TagLabel> + {formatCurrency(priceFrom) + '-' + formatCurrency(priceTo)} + </TagLabel> + <TagCloseButton + onClick={() => handleDeleteFilter('price', priceFrom)} + /> + </Tag> + )} + {brandValues?.length > 0 || + categoryValues?.length > 0 || + priceFrom || + priceTo ? ( + <span> + <button + className='btn-transparent py-2 px-5 h-[40px] text-red-700' + onClick={() => handleDeleteFilter('delete')} + > + Hapus Semua + </button> + </span> + ) : ( + '' + )} + </HStack> + </div> +); diff --git a/src/pages/shop/promo/index.tsx b/src/pages/shop/promo/index.tsx new file mode 100644 index 00000000..7ec4f6b0 --- /dev/null +++ b/src/pages/shop/promo/index.tsx @@ -0,0 +1,186 @@ +import dynamic from 'next/dynamic' +import { useEffect, useState } from 'react' +import { useRouter } from 'next/router' +import Seo from '../../../core/components/Seo.jsx' +import Promocrumb from '../../../lib/promo/components/Promocrumb.jsx' +import { fetchPromoItemsSolr } from '../../../api/promoApi.js' +import LogoSpinner from '../../../core/components/elements/Spinner/LogoSpinner.jsx' +import ProductPromoCard from '../../../../src-migrate/modules/product-promo/components/Card.tsx' +import { IPromotion } from '../../../../src-migrate/types/promotion.ts' +import React from 'react' +import { SolrResponse } from "../../../../src-migrate/types/solr.ts"; + +const BasicLayout = dynamic(() => import('../../../core/components/layouts/BasicLayout.jsx')) + +export default function Promo() { + const router = useRouter() + const { slug = '' } = router.query + const [promoItems, setPromoItems] = useState<any[]>([]) + const [promoData, setPromoData] = useState<IPromotion[] | null>(null) + const [loading, setLoading] = useState(true) + const [currentPage, setCurrentPage] = useState(1) + const [fetchingData, setFetchingData] = useState(false) + + useEffect(() => { + const loadPromo = async () => { + try { + const items = await fetchPromoItemsSolr(`*:*`) + + + setPromoItems(items) + + + if (items.length === 0) { + setPromoData([]) + setLoading(false); + return; + } + + const promoDataPromises = items.map(async (item) => { + const queryParams = new URLSearchParams({ q: `id:${item.id}` }) + + + try { + const response = await fetch(`/solr/promotion_program_lines/select?${queryParams.toString()}`) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data: SolrResponse<any[]> = await response.json() + + + const promotions = await map(data.response.docs) + return promotions; + } catch (fetchError) { + console.error("Error fetching promotion data:", fetchError) + return []; + } + }); + + const promoDataArray = await Promise.all(promoDataPromises); + const mergedPromoData = promoDataArray.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []); + setPromoData(mergedPromoData); + setTimeout(() => setLoading(false), 120); // Menambahkan delay 200ms sebelum mengubah status loading + } catch (loadError) { + console.error("Error loading promo items:", loadError) + setLoading(false); + } + } + + if (slug) { + loadPromo() + } + }, [slug]) + + const map = async (promotions: any[]): Promise<IPromotion[]> => { + const result: IPromotion[] = [] + + for (const promotion of promotions) { + const data: IPromotion = { + id: promotion.id, + program_id: promotion.program_id_i, + name: promotion.name_s, + type: { + value: promotion.type_value_s, + label: promotion.type_label_s, + }, + limit: promotion.package_limit_i, + limit_user: promotion.package_limit_user_i, + limit_trx: promotion.package_limit_trx_i, + price: promotion.price_f, + total_qty: promotion.total_qty_i, + products: JSON.parse(promotion.products_s), + free_products: JSON.parse(promotion.free_products_s), + } + + result.push(data) + } + + return result + } + + + + + useEffect(() => { + const handleScroll = () => { + if ( + !fetchingData && + window.innerHeight + document.documentElement.scrollTop >= 0.95 * document.documentElement.offsetHeight + ) { + // User has scrolled to 95% of page height + + setTimeout(() => setFetchingData(true), 120); + setCurrentPage((prevPage) => prevPage + 1) + } + } + + window.addEventListener('scroll', handleScroll) + return () => window.removeEventListener('scroll', handleScroll) + }, [fetchingData]) + + useEffect(() => { + if (fetchingData) { + // Fetch more data + // You may need to adjust this logic according to your API + fetchMoreData() + } + }, [fetchingData]) + + const fetchMoreData = async () => { + try { + // Add a delay of approximately 150ms + setTimeout(async () => { + // Fetch more data + // Update promoData state with the new data + }, 150) + } catch (error) { + console.error('Error fetching more data:', error) + } finally { + setTimeout(() => setFetchingData(false), 120); + + } + } + + const visiblePromotions = promoData?.slice(0, currentPage * 12) + + return ( + <BasicLayout> + <Seo + title={`Promo ${Array.isArray(slug) ? slug[0] : slug} Terkini`} + description='B2B Marketplace MRO & Industri dengan Layanan Pembayaran Tempo, Faktur Pajak, Online Quotation, Garansi Resmi & Harga Kompetitif' + /> + {/* <Promocrumb brandName={capitalizeFirstLetter(Array.isArray(slug) ? slug[0] : slug)} /> */} + <div className='container mx-auto mt-1 flex mb-1'> + <div className=''> + <h1 className='font-semibold'>Semua Promo di Indoteknik</h1> + </div> + </div> + {loading ? ( + <div className='container flex justify-center my-4'> + <LogoSpinner width={48} height={48} /> + </div> + ) : promoData && promoItems.length >= 1 ? ( + <> + <div className='flex flex-wrap justify-center'> + {visiblePromotions?.map((promotion) => ( + <div key={promotion.id} className="min-w-[40px] max-w-[400px] mr-[20px] mb-[20px] sm:w-full md:w-1/2 lg:w-1/3 xl:w-1/4"> + <ProductPromoCard promotion={promotion} /> + </div> + ))} + </div> + {fetchingData && ( + <div className='container flex justify-center my-4'> + <LogoSpinner width={48} height={48} /> + </div> + )} + </> + ) : ( + <div className="text-center my-8"> + <p>Belum ada promo pada kategori ini</p> + </div> + )} + </BasicLayout> + ) +} |
