From cdfcb55191b4a00a1c10d4c65cdde81025ffc0f1 Mon Sep 17 00:00:00 2001 From: Cherik Date: Mon, 7 Oct 2024 19:14:04 +0330 Subject: [PATCH 1/4] add fetchProjects --- src/components/views/projects/services.ts | 52 +++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/components/views/projects/services.ts diff --git a/src/components/views/projects/services.ts b/src/components/views/projects/services.ts new file mode 100644 index 0000000000..f226f47968 --- /dev/null +++ b/src/components/views/projects/services.ts @@ -0,0 +1,52 @@ +// 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; +} + +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; + const count: number = res.data?.allProjects?.totalCount; + + return { + data: dataProjects, + previousCursor: currentPage > 0 ? currentPage - 1 : undefined, + nextCursor: dataProjects.length > 0 ? currentPage + 1 : undefined, + }; +}; From 0b3ea24a15ea28380f771b7b1b344ebe2f92ea45 Mon Sep 17 00:00:00 2001 From: Cherik Date: Mon, 7 Oct 2024 19:36:27 +0330 Subject: [PATCH 2/4] add total count --- src/components/views/projects/services.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/projects/services.ts b/src/components/views/projects/services.ts index f226f47968..41c8e4a60e 100644 --- a/src/components/views/projects/services.ts +++ b/src/components/views/projects/services.ts @@ -17,6 +17,7 @@ export interface Page { data: IProject[]; previousCursor?: number; nextCursor?: number; + totalCount?: number; } export const fetchProjects = async ( @@ -42,11 +43,11 @@ export const fetchProjects = async ( }); const dataProjects: IProject[] = res.data?.allProjects?.projects; - const count: number = res.data?.allProjects?.totalCount; return { data: dataProjects, previousCursor: currentPage > 0 ? currentPage - 1 : undefined, nextCursor: dataProjects.length > 0 ? currentPage + 1 : undefined, + totalCount: res.data?.allProjects?.totalCount, }; }; From 0bdc3888f4b3c6bb404784438f66ccb3437ae45a Mon Sep 17 00:00:00 2001 From: Cherik Date: Mon, 7 Oct 2024 19:36:58 +0330 Subject: [PATCH 3/4] update projects index --- .../views/projects/ProjectsIndex.tsx | 266 +++++++----------- 1 file changed, 106 insertions(+), 160 deletions(-) diff --git a/src/components/views/projects/ProjectsIndex.tsx b/src/components/views/projects/ProjectsIndex.tsx index ac90a452c1..21b2a291ef 100644 --- a/src/components/views/projects/ProjectsIndex.tsx +++ b/src/components/views/projects/ProjectsIndex.tsx @@ -1,24 +1,23 @@ -import { Fragment, useCallback, useEffect, useRef, useState } from 'react'; +// components/ProjectsIndex.tsx + +import { Fragment, useCallback, useEffect, useRef } from 'react'; import { useRouter } from 'next/router'; import { brandColors, - OutlineButton, - FlexCenter, Container, deviceSize, + FlexCenter, + mediaQueries, + OutlineButton, } from '@giveth/ui-design-system'; -import styled from 'styled-components'; import { useIntl } from 'react-intl'; import { captureException } from '@sentry/nextjs'; -import { QueryFunctionContext, useInfiniteQuery } from '@tanstack/react-query'; +import { useInfiniteQuery } from '@tanstack/react-query'; +import styled from 'styled-components'; import ProjectCard from '@/components/project-card/ProjectCard'; import Routes from '@/lib/constants/Routes'; import { isUserRegistered, showToastError } from '@/lib/helpers'; -import { FETCH_ALL_PROJECTS } from '@/apollo/gql/gqlProjects'; -import { client } from '@/apollo/apolloClient'; -import { IProject } from '@/apollo/types/types'; import ProjectsNoResults from '@/components/views/projects/ProjectsNoResults'; -import { mediaQueries } from '@/lib/constants/constants'; import { useAppDispatch, useAppSelector } from '@/features/hooks'; import { setShowCompleteProfile } from '@/features/modal/modal.slice'; import { ProjectsBanner } from './ProjectsBanner'; @@ -29,7 +28,6 @@ import { PassportBanner } from '@/components/PassportBanner'; import { QFProjectsMiddleBanner } from './MiddleBanners/QFMiddleBanner'; import { QFNoResultBanner } from './MiddleBanners/QFNoResultBanner'; import { Spinner } from '@/components/Spinner'; -import { getMainCategorySlug } from '@/helpers/projects'; import { FilterContainer } from './filter/FilterContainer'; import { SortContainer } from './sort/SortContainer'; import { ArchivedQFRoundStats } from './ArchivedQFRoundStats'; @@ -39,145 +37,87 @@ import useMediaQuery from '@/hooks/useMediaQuery'; import { QFHeader } from '@/components/views/archivedQFRounds/QFHeader'; import { DefaultQFBanner } from '@/components/DefaultQFBanner'; import NotAvailable from '@/components/NotAvailable'; +import { fetchProjects, IQueries } from './services'; +import { IProject } from '@/apollo/types/types'; export interface IProjectsView { projects: IProject[]; totalCount: number; } -interface IQueries { - skip?: number; - limit?: number; - connectedWalletUserId?: number; -} - -/** - * A page of projects - return type in fetchProjects function - */ -interface Page { - data: IProject[]; - previousCursor?: number; - nextCursor?: number; -} - const ProjectsIndex = (props: IProjectsView) => { const { formatMessage } = useIntl(); const { projects, totalCount: _totalCount } = props; - const [projectsData, setProjectsData] = useState<{ - projects: IProject[]; - totalPages: number; - totalCount: number; - }>({ - projects: projects, - totalPages: Math.ceil(_totalCount / projects.length), - totalCount: _totalCount, - }); const user = useAppSelector(state => state.user.userData); - const { activeQFRound, mainCategories } = useAppSelector( state => state.general, ); - const [isNotFound, setIsNotFound] = useState(false); - const isMobile = useMediaQuery(`(max-width: ${deviceSize.tablet - 1}px)`); - const dispatch = useAppDispatch(); - const { variables: contextVariables, selectedMainCategory, isQF, isArchivedQF, } = useProjectsContext(); - const router = useRouter(); const lastElementRef = useRef(null); const isInfiniteScrolling = useRef(true); - const fetchProjects = useCallback( - async (pageParam: number | unknown): Promise => { - const currentPage = typeof pageParam === 'number' ? pageParam : 0; - - const variables: IQueries = { - limit: projects.length, - skip: projects.length * (currentPage || 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, + }; - const res = await client.query({ - query: FETCH_ALL_PROJECTS, - variables: { - ...variables, - ...contextVariables, - mainCategory: isArchivedQF - ? undefined - : getMainCategorySlug(selectedMainCategory), - qfRoundSlug: isArchivedQF ? router.query.slug : null, - }, - }); + if (user?.id) { + variables.connectedWalletUserId = Number(user.id); + } - const dataProjects = res.data?.allProjects?.projects; - const count = res.data?.allProjects?.totalCount; - - // Calculate the total number of pages - const totalPages = Math.ceil( - count / (projectsData.projects.length || 1), - ); - - setProjectsData(prevState => ({ - projects: [...prevState.projects, ...dataProjects], - totalPages: totalPages, - totalCount: count, - })); - - return { - data: dataProjects, - previousCursor: currentPage - 1, - nextCursor: currentPage + 1, - }; - }, - [ + return await fetchProjects( + pageParam, + variables, contextVariables, isArchivedQF, - projects.length, - projectsData.projects.length, - router.query.slug, selectedMainCategory, - user?.id, - ], - ); + router.query.slug, + ); + }; + // Use the useInfiniteQuery hook with the new v5 API const { data, error, fetchNextPage, + hasNextPage, isError, isFetching, isFetchingNextPage, - } = useInfiniteQuery({ + } = useInfiniteQuery({ queryKey: [ 'projects', contextVariables, isArchivedQF, selectedMainCategory, ], - queryFn: ({ pageParam = 0 }: QueryFunctionContext) => - fetchProjects(pageParam), - getNextPageParam: lastPage => { - return lastPage.nextCursor; - }, + queryFn: fetchProjectsPage, + getNextPageParam: lastPage => lastPage.nextCursor, + getPreviousPageParam: firstPage => firstPage.previousCursor, initialPageParam: 0, + // placeholderData: keepPreviousData, + placeholderData: { + pageParams: [0], + pages: [{ data: projects, totalCount: _totalCount }], + }, }); - // Function that triggers when you scroll down - infinite loading + // Function to load more data when scrolling const loadMore = useCallback(() => { - if (projectsData.totalCount > (data?.pages?.length || 0)) { + if (hasNextPage) { fetchNextPage(); } - }, [data?.pages.length, fetchNextPage, projectsData.totalCount]); + }, [fetchNextPage, hasNextPage]); const handleCreateButton = () => { if (isUserRegistered(user)) { @@ -187,32 +127,28 @@ const ProjectsIndex = (props: IProjectsView) => { } }; - // Check if there any active QF const onProjectsPageOrActiveQFPage = !isQF || (isQF && activeQFRound); - /* - * This function will be called when the observed elements intersect with the viewport. - * Observed element is last project on the list that trigger another fetch projects to load. - */ + // 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]); @@ -223,7 +159,9 @@ const ProjectsIndex = (props: IProjectsView) => { !selectedMainCategory && !isArchivedQF ) { - setIsNotFound(true); + isInfiniteScrolling.current = false; + } else { + isInfiniteScrolling.current = true; } }, [selectedMainCategory, mainCategories.length, isArchivedQF]); @@ -232,33 +170,46 @@ const ProjectsIndex = (props: IProjectsView) => { localStorage.setItem('lastProjectClicked', slug); }; - // Handle last clicked project, if it exist scroll to that position + // Scroll to last clicked project useEffect(() => { if (!isFetching && !isFetchingNextPage) { const lastProjectClicked = localStorage.getItem('lastProjectClicked'); if (lastProjectClicked) { - window.scrollTo({ - top: document.getElementById(lastProjectClicked)?.offsetTop, - behavior: 'smooth', - }); + const element = document.getElementById(lastProjectClicked); + if (element) { + window.scrollTo({ + top: element.offsetTop, + behavior: 'smooth', + }); + } localStorage.removeItem('lastProjectClicked'); } } }, [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 ; - // Handle fetching errors from React Query - if (isError) { - showToastError(error); - captureException(error, { - tags: { - section: 'fetchAllProjects', - }, - }); - } + const totalCount = data?.pages[data.pages.length - 1].totalCount || 0; + console.log('data', totalCount, data); return ( <> @@ -294,11 +245,11 @@ const ProjectsIndex = (props: IProjectsView) => { )} {onProjectsPageOrActiveQFPage && ( - + )} {isFetchingNextPage && } - {projectsData.projects.length > 0 ? ( + {data?.pages.some(page => page.data.length > 0) ? ( {isQF ? ( @@ -306,7 +257,7 @@ const ProjectsIndex = (props: IProjectsView) => { ) : ( )} - {data?.pages.map((page, pageIndex) => ( + {data.pages.map((page, pageIndex) => ( {page.data.map((project, idx) => (
{ } > @@ -333,39 +283,35 @@ const ProjectsIndex = (props: IProjectsView) => { ) : ( )} - {projectsData.totalCount > projectsData.projects.length && ( -
+ {hasNextPage &&
} + {!isFetching && !isFetchingNextPage && hasNextPage && ( + <> + fetchNextPage()} + label={ + isFetchingNextPage + ? '' + : formatMessage({ + id: 'component.button.load_more', + }) + } + icon={ + isFetchingNextPage && ( + +
+ + ) + } + /> + + )} - {!isFetching && - !isFetchingNextPage && - projectsData.totalPages > (data?.pages?.length || 0) && ( - <> - -
- - ) - } - /> - - - )} ); From d83daf9637fd63e699aba7c69a43f18ef9233dd1 Mon Sep 17 00:00:00 2001 From: Cherik Date: Mon, 7 Oct 2024 19:51:23 +0330 Subject: [PATCH 4/4] add LAST_PROJECT_CLICKED --- src/components/views/projects/ProjectsIndex.tsx | 7 ++++--- src/components/views/projects/constants.ts | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 src/components/views/projects/constants.ts diff --git a/src/components/views/projects/ProjectsIndex.tsx b/src/components/views/projects/ProjectsIndex.tsx index 21b2a291ef..e7490e312f 100644 --- a/src/components/views/projects/ProjectsIndex.tsx +++ b/src/components/views/projects/ProjectsIndex.tsx @@ -39,6 +39,7 @@ import { DefaultQFBanner } from '@/components/DefaultQFBanner'; import NotAvailable from '@/components/NotAvailable'; import { fetchProjects, IQueries } from './services'; import { IProject } from '@/apollo/types/types'; +import { LAST_PROJECT_CLICKED } from './constants'; export interface IProjectsView { projects: IProject[]; @@ -167,14 +168,14 @@ const ProjectsIndex = (props: IProjectsView) => { // Save last clicked project const handleProjectClick = (slug: string) => { - localStorage.setItem('lastProjectClicked', slug); + sessionStorage.setItem(LAST_PROJECT_CLICKED, slug); }; // Scroll to last clicked project useEffect(() => { if (!isFetching && !isFetchingNextPage) { const lastProjectClicked = - localStorage.getItem('lastProjectClicked'); + sessionStorage.getItem(LAST_PROJECT_CLICKED); if (lastProjectClicked) { const element = document.getElementById(lastProjectClicked); if (element) { @@ -183,7 +184,7 @@ const ProjectsIndex = (props: IProjectsView) => { behavior: 'smooth', }); } - localStorage.removeItem('lastProjectClicked'); + sessionStorage.removeItem(LAST_PROJECT_CLICKED); } } }, [isFetching, 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';