diff --git a/src/web/package.json b/src/web/package.json index 7d0dcab57..59f50ec96 100644 --- a/src/web/package.json +++ b/src/web/package.json @@ -40,7 +40,6 @@ "react-icons": "^4.12.0", "react-modal": "^3.16.1", "react-moment": "^1.1.3", - "react-responsive-carousel": "^3.2.23", "react-select": "^5.8.0", "react-toastify": "^9.1.3", "sass": "^1.69.5", diff --git a/src/web/src/components/Marketplace/StoreItemsCarousel.tsx b/src/web/src/components/Marketplace/StoreItemsCarousel.tsx index bedd2dea7..f8cabb2a5 100644 --- a/src/web/src/components/Marketplace/StoreItemsCarousel.tsx +++ b/src/web/src/components/Marketplace/StoreItemsCarousel.tsx @@ -1,182 +1,197 @@ -import Link from "next/link"; -import { Carousel } from "react-responsive-carousel"; -import "react-responsive-carousel/lib/styles/carousel.min.css"; -import { IoIosArrowBack, IoIosArrowForward } from "react-icons/io"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { screenWidthAtom } from "~/lib/store"; -import { useAtomValue } from "jotai"; -import { VIEWPORT_SIZE } from "~/lib/constants"; -import { LoadingSkeleton } from "../Status/LoadingSkeleton"; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import type { EngineType } from "embla-carousel/components/Engine"; +import type { EmblaCarouselType, EmblaOptionsType } from "embla-carousel"; +import useEmblaCarousel from "embla-carousel-react"; import type { StoreItemCategorySearchResults } from "~/api/models/marketplace"; import { ItemCardComponent } from "./ItemCard"; +import Link from "next/link"; +import { PAGE_SIZE_MINIMUM } from "~/lib/constants"; +import { + usePrevNextButtons, + PrevButton, + NextButton, +} from "../Carousel/ArrowButtons"; +import { + SelectedSnapDisplay, + useSelectedSnapDisplay, +} from "../Carousel/SelectedSnapDisplay"; + +const OPTIONS: EmblaOptionsType = { + dragFree: false, + containScroll: "keepSnaps", + watchSlides: true, + watchResize: true, +}; -export const StoreItemsCarousel: React.FC<{ +const StoreItemsCarousel: React.FC<{ [id: string]: any; title?: string; - data: StoreItemCategorySearchResults; viewAllUrl?: string; loadData: (startRow: number) => Promise; onClick: (item: any) => void; -}> = ({ id, title, data, viewAllUrl, loadData, onClick }) => { - const screenWidth = useAtomValue(screenWidthAtom); - const [cache, setCache] = useState(data?.items); - const isLoadingDataRef = useRef(false); - const [selectedItem, setSelectedItem] = useState(0); - const [slidePercentage, setSlidePercentage] = useState(screenWidth); - const [cols, setCols] = useState(1); - - useEffect(() => { - // update the slide percentage based on the viewport size - const slidePercentage = - screenWidth < VIEWPORT_SIZE.SM - ? 100 // 1 column - : screenWidth < VIEWPORT_SIZE.LG - ? 50 // 2 columns - : screenWidth < VIEWPORT_SIZE.XL - ? 33 // 3 columns - : 33; - setSlidePercentage(slidePercentage); - - // calculate the number of columns based on the viewport size - setCols(Math.round(100 / slidePercentage)); - - // reset to first item when resizing (UX fix with changing of carousel column) - setSelectedItem(0); - }, [screenWidth, setSelectedItem, setCols]); - - const onChange = useCallback( - async (index: number) => { - // if data is currently being loaded, do nothing - if (isLoadingDataRef.current) return; - - // calculate the start row based on the current index - const startRow = index + 1; - - // HACK: Update the selected item - // this helps move the selected items along for larger displays - // prevents large gaps around the selected item - if (cols > 2 && index == 1) { - //console.warn("SKIPPING... for larger displays: ", index + 1); - setSelectedItem(index + 1); - return; - } - - // if there's enough data in the cache, skip fetching more data - if (startRow + cols <= cache.length) { - // if the index is not close to the end, skip fetching more rows - return; + data: StoreItemCategorySearchResults; + options?: EmblaOptionsType; +}> = (props) => { + const { id, title, viewAllUrl, loadData, onClick, data: propData } = props; + const scrollListenerRef = useRef<() => void>(() => undefined); + const listenForScrollRef = useRef(true); + const hasMoreToLoadRef = useRef(true); + const [slides, setSlides] = useState(propData.items); + const [hasMoreToLoad, setHasMoreToLoad] = useState( + propData.items.length >= PAGE_SIZE_MINIMUM, + ); + const [loadingMore, setLoadingMore] = useState(false); + + const [emblaRef, emblaApi] = useEmblaCarousel({ + ...OPTIONS, + watchSlides: (emblaApi) => { + const reloadEmbla = (): void => { + const oldEngine = emblaApi.internalEngine(); + + emblaApi.reInit(); + const newEngine = emblaApi.internalEngine(); + const copyEngineModules: (keyof EngineType)[] = [ + "location", + "target", + "scrollBody", + ]; + copyEngineModules.forEach((engineModule) => { + Object.assign(newEngine[engineModule], oldEngine[engineModule]); + }); + + newEngine.translate.to(oldEngine.location.get()); + const { index } = newEngine.scrollTarget.byDistance(0, false); + newEngine.index.set(index); + newEngine.animation.start(); + + setLoadingMore(false); + listenForScrollRef.current = true; + }; + + const reloadAfterPointerUp = (): void => { + emblaApi.off("pointerUp", reloadAfterPointerUp); + reloadEmbla(); + }; + + const engine = emblaApi.internalEngine(); + + if (hasMoreToLoadRef.current && engine.dragHandler.pointerDown()) { + const boundsActive = engine.limit.reachedMax(engine.target.get()); + engine.scrollBounds.toggleActive(boundsActive); + emblaApi.on("pointerUp", reloadAfterPointerUp); + } else { + reloadEmbla(); } + }, + }); + const { selectedSnap, snapCount } = useSelectedSnapDisplay(emblaApi); + + const { + prevBtnDisabled, + nextBtnDisabled, + onPrevButtonClick, + onNextButtonClick, + } = usePrevNextButtons(emblaApi); + + const onScroll = useCallback( + (emblaApi: EmblaCarouselType) => { + if (!listenForScrollRef.current) return; + + setLoadingMore((loadingMore) => { + const lastSlide = emblaApi.slideNodes().length - 1; + const lastSlideInView = emblaApi.slidesInView().includes(lastSlide); + let loadMore = !loadingMore && lastSlideInView; + + // console.warn( + // `onScroll... lastSlide: ${lastSlide} lastSlideInView: ${lastSlideInView} loadMore: ${loadMore}`, + // ); + if (emblaApi.slideNodes().length < PAGE_SIZE_MINIMUM) { + loadMore = false; + setHasMoreToLoad(false); + } + + if (loadMore) { + listenForScrollRef.current = false; + + console.warn( + `Loading more data... ${lastSlide} lastSlideInView: ${lastSlideInView} nextStartRow: ${ + emblaApi.slideNodes().length + 1 + }`, + ); + + loadData(emblaApi.slideNodes().length + 1).then((data) => { + if (data.items.length == 0) { + setHasMoreToLoad(false); + emblaApi.off("scroll", scrollListenerRef.current); + } + + setSlides((prevSlides) => [...prevSlides, ...data.items]); + }); + } + + return loadingMore || lastSlideInView; + }); + }, + [loadData], + ); - // if we've reached this point, we need to fetch more data - isLoadingDataRef.current = true; - - // fetch more data - const nextStartRow = Math.round((startRow + 1) / cols) * cols + 1; - const newData = await loadData?.(nextStartRow); - - // filter out any items that are already in the cacheRef.current.items - const local = cache; - const newItems = newData?.items.filter( - (newItem) => !local.find((item) => item.id === newItem.id), - ); + const addScrollListener = useCallback( + (emblaApi: EmblaCarouselType) => { + scrollListenerRef.current = () => onScroll(emblaApi); + emblaApi.on("scroll", scrollListenerRef.current); + }, + [onScroll], + ); - // set isLoadingData to false now that the data has been loaded - isLoadingDataRef.current = false; + useEffect(() => { + if (!emblaApi) return; + addScrollListener(emblaApi); - // set the cacheRef.current.items to the new data - setCache([...cache, ...newItems]); + const onResize = () => emblaApi.reInit(); + window.addEventListener("resize", onResize); + emblaApi.on("destroy", () => + window.removeEventListener("resize", onResize), + ); + }, [emblaApi, addScrollListener]); - // HACK: this helps move the carousel along with the new data for larger displays - if (newItems.length > 0 && cols > 2) { - setSelectedItem(index); - } - }, - [loadData, isLoadingDataRef, cache, setCache, setSelectedItem, cols], - ); + useEffect(() => { + hasMoreToLoadRef.current = hasMoreToLoad; + }, [hasMoreToLoad]); return ( -
- {(data?.items?.length ?? 0) > 0 && ( -
-
-
-
- {title} -
+ <> +
+
+
+
+ {title}
- {viewAllUrl && ( - - View all - - )}
- - {slidePercentage <= 0 && ( -
- -
- )} - - {slidePercentage > 0 && ( - void, - hasPrev: boolean, - label: string, - ) => - hasPrev && ( - - ) - } - renderArrowNext={( - onClickHandler: () => void, - hasNext: boolean, - label: string, - ) => - hasNext && - !(cache.length < cols) && ( - - ) - } + {viewAllUrl && ( + - {cache.map((item: any, index: number) => ( -
+ View all + + )} +
+ {/* {slidePercentage <= 0 && ( +
+ +
+ )} */} +
+ +
+
+
+ {slides?.map((item, index) => ( +
+
onClick(item)} />
- ))} - - )} +
+ ))} + {hasMoreToLoad && ( +
+ +
+ )} +
- )} -
+ + {snapCount > 1 && selectedSnap < snapCount && ( +
+ + + + + +
+ )} +
+ ); }; + +export default StoreItemsCarousel; diff --git a/src/web/src/components/Marketplace/StoreItemsCarousel2.tsx b/src/web/src/components/Marketplace/StoreItemsCarousel2.tsx deleted file mode 100644 index 500bffdb8..000000000 --- a/src/web/src/components/Marketplace/StoreItemsCarousel2.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; -import type { EngineType } from "embla-carousel/components/Engine"; -import type { EmblaCarouselType, EmblaOptionsType } from "embla-carousel"; -import useEmblaCarousel from "embla-carousel-react"; -import type { StoreItemCategorySearchResults } from "~/api/models/marketplace"; -import { ItemCardComponent } from "./ItemCard"; -import Link from "next/link"; -import { PAGE_SIZE_MINIMUM } from "~/lib/constants"; -import { - usePrevNextButtons, - PrevButton, - NextButton, -} from "../Carousel/ArrowButtons"; -import { - SelectedSnapDisplay, - useSelectedSnapDisplay, -} from "../Carousel/SelectedSnapDisplay"; - -const OPTIONS: EmblaOptionsType = { - dragFree: true, - containScroll: "keepSnaps", - watchSlides: true, - watchResize: true, -}; - -const StoreItemsCarousel2: React.FC<{ - [id: string]: any; - title?: string; - viewAllUrl?: string; - loadData: (startRow: number) => Promise; - onClick: (item: any) => void; - data: StoreItemCategorySearchResults; - options?: EmblaOptionsType; -}> = (props) => { - const { id, title, viewAllUrl, loadData, onClick, data: propData } = props; - const scrollListenerRef = useRef<() => void>(() => undefined); - const listenForScrollRef = useRef(true); - const hasMoreToLoadRef = useRef(true); - const [slides, setSlides] = useState(propData.items); - const [hasMoreToLoad, setHasMoreToLoad] = useState( - propData.items.length >= PAGE_SIZE_MINIMUM, - ); - const [loadingMore, setLoadingMore] = useState(false); - - const [emblaRef, emblaApi] = useEmblaCarousel({ - ...OPTIONS, - watchSlides: (emblaApi) => { - const reloadEmbla = (): void => { - const oldEngine = emblaApi.internalEngine(); - - emblaApi.reInit(); - const newEngine = emblaApi.internalEngine(); - const copyEngineModules: (keyof EngineType)[] = [ - "location", - "target", - "scrollBody", - ]; - copyEngineModules.forEach((engineModule) => { - Object.assign(newEngine[engineModule], oldEngine[engineModule]); - }); - - newEngine.translate.to(oldEngine.location.get()); - const { index } = newEngine.scrollTarget.byDistance(0, false); - newEngine.index.set(index); - newEngine.animation.start(); - - setLoadingMore(false); - listenForScrollRef.current = true; - }; - - const reloadAfterPointerUp = (): void => { - emblaApi.off("pointerUp", reloadAfterPointerUp); - reloadEmbla(); - }; - - const engine = emblaApi.internalEngine(); - - if (hasMoreToLoadRef.current && engine.dragHandler.pointerDown()) { - const boundsActive = engine.limit.reachedMax(engine.target.get()); - engine.scrollBounds.toggleActive(boundsActive); - emblaApi.on("pointerUp", reloadAfterPointerUp); - } else { - reloadEmbla(); - } - }, - }); - const { selectedSnap, snapCount } = useSelectedSnapDisplay(emblaApi); - - const { - prevBtnDisabled, - nextBtnDisabled, - onPrevButtonClick, - onNextButtonClick, - } = usePrevNextButtons(emblaApi); - - const onScroll = useCallback( - (emblaApi: EmblaCarouselType) => { - if (!listenForScrollRef.current) return; - - setLoadingMore((loadingMore) => { - const lastSlide = emblaApi.slideNodes().length - 1; - const lastSlideInView = emblaApi.slidesInView().includes(lastSlide); - let loadMore = !loadingMore && lastSlideInView; - - // console.warn( - // `onScroll... lastSlide: ${lastSlide} lastSlideInView: ${lastSlideInView} loadMore: ${loadMore}`, - // ); - if (emblaApi.slideNodes().length < PAGE_SIZE_MINIMUM) { - loadMore = false; - setHasMoreToLoad(false); - } - - if (loadMore) { - listenForScrollRef.current = false; - - console.warn( - `Loading more data... ${lastSlide} lastSlideInView: ${lastSlideInView} nextStartRow: ${ - emblaApi.slideNodes().length + 1 - }`, - ); - - loadData(emblaApi.slideNodes().length + 1).then((data) => { - if (data.items.length == 0) { - setHasMoreToLoad(false); - emblaApi.off("scroll", scrollListenerRef.current); - } - - setSlides((prevSlides) => [...prevSlides, ...data.items]); - }); - } - - return loadingMore || lastSlideInView; - }); - }, - [loadData], - ); - - const addScrollListener = useCallback( - (emblaApi: EmblaCarouselType) => { - scrollListenerRef.current = () => onScroll(emblaApi); - emblaApi.on("scroll", scrollListenerRef.current); - }, - [onScroll], - ); - - useEffect(() => { - if (!emblaApi) return; - addScrollListener(emblaApi); - - const onResize = () => emblaApi.reInit(); - window.addEventListener("resize", onResize); - emblaApi.on("destroy", () => - window.removeEventListener("resize", onResize), - ); - }, [emblaApi, addScrollListener]); - - useEffect(() => { - hasMoreToLoadRef.current = hasMoreToLoad; - }, [hasMoreToLoad]); - - return ( - <> -
-
-
-
- {title} -
-
- {viewAllUrl && ( - - View all - - )} -
- {/* {slidePercentage <= 0 && ( -
- -
- )} */} -
- -
-
-
- {slides?.map((item, index) => ( -
-
- onClick(item)} - /> -
-
- ))} - {hasMoreToLoad && ( -
- -
- )} -
-
- - {snapCount > 1 && selectedSnap < snapCount && ( -
- - - - - -
- )} -
- - ); -}; - -export default StoreItemsCarousel2; diff --git a/src/web/src/components/Opportunity/OpportunitiesCarousel.tsx b/src/web/src/components/Opportunity/OpportunitiesCarousel.tsx index 8e1b082b6..b5f1e5cb5 100644 --- a/src/web/src/components/Opportunity/OpportunitiesCarousel.tsx +++ b/src/web/src/components/Opportunity/OpportunitiesCarousel.tsx @@ -1,203 +1,230 @@ -import { OpportunityPublicSmallComponent } from "./OpportunityPublicSmall"; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import type { EngineType } from "embla-carousel/components/Engine"; +import type { EmblaCarouselType, EmblaOptionsType } from "embla-carousel"; +import useEmblaCarousel from "embla-carousel-react"; import type { OpportunitySearchResultsInfo } from "~/api/models/opportunity"; import Link from "next/link"; -import { Carousel } from "react-responsive-carousel"; -import "react-responsive-carousel/lib/styles/carousel.min.css"; -import { IoIosArrowBack, IoIosArrowForward } from "react-icons/io"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { screenWidthAtom } from "~/lib/store"; -import { useAtomValue } from "jotai"; -import { VIEWPORT_SIZE } from "~/lib/constants"; -import { LoadingSkeleton } from "../Status/LoadingSkeleton"; - -export const OpportunitiesCarousel: React.FC<{ +import { PAGE_SIZE_MINIMUM } from "~/lib/constants"; +import { OpportunityPublicSmallComponent } from "./OpportunityPublicSmall"; +import { + SelectedSnapDisplay, + useSelectedSnapDisplay, +} from "../Carousel/SelectedSnapDisplay"; +import { + usePrevNextButtons, + PrevButton, + NextButton, +} from "../Carousel/ArrowButtons"; + +const OPTIONS: EmblaOptionsType = { + dragFree: false, + containScroll: "keepSnaps", + watchSlides: true, + watchResize: true, +}; + +const OpportunitiesCarousel: React.FC<{ [id: string]: any; title?: string; - data: OpportunitySearchResultsInfo; viewAllUrl?: string; loadData: (startRow: number) => Promise; -}> = ({ id, title, data, viewAllUrl, loadData }) => { - const screenWidth = useAtomValue(screenWidthAtom); - const [cache, setCache] = useState(data?.items); - const isLoadingDataRef = useRef(false); - const [selectedItem, setSelectedItem] = useState(0); - const [cols, setCols] = useState(1); - - const getSlidePercentage = (screenWidth: number) => { - if (screenWidth < VIEWPORT_SIZE.SM) { - return 100; // 1 column - } else if (screenWidth < VIEWPORT_SIZE.LG) { - return 50; // 2 columns - } else if (screenWidth < VIEWPORT_SIZE.XL) { - return 33; // 3 columns - } else if (screenWidth < VIEWPORT_SIZE["2XL"]) { - return 25; // 4 columns - } else { - return 25; - } - }; - - // calculate the slider percentage based on the viewport size - // i.e 33% = cols 3, 25% = cols 4 etc - const [slidePercentage, setSlidePercentage] = useState( - getSlidePercentage(screenWidth), - ); - - useEffect(() => { - // update the slide percentage based on the viewport size - setSlidePercentage(getSlidePercentage(screenWidth)); - - // calculate the number of columns based on the viewport size - setCols(Math.round(100 / slidePercentage)); - - // reset to first item when resizing (UX fix with changing of carousel column) - setSelectedItem(0); - }, [screenWidth, setSelectedItem, setCols, slidePercentage]); - - const onChange = useCallback( - async (index: number) => { - // if data is currently being loaded, do nothing - if (isLoadingDataRef.current) return; - - // calculate the start row based on the current index - const startRow = index + 1; - - // console.warn( - // `index: ${index}, startRow: ${startRow}, nextStartRow: ${nextStartRow} cols: ${cols}`, - // ); - - // HACK: Update the selected item - // this helps move the selected items along for larger displays - // prevents large gaps around the selected item - if (cols > 2 && index == 1) { - //console.warn("SKIPPING... for larger displays: ", index + 1); - setSelectedItem(index + 1); - return; - } - - // if there's enough data in the cache, skip fetching more data - if (startRow + cols <= cache.length) { - //console.warn("SKIPPING... enough data"); - // if the index is not close to the end, skip fetching more rows - return; + data: OpportunitySearchResultsInfo; + options?: EmblaOptionsType; +}> = (props) => { + const { id, title, viewAllUrl, loadData, data: propData } = props; + const scrollListenerRef = useRef<() => void>(() => undefined); + const listenForScrollRef = useRef(true); + const hasMoreToLoadRef = useRef(true); + const [slides, setSlides] = useState(propData.items); + const [hasMoreToLoad, setHasMoreToLoad] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + + const [emblaRef, emblaApi] = useEmblaCarousel({ + ...OPTIONS, + watchSlides: (emblaApi) => { + const reloadEmbla = (): void => { + const oldEngine = emblaApi.internalEngine(); + + emblaApi.reInit(); + const newEngine = emblaApi.internalEngine(); + const copyEngineModules: (keyof EngineType)[] = [ + "location", + "target", + "scrollBody", + ]; + copyEngineModules.forEach((engineModule) => { + Object.assign(newEngine[engineModule], oldEngine[engineModule]); + }); + + newEngine.translate.to(oldEngine.location.get()); + const { index } = newEngine.scrollTarget.byDistance(0, false); + newEngine.index.set(index); + newEngine.animation.start(); + + setLoadingMore(false); + listenForScrollRef.current = true; + }; + + const reloadAfterPointerUp = (): void => { + emblaApi.off("pointerUp", reloadAfterPointerUp); + reloadEmbla(); + }; + + const engine = emblaApi.internalEngine(); + + if (hasMoreToLoadRef.current && engine.dragHandler.pointerDown()) { + const boundsActive = engine.limit.reachedMax(engine.target.get()); + engine.scrollBounds.toggleActive(boundsActive); + emblaApi.on("pointerUp", reloadAfterPointerUp); + } else { + reloadEmbla(); } + }, + }); + const { selectedSnap, snapCount } = useSelectedSnapDisplay(emblaApi); + + const { + prevBtnDisabled, + nextBtnDisabled, + onPrevButtonClick, + onNextButtonClick, + } = usePrevNextButtons(emblaApi); + + const onScroll = useCallback( + (emblaApi: EmblaCarouselType) => { + if (!listenForScrollRef.current) return; + + setLoadingMore((loadingMore) => { + const lastSlide = emblaApi.slideNodes().length - 1; + const lastSlideInView = emblaApi.slidesInView().includes(lastSlide); + let loadMore = !loadingMore && lastSlideInView; + + // console.warn( + // `onScroll... lastSlide: ${lastSlide} lastSlideInView: ${lastSlideInView} loadMore: ${loadMore}`, + // ); + if (emblaApi.slideNodes().length < PAGE_SIZE_MINIMUM) { + loadMore = false; + } + + if (loadMore) { + listenForScrollRef.current = false; + + console.warn( + `Loading more data... ${lastSlide} lastSlideInView: ${lastSlideInView} nextStartRow: ${ + emblaApi.slideNodes().length + 1 + }`, + ); + + loadData(emblaApi.slideNodes().length + 1).then((data) => { + // debugger; + if (data.items.length == 0) { + setHasMoreToLoad(false); + emblaApi.off("scroll", scrollListenerRef.current); + } + + setSlides((prevSlides) => [...prevSlides, ...data.items]); + }); + } + + return loadingMore || lastSlideInView; + }); + }, + [loadData], + ); - // if we've reached this point, we need to fetch more data - isLoadingDataRef.current = true; - - // fetch more data - const nextStartRow = Math.round((startRow + 1) / cols) * cols + 1; - const newData = await loadData?.(nextStartRow); - - // filter out any items that are already in the cacheRef.current.items - const local = cache; - const newItems = newData?.items.filter( - (newItem) => !local.find((item) => item.id === newItem.id), - ); + const addScrollListener = useCallback( + (emblaApi: EmblaCarouselType) => { + scrollListenerRef.current = () => onScroll(emblaApi); + emblaApi.on("scroll", scrollListenerRef.current); + }, + [onScroll], + ); - // set isLoadingData to false now that the data has been loaded - isLoadingDataRef.current = false; + useEffect(() => { + if (!emblaApi) return; + addScrollListener(emblaApi); - // set the cacheRef.current.items to the new data - setCache([...cache, ...newItems]); + const onResize = () => emblaApi.reInit(); + window.addEventListener("resize", onResize); + emblaApi.on("destroy", () => + window.removeEventListener("resize", onResize), + ); + }, [emblaApi, addScrollListener]); - // HACK: this helps move the carousel along with the new data for larger displays - if (newItems.length > 0 && cols > 2) { - setSelectedItem(index); - } - }, - [loadData, isLoadingDataRef, cache, setCache, setSelectedItem, cols], - ); + useEffect(() => { + hasMoreToLoadRef.current = hasMoreToLoad; + }, [hasMoreToLoad]); return ( -
- {(data?.items?.length ?? 0) > 0 && ( -
-
-
-
- {title} -
+
+
+
+
+
+ {title}
- {viewAllUrl && ( - - View all - - )}
- - {slidePercentage <= 0 && ( -
- -
- )} - - {slidePercentage > 0 && ( - void, - hasPrev: boolean, - label: string, - ) => - hasPrev && ( - - ) - } - renderArrowNext={( - onClickHandler: () => void, - hasNext: boolean, - label: string, - ) => - hasNext && ( - - ) - } + {viewAllUrl && ( + - {cache.map((item: any) => ( -
+ View all + + )} +
+ {/* {slidePercentage <= 0 && ( +
+ +
+ )} */} +
+ +
+
+
+ {slides?.map((item, index) => ( +
+
- ))} - - )} +
+ ))} + {hasMoreToLoad && ( +
+ +
+ )} +
- )} + + {snapCount > 1 && selectedSnap < snapCount && ( +
+ + + + +
+ )} +
); }; + +export default OpportunitiesCarousel; diff --git a/src/web/src/components/Opportunity/OpportunitiesCarousel2.tsx b/src/web/src/components/Opportunity/OpportunitiesCarousel2.tsx deleted file mode 100644 index bfe8e139c..000000000 --- a/src/web/src/components/Opportunity/OpportunitiesCarousel2.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; -import type { EngineType } from "embla-carousel/components/Engine"; -import type { EmblaCarouselType, EmblaOptionsType } from "embla-carousel"; -import useEmblaCarousel from "embla-carousel-react"; -import type { OpportunitySearchResultsInfo } from "~/api/models/opportunity"; -import Link from "next/link"; -import { PAGE_SIZE_MINIMUM } from "~/lib/constants"; -import { OpportunityPublicSmallComponent } from "./OpportunityPublicSmall"; -import { - SelectedSnapDisplay, - useSelectedSnapDisplay, -} from "../Carousel/SelectedSnapDisplay"; -import { - usePrevNextButtons, - PrevButton, - NextButton, -} from "../Carousel/ArrowButtons"; - -const OPTIONS: EmblaOptionsType = { - dragFree: true, - containScroll: "keepSnaps", - watchSlides: true, - watchResize: true, -}; - -const OpportunitiesCarousel2: React.FC<{ - [id: string]: any; - title?: string; - viewAllUrl?: string; - loadData: (startRow: number) => Promise; - data: OpportunitySearchResultsInfo; - options?: EmblaOptionsType; -}> = (props) => { - const { id, title, viewAllUrl, loadData, data: propData } = props; - const scrollListenerRef = useRef<() => void>(() => undefined); - const listenForScrollRef = useRef(true); - const hasMoreToLoadRef = useRef(true); - const [slides, setSlides] = useState(propData.items); - const [hasMoreToLoad, setHasMoreToLoad] = useState(true); - const [loadingMore, setLoadingMore] = useState(false); - - const [emblaRef, emblaApi] = useEmblaCarousel({ - ...OPTIONS, - watchSlides: (emblaApi) => { - const reloadEmbla = (): void => { - const oldEngine = emblaApi.internalEngine(); - - emblaApi.reInit(); - const newEngine = emblaApi.internalEngine(); - const copyEngineModules: (keyof EngineType)[] = [ - "location", - "target", - "scrollBody", - ]; - copyEngineModules.forEach((engineModule) => { - Object.assign(newEngine[engineModule], oldEngine[engineModule]); - }); - - newEngine.translate.to(oldEngine.location.get()); - const { index } = newEngine.scrollTarget.byDistance(0, false); - newEngine.index.set(index); - newEngine.animation.start(); - - setLoadingMore(false); - listenForScrollRef.current = true; - }; - - const reloadAfterPointerUp = (): void => { - emblaApi.off("pointerUp", reloadAfterPointerUp); - reloadEmbla(); - }; - - const engine = emblaApi.internalEngine(); - - if (hasMoreToLoadRef.current && engine.dragHandler.pointerDown()) { - const boundsActive = engine.limit.reachedMax(engine.target.get()); - engine.scrollBounds.toggleActive(boundsActive); - emblaApi.on("pointerUp", reloadAfterPointerUp); - } else { - reloadEmbla(); - } - }, - }); - const { selectedSnap, snapCount } = useSelectedSnapDisplay(emblaApi); - - const { - prevBtnDisabled, - nextBtnDisabled, - onPrevButtonClick, - onNextButtonClick, - } = usePrevNextButtons(emblaApi); - - const onScroll = useCallback( - (emblaApi: EmblaCarouselType) => { - if (!listenForScrollRef.current) return; - - setLoadingMore((loadingMore) => { - const lastSlide = emblaApi.slideNodes().length - 1; - const lastSlideInView = emblaApi.slidesInView().includes(lastSlide); - let loadMore = !loadingMore && lastSlideInView; - - // console.warn( - // `onScroll... lastSlide: ${lastSlide} lastSlideInView: ${lastSlideInView} loadMore: ${loadMore}`, - // ); - if (emblaApi.slideNodes().length < PAGE_SIZE_MINIMUM) { - loadMore = false; - } - - if (loadMore) { - listenForScrollRef.current = false; - - console.warn( - `Loading more data... ${lastSlide} lastSlideInView: ${lastSlideInView} nextStartRow: ${ - emblaApi.slideNodes().length + 1 - }`, - ); - - loadData(emblaApi.slideNodes().length + 1).then((data) => { - // debugger; - if (data.items.length == 0) { - setHasMoreToLoad(false); - emblaApi.off("scroll", scrollListenerRef.current); - } - - setSlides((prevSlides) => [...prevSlides, ...data.items]); - }); - } - - return loadingMore || lastSlideInView; - }); - }, - [loadData], - ); - - const addScrollListener = useCallback( - (emblaApi: EmblaCarouselType) => { - scrollListenerRef.current = () => onScroll(emblaApi); - emblaApi.on("scroll", scrollListenerRef.current); - }, - [onScroll], - ); - - useEffect(() => { - if (!emblaApi) return; - addScrollListener(emblaApi); - - const onResize = () => emblaApi.reInit(); - window.addEventListener("resize", onResize); - emblaApi.on("destroy", () => - window.removeEventListener("resize", onResize), - ); - }, [emblaApi, addScrollListener]); - - useEffect(() => { - hasMoreToLoadRef.current = hasMoreToLoad; - }, [hasMoreToLoad]); - - return ( -
-
-
-
-
- {title} -
-
- {viewAllUrl && ( - - View all - - )} -
- {/* {slidePercentage <= 0 && ( -
- -
- )} */} -
- -
-
-
- {slides?.map((item, index) => ( -
-
- -
-
- ))} - {hasMoreToLoad && ( -
- -
- )} -
-
- - {snapCount > 1 && selectedSnap < snapCount && ( -
- - - - -
- )} -
-
- ); -}; - -export default OpportunitiesCarousel2; diff --git a/src/web/src/pages/marketplace/[country]/index.tsx b/src/web/src/pages/marketplace/[country]/index.tsx index 0bb5e87df..42e0679e9 100644 --- a/src/web/src/pages/marketplace/[country]/index.tsx +++ b/src/web/src/pages/marketplace/[country]/index.tsx @@ -47,7 +47,7 @@ import { Unauthenticated } from "~/components/Status/Unauthenticated"; import { Unauthorized } from "~/components/Status/Unauthorized"; import { env } from "process"; import { MarketplaceDown } from "~/components/Status/MarketplaceDown"; -import StoreItemsCarousel2 from "~/components/Marketplace/StoreItemsCarousel2"; +import StoreItemsCarousel from "~/components/Marketplace/StoreItemsCarousel"; interface IParams extends ParsedUrlQuery { country: string; @@ -755,7 +755,7 @@ const MarketplaceStoreCategories: NextPageWithLayout<{
- {/* TRENDING */} {(opportunities_trending?.totalCount ?? 0) > 0 && ( - 0 && ( - 0 && ( - 0 && ( -