diff --git a/src/components/project-card/ProjectCard.tsx b/src/components/project-card/ProjectCard.tsx index 9dfd88caa6..cacd19cc4c 100644 --- a/src/components/project-card/ProjectCard.tsx +++ b/src/components/project-card/ProjectCard.tsx @@ -178,6 +178,7 @@ const ProjectCard = (props: IProjectCard) => { { const { formatMessage } = useIntl(); const { projects, totalCount: _totalCount } = props; @@ -60,110 +53,72 @@ const ProjectsIndex = (props: IProjectsView) => { const { activeQFRound, mainCategories } = useAppSelector( state => state.general, ); - const [isLoading, setIsLoading] = useState(false); - const [isNotFound, setIsNotFound] = useState(false); - const [filteredProjects, setFilteredProjects] = - useState(projects); - const [totalCount, setTotalCount] = useState(_totalCount); const isMobile = useMediaQuery(`(max-width: ${deviceSize.tablet - 1}px)`); - const dispatch = useAppDispatch(); - const { variables: contextVariables, selectedMainCategory, isQF, isArchivedQF, } = useProjectsContext(); - const router = useRouter(); - const pageNum = useRef(0); const lastElementRef = useRef(null); const isInfiniteScrolling = useRef(true); - router?.events?.on('routeChangeStart', () => setIsLoading(true)); - - const fetchProjects = useCallback( - (isLoadMore?: boolean, loadNum?: number, userIdChanged = false) => { - const variables: IQueries = { - limit: userIdChanged - ? filteredProjects.length > 50 - ? BACKEND_QUERY_LIMIT - : filteredProjects.length - : projects.length, - skip: userIdChanged ? 0 : projects.length * (loadNum || 0), - }; - - if (user?.id) { - variables.connectedWalletUserId = Number(user?.id); - } + // Define the fetch function for React Query + const fetchProjectsPage = async ({ pageParam = 0 }) => { + const variables: IQueries = { + limit: 20, // Adjust the limit as needed + skip: 20 * pageParam, + }; - setIsLoading(true); - if ( - contextVariables.mainCategory !== router.query?.slug?.toString() - ) - return; - - client - .query({ - query: FETCH_ALL_PROJECTS, - variables: { - ...variables, - ...contextVariables, - mainCategory: isArchivedQF - ? undefined - : getMainCategorySlug(selectedMainCategory), - qfRoundSlug: isArchivedQF ? router.query.slug : null, - }, - }) - .then((res: { data: { allProjects: IFetchAllProjects } }) => { - const data = res.data?.allProjects?.projects; - const count = res.data?.allProjects?.totalCount; - setTotalCount(count); - - setFilteredProjects(prevProjects => { - isInfiniteScrolling.current = - (data.length + prevProjects.length) % 45 !== 0; - return isLoadMore ? [...prevProjects, ...data] : data; - }); - setIsLoading(false); - }) - .catch((err: any) => { - setIsLoading(false); - showToastError(err); - captureException(err, { - tags: { - section: 'fetchAllProjects', - }, - }); - }); - }, - [ + if (user?.id) { + variables.connectedWalletUserId = Number(user.id); + } + + return await fetchProjects( + pageParam, + variables, contextVariables, - filteredProjects.length, isArchivedQF, - projects.length, + selectedMainCategory, router.query.slug, + ); + }; + + // Use the useInfiniteQuery hook with the new v5 API + const { + data, + error, + fetchNextPage, + hasNextPage, + isError, + isFetching, + isFetchingNextPage, + } = useInfiniteQuery({ + queryKey: [ + 'projects', + contextVariables, + isArchivedQF, selectedMainCategory, - user?.id, ], - ); - - useEffect(() => { - pageNum.current = 0; - fetchProjects(false, 0, true); - }, [user?.id]); - - useEffect(() => { - pageNum.current = 0; - fetchProjects(false, 0); - }, [contextVariables]); + queryFn: fetchProjectsPage, + getNextPageParam: lastPage => lastPage.nextCursor, + getPreviousPageParam: firstPage => firstPage.previousCursor, + initialPageParam: 0, + // placeholderData: keepPreviousData, + placeholderData: { + pageParams: [0], + pages: [{ data: projects, totalCount: _totalCount }], + }, + }); + // Function to load more data when scrolling const loadMore = useCallback(() => { - if (isLoading) return; - fetchProjects(true, pageNum.current + 1); - pageNum.current = pageNum.current + 1; - }, [fetchProjects, isLoading]); + if (hasNextPage) { + fetchNextPage(); + } + }, [fetchNextPage, hasNextPage]); const handleCreateButton = () => { if (isUserRegistered(user)) { @@ -173,30 +128,28 @@ const ProjectsIndex = (props: IProjectsView) => { } }; - const showLoadMore = - totalCount > filteredProjects?.length && !isInfiniteScrolling.current; - const onProjectsPageOrActiveQFPage = !isQF || (isQF && activeQFRound); + // Intersection Observer for infinite scrolling useEffect(() => { - const handleObserver = (entities: any) => { + const handleObserver = (entries: IntersectionObserverEntry[]) => { if (!isInfiniteScrolling.current) return; - const target = entities[0]; + const target = entries[0]; if (target.isIntersecting) { loadMore(); } }; const option = { root: null, - threshold: 1, + threshold: 1.0, }; const observer = new IntersectionObserver(handleObserver, option); if (lastElementRef.current) { observer.observe(lastElementRef.current); } return () => { - if (observer) { - observer.disconnect(); + if (observer && lastElementRef.current) { + observer.unobserve(lastElementRef.current); } }; }, [loadMore]); @@ -207,16 +160,61 @@ const ProjectsIndex = (props: IProjectsView) => { !selectedMainCategory && !isArchivedQF ) { - setIsNotFound(true); + isInfiniteScrolling.current = false; + } else { + isInfiniteScrolling.current = true; } - }, [selectedMainCategory, mainCategories.length]); + }, [selectedMainCategory, mainCategories.length, isArchivedQF]); + + // Save last clicked project + const handleProjectClick = (slug: string) => { + sessionStorage.setItem(LAST_PROJECT_CLICKED, slug); + }; + + // Scroll to last clicked project + useEffect(() => { + if (!isFetching && !isFetchingNextPage) { + const lastProjectClicked = + sessionStorage.getItem(LAST_PROJECT_CLICKED); + if (lastProjectClicked) { + const element = document.getElementById(lastProjectClicked); + if (element) { + window.scrollTo({ + top: element.offsetTop, + behavior: 'smooth', + }); + } + sessionStorage.removeItem(LAST_PROJECT_CLICKED); + } + } + }, [isFetching, isFetchingNextPage]); + + // Handle errors + useEffect(() => { + if (isError && error) { + showToastError(error); + captureException(error, { + tags: { + section: 'fetchAllProjects', + }, + }); + } + }, [isError, error]); + + // Determine if no results should be shown + const isNotFound = + (mainCategories.length > 0 && !selectedMainCategory && !isArchivedQF) || + (!isQF && data?.pages?.[0]?.data.length === 0); if (isNotFound) return ; + const totalCount = data?.pages[data.pages.length - 1].totalCount || 0; + console.log('data', totalCount, data); + return ( <> - {isLoading && ( + {(isFetching || isFetchingNextPage) && ( @@ -251,8 +249,8 @@ const ProjectsIndex = (props: IProjectsView) => { )} - {isLoading && } - {filteredProjects?.length > 0 ? ( + {isFetchingNextPage && } + {data?.pages.some(page => page.data.length > 0) ? ( {isQF ? ( @@ -260,12 +258,23 @@ const ProjectsIndex = (props: IProjectsView) => { ) : ( )} - {filteredProjects.map((project, idx) => ( - + {data.pages.map((page, pageIndex) => ( + + {page.data.map((project, idx) => ( +
+ handleProjectClick(project.slug) + } + > + +
+ ))} +
))}
{/* */} @@ -275,22 +284,20 @@ const ProjectsIndex = (props: IProjectsView) => { ) : ( )} - {totalCount > filteredProjects?.length && ( -
- )} - {showLoadMore && ( + {hasNextPage &&
} + {!isFetching && !isFetchingNextPage && hasNextPage && ( <> fetchNextPage()} label={ - isLoading + isFetchingNextPage ? '' : formatMessage({ id: 'component.button.load_more', }) } icon={ - isLoading && ( + isFetchingNextPage && (
diff --git a/src/components/views/projects/constants.ts b/src/components/views/projects/constants.ts new file mode 100644 index 0000000000..9248de5056 --- /dev/null +++ b/src/components/views/projects/constants.ts @@ -0,0 +1 @@ +export const LAST_PROJECT_CLICKED = 'lastProjectClicked'; diff --git a/src/components/views/projects/services.ts b/src/components/views/projects/services.ts new file mode 100644 index 0000000000..41c8e4a60e --- /dev/null +++ b/src/components/views/projects/services.ts @@ -0,0 +1,53 @@ +// services/projectsService.ts + +import { client } from '@/apollo/apolloClient'; +import { FETCH_ALL_PROJECTS } from '@/apollo/gql/gqlProjects'; +import { IMainCategory, IProject } from '@/apollo/types/types'; +import { getMainCategorySlug } from '@/helpers/projects'; + +export interface IQueries { + skip?: number; + limit?: number; + connectedWalletUserId?: number; + mainCategory?: string; + qfRoundSlug?: string | null; +} + +export interface Page { + data: IProject[]; + previousCursor?: number; + nextCursor?: number; + totalCount?: number; +} + +export const fetchProjects = async ( + pageParam: number, + variables: IQueries, + contextVariables: any, + isArchivedQF?: boolean, + selectedMainCategory?: IMainCategory, + routerQuerySlug?: string | string[], +): Promise => { + const currentPage = pageParam; + + const res = await client.query({ + query: FETCH_ALL_PROJECTS, + variables: { + ...variables, + ...contextVariables, + mainCategory: isArchivedQF + ? undefined + : getMainCategorySlug(selectedMainCategory), + qfRoundSlug: isArchivedQF ? routerQuerySlug : null, + }, + }); + + const dataProjects: IProject[] = res.data?.allProjects?.projects; + + return { + data: dataProjects, + previousCursor: currentPage > 0 ? currentPage - 1 : undefined, + nextCursor: dataProjects.length > 0 ? currentPage + 1 : undefined, + totalCount: res.data?.allProjects?.totalCount, + }; +};