From 3ab8c49fcf1c2e48c3148fcdb49365396b8df6ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Wed, 2 Oct 2024 15:03:23 +0200 Subject: [PATCH 1/8] feat: initial prototype --- client/src/api/calls/projects.ts | 17 ++++ client/src/api/queryKeys/index.ts | 1 + client/src/api/queryKeys/types.ts | 1 + .../components/shared/ViewTitle/ViewTitle.tsx | 8 +- .../src/components/shared/ViewTitle/types.ts | 6 ++ .../src/hooks/queries/useSearchedProjects.ts | 28 +++++++ client/src/locales/en/translation.json | 1 + client/src/utils/regExp.test.ts | 17 ++++ client/src/utils/regExp.ts | 7 +- client/src/views/HomeView/HomeView.tsx | 7 +- .../ProjectsView/ProjectsView.module.scss | 5 ++ .../src/views/ProjectsView/ProjectsView.tsx | 78 ++++++++++++++++++- client/src/views/ProjectsView/types.ts | 5 ++ 13 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 client/src/components/shared/ViewTitle/types.ts create mode 100644 client/src/hooks/queries/useSearchedProjects.ts diff --git a/client/src/api/calls/projects.ts b/client/src/api/calls/projects.ts index 509973bb43..a2f33077e3 100644 --- a/client/src/api/calls/projects.ts +++ b/client/src/api/calls/projects.ts @@ -30,3 +30,20 @@ export type Projects = { export async function apiGetProjects(epoch: number): Promise { return apiService.get(`${env.serverEndpoint}projects/epoch/${epoch}`).then(({ data }) => data); } + +export type ProjectsDetails = { + projectsDetails: { + address: string; + epoch: string; + name: string; + }[]; +}; + +export async function apiGetProjectsDetails( + epochs: string, + searchPhrases: string, +): Promise { + return apiService + .get(`${env.serverEndpoint}projects/details?epochs=${epochs}&searchPhrases=${searchPhrases}`) + .then(({ data }) => data); +} diff --git a/client/src/api/queryKeys/index.ts b/client/src/api/queryKeys/index.ts index 7d98e87161..b832d4713b 100644 --- a/client/src/api/queryKeys/index.ts +++ b/client/src/api/queryKeys/index.ts @@ -68,6 +68,7 @@ export const QUERY_KEYS: QueryKeys = { projectsMetadataAccumulateds: ['projectsMetadataAccumulateds'], projectsMetadataPerEpoches: ['projectsMetadataPerEpoches'], rewardsRate: epochNumber => [ROOTS.rewardsRate, epochNumber.toString()], + searchResults: ['searchResults'], syncStatus: ['syncStatus'], totalAddresses: ['totalAddresses'], totalWithdrawals: ['totalWithdrawals'], diff --git a/client/src/api/queryKeys/types.ts b/client/src/api/queryKeys/types.ts index 4c8e64998b..590b6de5d2 100644 --- a/client/src/api/queryKeys/types.ts +++ b/client/src/api/queryKeys/types.ts @@ -69,6 +69,7 @@ export type QueryKeys = { projectsMetadataAccumulateds: ['projectsMetadataAccumulateds']; projectsMetadataPerEpoches: ['projectsMetadataPerEpoches']; rewardsRate: (epochNumber: number) => [Root['rewardsRate'], string]; + searchResults: ['searchResults']; syncStatus: ['syncStatus']; totalAddresses: ['totalAddresses']; totalWithdrawals: ['totalWithdrawals']; diff --git a/client/src/components/shared/ViewTitle/ViewTitle.tsx b/client/src/components/shared/ViewTitle/ViewTitle.tsx index 7bc8e025eb..f50120e6b7 100644 --- a/client/src/components/shared/ViewTitle/ViewTitle.tsx +++ b/client/src/components/shared/ViewTitle/ViewTitle.tsx @@ -1,9 +1,11 @@ -import React, { FC, ReactNode } from 'react'; +import cx from 'classnames'; +import React, { FC } from 'react'; +import ViewTitleProps from './types'; import styles from './ViewTitle.module.scss'; -const ViewTitle: FC<{ children: ReactNode }> = ({ children }) => { - return
{children}
; +const ViewTitle: FC = ({ children, className }) => { + return
{children}
; }; export default ViewTitle; diff --git a/client/src/components/shared/ViewTitle/types.ts b/client/src/components/shared/ViewTitle/types.ts new file mode 100644 index 0000000000..f82be25eeb --- /dev/null +++ b/client/src/components/shared/ViewTitle/types.ts @@ -0,0 +1,6 @@ +import { ReactNode } from 'react'; + +export default interface ViewTitleProps { + children: ReactNode; + className?: string; +} diff --git a/client/src/hooks/queries/useSearchedProjects.ts b/client/src/hooks/queries/useSearchedProjects.ts new file mode 100644 index 0000000000..4e3dd909ce --- /dev/null +++ b/client/src/hooks/queries/useSearchedProjects.ts @@ -0,0 +1,28 @@ +import { UseQueryResult, useQuery } from '@tanstack/react-query'; + +import { apiGetProjectsDetails, ProjectsDetails as ApiProjectsDetails } from 'api/calls/projects'; +import { QUERY_KEYS } from 'api/queryKeys'; +import { ProjectsDetailsSearchParameters } from 'views/ProjectsView/types'; + +type ProjectsDetails = ApiProjectsDetails['projectsDetails']; + +export default function useSearchedProjects( + projectsDetailsSearchParameters: ProjectsDetailsSearchParameters | undefined, +): UseQueryResult { + return useQuery({ + enabled: + !!projectsDetailsSearchParameters && + ((projectsDetailsSearchParameters.epochs && + projectsDetailsSearchParameters.epochs.length > 0) || + (projectsDetailsSearchParameters.searchPhrases && + projectsDetailsSearchParameters.searchPhrases?.length > 0)), + queryFn: () => + apiGetProjectsDetails( + projectsDetailsSearchParameters!.epochs!.map(String).toString(), + projectsDetailsSearchParameters!.searchPhrases!.join(), + ), + // No point in strigifying params, they will just flood the memory. + queryKey: QUERY_KEYS.searchResults, + select: data => data.projectsDetails, + }); +} diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 68bf6f2538..0d5be0030e 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -466,6 +466,7 @@ "share": "Share" }, "projects": { + "viewTitle": "Browse Epoch {{epochNumber}} projects", "projectsTimelineWidget": { "applicationsOpen": "Applications open", "projectUpdatesClose": "Project updates close", diff --git a/client/src/utils/regExp.test.ts b/client/src/utils/regExp.test.ts index df27046187..f29d889573 100644 --- a/client/src/utils/regExp.test.ts +++ b/client/src/utils/regExp.test.ts @@ -7,6 +7,7 @@ import { groupingNumbersUpTo3, numbersOnly, percentageOnly, + ethAddress, } from './regExp'; const regExpTestCases = [ @@ -217,6 +218,22 @@ const regExpTestCases = [ { expectedValue: true, test: '1000000,0' }, ], }, + { + name: 'ethAddress', + regExp: ethAddress, + testCases: [ + { expectedValue: true, test: '0xb794f5ea0ba39494ce839613fffba74279579268' }, + { expectedValue: false, test: '0' }, + { expectedValue: false, test: '0,0' }, + { expectedValue: false, test: 'abc' }, + { expectedValue: false, test: 'Abc' }, + { expectedValue: false, test: ' ' }, + { expectedValue: false, test: '' }, + { expectedValue: false, test: '.' }, + { expectedValue: false, test: '-1' }, + { expectedValue: false, test: '0aw0d98a0D(*W)C)(AK' }, + ], + }, ]; describe('regExp', () => { diff --git a/client/src/utils/regExp.ts b/client/src/utils/regExp.ts index edaedecd59..345142c1d5 100644 --- a/client/src/utils/regExp.ts +++ b/client/src/utils/regExp.ts @@ -1,8 +1,9 @@ export const floatNumberWithUpTo2DecimalPlaces = /^\d+\.?(\d{1,2})?$|^\d+$/; -export const floatNumberWithUpTo18DecimalPlaces = /^\d+\.?(\d{1,18})?$|^\d+$/; export const floatNumberWithUpTo9DecimalPlaces = /^\d+\.?(\d{1,9})?$|^\d+$/; +export const floatNumberWithUpTo18DecimalPlaces = /^\d+\.?(\d{1,18})?$|^\d+$/; + export const numbersOnly = /^[0-9]*$/; export const percentageOnly = /(^100$)|(^([1-9]([0-9])?|0)$)/; @@ -12,3 +13,7 @@ export const dotAndZeroes = /\.?0+$/; export const comma = /,/; export const groupingNumbersUpTo3 = /\B(?=(\d{3})+(?!\d))/g; + +export const ethAddress = /0x[a-fA-F0-9]{40}/g; + +export const testRegexp = /(?:^|\s)(?:epoch|Epoch|e|E)(?: ?)([0-9-]+)+/g; diff --git a/client/src/views/HomeView/HomeView.tsx b/client/src/views/HomeView/HomeView.tsx index fb7e68b8f5..1bb9fbc4ae 100644 --- a/client/src/views/HomeView/HomeView.tsx +++ b/client/src/views/HomeView/HomeView.tsx @@ -1,4 +1,3 @@ -import _first from 'lodash/first'; import React, { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; @@ -6,14 +5,18 @@ import HomeGrid from 'components/Home/HomeGrid/HomeGrid'; import HomeRewards from 'components/Home/HomeRewards/HomeRewards'; import ViewTitle from 'components/shared/ViewTitle/ViewTitle'; import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; +import useIsDecisionWindowOpen from 'hooks/queries/useIsDecisionWindowOpen'; const HomeView = (): ReactElement => { const { t } = useTranslation('translation', { keyPrefix: 'views.home' }); const { data: currentEpoch } = useCurrentEpoch(); + const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen(); return ( <> - {t('title', { epoch: currentEpoch! - 1 })} + + {t('title', { epoch: isDecisionWindowOpen ? currentEpoch! - 1 : currentEpoch })} + diff --git a/client/src/views/ProjectsView/ProjectsView.module.scss b/client/src/views/ProjectsView/ProjectsView.module.scss index b910421f48..c6a8d694e3 100644 --- a/client/src/views/ProjectsView/ProjectsView.module.scss +++ b/client/src/views/ProjectsView/ProjectsView.module.scss @@ -1,5 +1,10 @@ $elementMargin: 1.6rem; +.viewTitle { + border-bottom: 0.1rem solid $color-octant-grey1; + margin-bottom: 2.4rem; +} + .tip { margin-bottom: $elementMargin; diff --git a/client/src/views/ProjectsView/ProjectsView.tsx b/client/src/views/ProjectsView/ProjectsView.tsx index b1808cf92f..2e3b989af0 100644 --- a/client/src/views/ProjectsView/ProjectsView.tsx +++ b/client/src/views/ProjectsView/ProjectsView.tsx @@ -1,3 +1,5 @@ +import debounce from 'lodash/debounce'; +import range from 'lodash/range'; import React, { ReactElement, useState, @@ -5,11 +7,13 @@ import React, { useLayoutEffect, useEffect, ChangeEvent, + useCallback, } from 'react'; import { useTranslation } from 'react-i18next'; import InfiniteScroll from 'react-infinite-scroller'; import ProjectsList from 'components/Projects/ProjectsList'; +import ViewTitle from 'components/shared/ViewTitle/ViewTitle'; import InputSelect from 'components/ui/InputSelect'; import InputText from 'components/ui/InputText'; import Loader from 'components/ui/Loader'; @@ -21,10 +25,12 @@ import { import useAreCurrentEpochsProjectsHiddenOutsideAllocationWindow from 'hooks/helpers/useAreCurrentEpochsProjectsHiddenOutsideAllocationWindow'; import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; import useIsDecisionWindowOpen from 'hooks/queries/useIsDecisionWindowOpen'; +import useSearchedProjects from 'hooks/queries/useSearchedProjects'; import { magnifyingGlass } from 'svg/misc'; +import { ethAddress as ethAddressRegExp, testRegexp } from 'utils/regExp'; import styles from './ProjectsView.module.scss'; -import { OrderOption } from './types'; +import { OrderOption, ProjectsDetailsSearchParameters } from './types'; import { ORDER_OPTIONS } from './utils'; const ProjectsView = (): ReactElement => { @@ -35,6 +41,9 @@ const ProjectsView = (): ReactElement => { const { data: currentEpoch } = useCurrentEpoch(); const [searchQuery, setSearchQuery] = useState(''); + const [projectsDetailsSearchParameters, setProjectsDetailsSearchParameters] = useState< + ProjectsDetailsSearchParameters | undefined + >(undefined); const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen({ refetchOnMount: true, @@ -56,8 +65,70 @@ const ProjectsView = (): ReactElement => { return projectsLoadedArchivedEpochsNumber ?? 0; }); + const { data: searchedProjects, refetch: refetchSearchedProjects } = useSearchedProjects( + projectsDetailsSearchParameters, + ); + + useEffect(() => { + // Refetch is not required when no data already fetched. + if (!searchedProjects || searchedProjects.length === 0) { + return; + } + refetchSearchedProjects(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + searchedProjects, + projectsDetailsSearchParameters?.epochs, + projectsDetailsSearchParameters?.searchPhrases, + ]); + + const setProjectsDetailsSearchParametersWrapper = (query: string) => { + const epochNumbersMatched = [...query.matchAll(testRegexp)]; + + const epochNumbers = epochNumbersMatched + .map(match => match[1]) + .reduce((acc, curr) => { + if (curr.includes('-')) { + const borderNumbersInRange = curr + .split('-') + .map(Number) + .sort((a, b) => a - b); + const numbersInRange = range(borderNumbersInRange[0], borderNumbersInRange[1] + 1); + return [...acc, ...numbersInRange]; + } + return [...acc, Number(curr)]; + }, [] as number[]); + + const ethAddresses = query.match(ethAddressRegExp); + + let queryFiltered = query; + ethAddresses?.forEach(element => { + queryFiltered = queryFiltered.replace(element, ''); + }); + epochNumbersMatched?.forEach(element => { + queryFiltered = queryFiltered.replace(element[0], ''); + }); + queryFiltered = queryFiltered.trim(); + + setProjectsDetailsSearchParameters({ + epochs: + epochNumbers.length > 0 + ? epochNumbers + : Array.from({ length: currentEpoch! }, (_v, k) => k + 1), + searchPhrases: queryFiltered.split(' ').concat(ethAddresses || []) || [''], + }); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + const setProjectsDetailsSearchParametersWrapperDebounced = useCallback( + debounce(query => setProjectsDetailsSearchParametersWrapper(query), 1000, { trailing: true }), + [], + ); + const onChangeSearchQuery = (e: ChangeEvent): void => { - setSearchQuery(e.target.value); + const query = e.target.value; + setSearchQuery(query); + setProjectsDetailsSearchParametersWrapperDebounced(query); }; const lastArchivedEpochNumber = useMemo(() => { @@ -108,6 +179,9 @@ const ProjectsView = (): ReactElement => { return ( <> + + {t('viewTitle', { epochNumber: currentEpoch })} +
Date: Fri, 4 Oct 2024 11:11:04 +0200 Subject: [PATCH 2/8] feat: search working --- client/src/api/calls/projects.ts | 6 +- client/src/api/calls/userAllocations.ts | 21 +++- client/src/api/queryKeys/index.ts | 2 + client/src/api/queryKeys/types.ts | 5 + .../ProjectsListItem.module.scss | 22 +++- .../ProjectsListItem/ProjectsListItem.tsx | 23 ++-- .../Projects/ProjectsListItem/types.ts | 1 + .../ProjectsSearchResults.module.scss | 65 ++++++++++ .../ProjectsSearchResults.tsx | 114 ++++++++++++++++++ .../Projects/ProjectsSearchResults/index.tsx | 2 + .../Projects/ProjectsSearchResults/types.ts | 8 ++ .../LayoutTopBar/LayoutTopBar.module.scss | 12 ++ .../Layout/LayoutTopBar/LayoutTopBar.tsx | 8 +- .../LayoutTopBarCalendar.tsx | 4 +- .../ui/TinyLabel/TinyLabel.module.scss | 24 ++++ .../src/components/ui/TinyLabel/TinyLabel.tsx | 15 +++ client/src/components/ui/TinyLabel/index.ts | 2 + client/src/components/ui/TinyLabel/types.ts | 4 + .../helpers/useUserAllocationsAllEpochs.ts | 5 +- .../hooks/queries/useMatchedProjectRewards.ts | 1 + .../src/hooks/queries/useSearchedProjects.ts | 38 +++--- .../queries/useSearchedProjectsDetails.ts | 95 +++++++++++++++ .../src/hooks/queries/useUserAllocations.ts | 5 +- .../src/views/ProjectsView/ProjectsView.tsx | 44 ++++--- client/src/views/ProjectsView/types.ts | 2 +- 25 files changed, 477 insertions(+), 51 deletions(-) create mode 100644 client/src/components/Projects/ProjectsSearchResults/ProjectsSearchResults.module.scss create mode 100644 client/src/components/Projects/ProjectsSearchResults/ProjectsSearchResults.tsx create mode 100644 client/src/components/Projects/ProjectsSearchResults/index.tsx create mode 100644 client/src/components/Projects/ProjectsSearchResults/types.ts create mode 100644 client/src/components/ui/TinyLabel/TinyLabel.module.scss create mode 100644 client/src/components/ui/TinyLabel/TinyLabel.tsx create mode 100644 client/src/components/ui/TinyLabel/index.ts create mode 100644 client/src/components/ui/TinyLabel/types.ts create mode 100644 client/src/hooks/queries/useSearchedProjectsDetails.ts diff --git a/client/src/api/calls/projects.ts b/client/src/api/calls/projects.ts index a2f33077e3..b487f0a873 100644 --- a/client/src/api/calls/projects.ts +++ b/client/src/api/calls/projects.ts @@ -31,7 +31,7 @@ export async function apiGetProjects(epoch: number): Promise { return apiService.get(`${env.serverEndpoint}projects/epoch/${epoch}`).then(({ data }) => data); } -export type ProjectsDetails = { +export type ProjectsSearchResults = { projectsDetails: { address: string; epoch: string; @@ -39,10 +39,10 @@ export type ProjectsDetails = { }[]; }; -export async function apiGetProjectsDetails( +export async function apiGetProjectsSearch( epochs: string, searchPhrases: string, -): Promise { +): Promise { return apiService .get(`${env.serverEndpoint}projects/details?epochs=${epochs}&searchPhrases=${searchPhrases}`) .then(({ data }) => data); diff --git a/client/src/api/calls/userAllocations.ts b/client/src/api/calls/userAllocations.ts index bc33fc6912..7f463588fb 100644 --- a/client/src/api/calls/userAllocations.ts +++ b/client/src/api/calls/userAllocations.ts @@ -1,7 +1,7 @@ import env from 'env'; import apiService from 'services/apiService'; -export type Response = { +export type GetUserAllocationsResponse = { allocations: { address: string; // Funds allocated by user for the project in WEI @@ -10,8 +10,25 @@ export type Response = { isManuallyEdited: boolean | null; }; -export async function apiGetUserAllocations(address: string, epoch: number): Promise { +export async function apiGetUserAllocations( + address: string, + epoch: number, +): Promise { return apiService .get(`${env.serverEndpoint}allocations/user/${address}/epoch/${epoch}`) .then(({ data }) => data); } + +export type AllocationsPerProjectResponse = { + address: string; + amount: string; +}[]; + +export async function apiGetAllocationsPerProject( + projectAddress: string, + epoch: number, +): Promise { + return apiService + .get(`${env.serverEndpoint}allocations/project/${projectAddress}/epoch/${epoch}`) + .then(({ data }) => data); +} diff --git a/client/src/api/queryKeys/index.ts b/client/src/api/queryKeys/index.ts index b832d4713b..b43595ea00 100644 --- a/client/src/api/queryKeys/index.ts +++ b/client/src/api/queryKeys/index.ts @@ -22,6 +22,7 @@ export const ROOTS: Root = { projectsEpoch: 'projectsEpoch', projectsIpfsResults: 'projectsIpfsResults', rewardsRate: 'rewardsRate', + searchResultsDetails: 'searchResultsDetails', upcomingBudget: 'upcomingBudget', uqScore: 'uqScore', userAllocationNonce: 'userAllocationNonce', @@ -69,6 +70,7 @@ export const QUERY_KEYS: QueryKeys = { projectsMetadataPerEpoches: ['projectsMetadataPerEpoches'], rewardsRate: epochNumber => [ROOTS.rewardsRate, epochNumber.toString()], searchResults: ['searchResults'], + searchResultsDetails: (address, epoch) => [ROOTS.searchResultsDetails, address, epoch.toString()], syncStatus: ['syncStatus'], totalAddresses: ['totalAddresses'], totalWithdrawals: ['totalWithdrawals'], diff --git a/client/src/api/queryKeys/types.ts b/client/src/api/queryKeys/types.ts index 590b6de5d2..f5b0ed8d42 100644 --- a/client/src/api/queryKeys/types.ts +++ b/client/src/api/queryKeys/types.ts @@ -22,6 +22,7 @@ export type Root = { projectsEpoch: 'projectsEpoch'; projectsIpfsResults: 'projectsIpfsResults'; rewardsRate: 'rewardsRate'; + searchResultsDetails: 'searchResultsDetails'; upcomingBudget: 'upcomingBudget'; uqScore: 'uqScore'; userAllocationNonce: 'userAllocationNonce'; @@ -70,6 +71,10 @@ export type QueryKeys = { projectsMetadataPerEpoches: ['projectsMetadataPerEpoches']; rewardsRate: (epochNumber: number) => [Root['rewardsRate'], string]; searchResults: ['searchResults']; + searchResultsDetails: ( + address: string, + epoch: number, + ) => [Root['searchResultsDetails'], string, string]; syncStatus: ['syncStatus']; totalAddresses: ['totalAddresses']; totalWithdrawals: ['totalWithdrawals']; diff --git a/client/src/components/Projects/ProjectsListItem/ProjectsListItem.module.scss b/client/src/components/Projects/ProjectsListItem/ProjectsListItem.module.scss index b2d91ab761..105f16bf44 100644 --- a/client/src/components/Projects/ProjectsListItem/ProjectsListItem.module.scss +++ b/client/src/components/Projects/ProjectsListItem/ProjectsListItem.module.scss @@ -34,12 +34,26 @@ right: 0.8rem; } - .imageProfile { - border-radius: 50%; - width: 4rem; - height: 4rem; + .imageProfileWrapper { + position: relative; + + .imageProfile { + border-radius: 50%; + width: 4rem; + height: 4rem; + } + + .tinyLabel { + position: absolute; + top: 0; + right: 0; + white-space: nowrap; + background: $color-white; + text-transform: none; + } } + .body { padding: 0 $projectItemPadding; text-align: left; diff --git a/client/src/components/Projects/ProjectsListItem/ProjectsListItem.tsx b/client/src/components/Projects/ProjectsListItem/ProjectsListItem.tsx index 0431234d4b..e2b1652765 100644 --- a/client/src/components/Projects/ProjectsListItem/ProjectsListItem.tsx +++ b/client/src/components/Projects/ProjectsListItem/ProjectsListItem.tsx @@ -8,6 +8,7 @@ import RewardsWithoutThreshold from 'components/shared/RewardsWithoutThreshold'; import RewardsWithThreshold from 'components/shared/RewardsWithThreshold'; import Description from 'components/ui/Description'; import Img from 'components/ui/Img'; +import TinyLabel from 'components/ui/TinyLabel'; import { WINDOW_PROJECTS_SCROLL_Y } from 'constants/window'; import env from 'env'; import useIdsInAllocation from 'hooks/helpers/useIdsInAllocation'; @@ -26,6 +27,7 @@ const ProjectsListItem: FC = ({ dataTest, epoch, projectIpfsWithRewards, + searchResultsLabel, }) => { const { ipfsGateways } = env; const { address, isLoadingError, profileImageSmall, name, introDescription } = @@ -99,13 +101,20 @@ const ProjectsListItem: FC = ({ ) : (
- `${element}${profileImageSmall}`)} - /> +
+ `${element}${profileImageSmall}`)} + /> + {searchResultsLabel && ( + + )} +
{isAddToAllocateButtonVisible && ( = ({ + orderOption, + projectsIpfsWithRewardsAndEpochs, + isLoading, +}) => { + const { data: currentEpoch } = useCurrentEpoch(); + const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen(); + + const projectsIpfsWithRewardsSorted = + useMemo((): ProjectsSearchResultsProps['projectsIpfsWithRewardsAndEpochs'] => { + switch (orderOption) { + case 'randomized': { + const projectsAddressesRandomizedOrder = JSON.parse( + localStorage.getItem(PROJECTS_ADDRESSES_RANDOMIZED_ORDER)!, + ) as ProjectsAddressesRandomizedOrder; + + const { addressesRandomizedOrder } = projectsAddressesRandomizedOrder; + + return projectsIpfsWithRewardsAndEpochs.sort((a, b) => { + return ( + addressesRandomizedOrder.indexOf(a.address) - + addressesRandomizedOrder.indexOf(b.address) + ); + }); + } + case 'alphabeticalAscending': { + const projectIpfsWithRewardsFiltered = projectsIpfsWithRewardsAndEpochs.filter( + element => element.name !== undefined, + ); + return projectIpfsWithRewardsFiltered.sort((a, b) => a.name!.localeCompare(b.name!)); + } + case 'alphabeticalDescending': { + const projectIpfsWithRewardsFiltered = projectsIpfsWithRewardsAndEpochs.filter( + element => element.name !== undefined, + ); + return projectIpfsWithRewardsFiltered.sort((a, b) => b.name!.localeCompare(a.name!)); + } + case 'donorsAscending': { + return projectsIpfsWithRewardsAndEpochs.sort( + (a, b) => a.numberOfDonors - b.numberOfDonors, + ); + } + case 'donorsDescending': { + return projectsIpfsWithRewardsAndEpochs.sort( + (a, b) => b.numberOfDonors - a.numberOfDonors, + ); + } + case 'totalsAscending': { + const projectIpfsWithRewardsFiltered = projectsIpfsWithRewardsAndEpochs.filter( + element => element.totalValueOfAllocations !== undefined, + ); + return projectIpfsWithRewardsFiltered.sort((a, b) => + Number(a.totalValueOfAllocations! - b.totalValueOfAllocations!), + ); + } + case 'totalsDescending': { + const projectIpfsWithRewardsFiltered = projectsIpfsWithRewardsAndEpochs.filter( + element => element.totalValueOfAllocations !== undefined, + ); + return projectIpfsWithRewardsFiltered.sort((a, b) => + Number(b.totalValueOfAllocations! - a.totalValueOfAllocations!), + ); + } + default: { + return projectsIpfsWithRewardsAndEpochs; + } + } + }, [projectsIpfsWithRewardsAndEpochs, orderOption]); + + return ( +
+ + {projectsIpfsWithRewardsAndEpochs.length > 0 && !isLoading + ? projectsIpfsWithRewardsSorted.map((projectIpfsWithRewards, index) => ( + + )) + : [...Array(5).keys()].map((_, index) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} + +
+ ); +}; + +export default memo(ProjectsSearchResults); diff --git a/client/src/components/Projects/ProjectsSearchResults/index.tsx b/client/src/components/Projects/ProjectsSearchResults/index.tsx new file mode 100644 index 0000000000..de0a42181c --- /dev/null +++ b/client/src/components/Projects/ProjectsSearchResults/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './ProjectsSearchResults'; diff --git a/client/src/components/Projects/ProjectsSearchResults/types.ts b/client/src/components/Projects/ProjectsSearchResults/types.ts new file mode 100644 index 0000000000..ad449f839a --- /dev/null +++ b/client/src/components/Projects/ProjectsSearchResults/types.ts @@ -0,0 +1,8 @@ +import { ProjectsIpfsWithRewardsAndEpochs } from 'hooks/queries/useSearchedProjectsDetails'; +import { OrderOption } from 'views/ProjectsView/types'; + +export default interface ProjectsSearchResultsProps { + isLoading: boolean; + orderOption: OrderOption; + projectsIpfsWithRewardsAndEpochs: ProjectsIpfsWithRewardsAndEpochs[]; +} diff --git a/client/src/components/shared/Layout/LayoutTopBar/LayoutTopBar.module.scss b/client/src/components/shared/Layout/LayoutTopBar/LayoutTopBar.module.scss index bef46570ca..1dec830491 100644 --- a/client/src/components/shared/Layout/LayoutTopBar/LayoutTopBar.module.scss +++ b/client/src/components/shared/Layout/LayoutTopBar/LayoutTopBar.module.scss @@ -23,10 +23,22 @@ } } + .logoWrapper { + position: relative; + } + .octantLogo { cursor: pointer; } + .testnetIndicator { + position: absolute; + top: -1rem; + right: -0.2rem; + text-transform: uppercase; + letter-spacing: 0.1rem; + } + .links { display: flex; margin-left: 2.4rem; diff --git a/client/src/components/shared/Layout/LayoutTopBar/LayoutTopBar.tsx b/client/src/components/shared/Layout/LayoutTopBar/LayoutTopBar.tsx index f282dbdbc6..bc9fb6c20a 100644 --- a/client/src/components/shared/Layout/LayoutTopBar/LayoutTopBar.tsx +++ b/client/src/components/shared/Layout/LayoutTopBar/LayoutTopBar.tsx @@ -11,6 +11,8 @@ import LayoutTopBarCalendar from 'components/shared/Layout/LayoutTopBarCalendar' import Button from 'components/ui/Button'; import Drawer from 'components/ui/Drawer'; import Svg from 'components/ui/Svg'; +import TinyLabel from 'components/ui/TinyLabel'; +import networkConfig from 'constants/networkConfig'; import useIsProjectAdminMode from 'hooks/helpers/useIsProjectAdminMode'; import useMediaQuery from 'hooks/helpers/useMediaQuery'; import useNavigationTabs from 'hooks/helpers/useNavigationTabs'; @@ -127,7 +129,11 @@ const LayoutTopBar: FC = ({ className }) => { return (
- +
+ + {networkConfig.isTestnet || + (true && )} +
{isDesktop && (
{tabs.map(({ label, to, isActive, isDisabled }, index) => ( diff --git a/client/src/components/shared/Layout/LayoutTopBarCalendar/LayoutTopBarCalendar.tsx b/client/src/components/shared/Layout/LayoutTopBarCalendar/LayoutTopBarCalendar.tsx index 1c139e6c75..62f40c1c1c 100644 --- a/client/src/components/shared/Layout/LayoutTopBarCalendar/LayoutTopBarCalendar.tsx +++ b/client/src/components/shared/Layout/LayoutTopBarCalendar/LayoutTopBarCalendar.tsx @@ -1,6 +1,6 @@ import cx from 'classnames'; import { motion, AnimatePresence } from 'framer-motion'; -import React, { ReactNode, useMemo, useState } from 'react'; +import React, { ReactElement, useMemo, useState } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; @@ -14,7 +14,7 @@ import { calendar } from 'svg/misc'; import styles from './LayoutTopBarCalendar.module.scss'; -const LayoutTopBarCalendar = (): ReactNode => { +const LayoutTopBarCalendar = (): ReactElement => { const { t } = useTranslation('translation', { keyPrefix: 'layout.topBar' }); const { isMobile } = useMediaQuery(); const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen(); diff --git a/client/src/components/ui/TinyLabel/TinyLabel.module.scss b/client/src/components/ui/TinyLabel/TinyLabel.module.scss new file mode 100644 index 0000000000..cd95e76e8a --- /dev/null +++ b/client/src/components/ui/TinyLabel/TinyLabel.module.scss @@ -0,0 +1,24 @@ +.root { + display: flex; + align-items: center; + justify-content: center; + transform: translate(75%, 0); + background: $color-octant-grey3; + height: 1.6rem; + box-sizing: content-box; + border-radius: 1.2rem; + padding: 0.4rem 0.5rem; + font-size: $font-size-10; + font-weight: $font-weight-bold; +} + +.text { + display: flex; + align-items: center; + color: $color-white; + background: $color-octant-orange; + border-radius: 0.8rem; + height: 1.6rem; + padding: 0 0.8rem; + user-select: none; +} diff --git a/client/src/components/ui/TinyLabel/TinyLabel.tsx b/client/src/components/ui/TinyLabel/TinyLabel.tsx new file mode 100644 index 0000000000..e715193650 --- /dev/null +++ b/client/src/components/ui/TinyLabel/TinyLabel.tsx @@ -0,0 +1,15 @@ +import cx from 'classnames'; +import React, { FC } from 'react'; + +import styles from './TinyLabel.module.scss'; +import TinyLabelProps from './types'; + +const TinyLabel: FC = ({ className, text }) => { + return ( +
+
{text}
+
+ ); +}; + +export default TinyLabel; diff --git a/client/src/components/ui/TinyLabel/index.ts b/client/src/components/ui/TinyLabel/index.ts new file mode 100644 index 0000000000..3739733c96 --- /dev/null +++ b/client/src/components/ui/TinyLabel/index.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './TinyLabel'; diff --git a/client/src/components/ui/TinyLabel/types.ts b/client/src/components/ui/TinyLabel/types.ts new file mode 100644 index 0000000000..2789cbdcea --- /dev/null +++ b/client/src/components/ui/TinyLabel/types.ts @@ -0,0 +1,4 @@ +export default interface TinyLabelProps { + className?: string; + text: string; +} diff --git a/client/src/hooks/helpers/useUserAllocationsAllEpochs.ts b/client/src/hooks/helpers/useUserAllocationsAllEpochs.ts index c9da289bff..3ad4c7dcb1 100644 --- a/client/src/hooks/helpers/useUserAllocationsAllEpochs.ts +++ b/client/src/hooks/helpers/useUserAllocationsAllEpochs.ts @@ -1,7 +1,10 @@ import { useQueries, UseQueryResult } from '@tanstack/react-query'; import { useAccount } from 'wagmi'; -import { apiGetUserAllocations, Response as ApiResponse } from 'api/calls/userAllocations'; +import { + apiGetUserAllocations, + GetUserAllocationsResponse as ApiResponse, +} from 'api/calls/userAllocations'; import { QUERY_KEYS } from 'api/queryKeys'; import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; import { UserAllocationElement } from 'hooks/queries/useUserAllocations'; diff --git a/client/src/hooks/queries/useMatchedProjectRewards.ts b/client/src/hooks/queries/useMatchedProjectRewards.ts index 17c09a401a..5c2cdcd11e 100644 --- a/client/src/hooks/queries/useMatchedProjectRewards.ts +++ b/client/src/hooks/queries/useMatchedProjectRewards.ts @@ -24,6 +24,7 @@ export type ProjectRewards = { }; function parseResponse(response: Response): ProjectRewards[] { + // TODO unify with totalValueOfAllocations. const totalDonations = response?.rewards.reduce( (acc, { allocated, matched }) => acc + parseUnitsBigInt(allocated, 'wei') + parseUnitsBigInt(matched, 'wei'), diff --git a/client/src/hooks/queries/useSearchedProjects.ts b/client/src/hooks/queries/useSearchedProjects.ts index 4e3dd909ce..369f0c7eea 100644 --- a/client/src/hooks/queries/useSearchedProjects.ts +++ b/client/src/hooks/queries/useSearchedProjects.ts @@ -1,28 +1,38 @@ import { UseQueryResult, useQuery } from '@tanstack/react-query'; -import { apiGetProjectsDetails, ProjectsDetails as ApiProjectsDetails } from 'api/calls/projects'; +import { + apiGetProjectsSearch, + // ProjectsSearchResults as ApiProjectsSearchResults, +} from 'api/calls/projects'; import { QUERY_KEYS } from 'api/queryKeys'; -import { ProjectsDetailsSearchParameters } from 'views/ProjectsView/types'; +import { ProjectsSearchParameters } from 'views/ProjectsView/types'; -type ProjectsDetails = ApiProjectsDetails['projectsDetails']; +export type ProjectsSearchResults = { + address: string; + epoch: number; + name: string; +}[]; export default function useSearchedProjects( - projectsDetailsSearchParameters: ProjectsDetailsSearchParameters | undefined, -): UseQueryResult { + projectsSearchParameters: ProjectsSearchParameters | undefined, +): UseQueryResult { return useQuery({ enabled: - !!projectsDetailsSearchParameters && - ((projectsDetailsSearchParameters.epochs && - projectsDetailsSearchParameters.epochs.length > 0) || - (projectsDetailsSearchParameters.searchPhrases && - projectsDetailsSearchParameters.searchPhrases?.length > 0)), + !!projectsSearchParameters && + ((projectsSearchParameters.epochs && projectsSearchParameters.epochs.length > 0) || + (projectsSearchParameters.searchPhrases && + projectsSearchParameters.searchPhrases?.length > 0)), queryFn: () => - apiGetProjectsDetails( - projectsDetailsSearchParameters!.epochs!.map(String).toString(), - projectsDetailsSearchParameters!.searchPhrases!.join(), + apiGetProjectsSearch( + projectsSearchParameters!.epochs!.map(String).toString(), + projectsSearchParameters!.searchPhrases!.join(), ), // No point in strigifying params, they will just flood the memory. queryKey: QUERY_KEYS.searchResults, - select: data => data.projectsDetails, + select: data => + data.projectsDetails.map(element => ({ + ...element, + epoch: Number(element.epoch), + })), }); } diff --git a/client/src/hooks/queries/useSearchedProjectsDetails.ts b/client/src/hooks/queries/useSearchedProjectsDetails.ts new file mode 100644 index 0000000000..5d7d1a9d6b --- /dev/null +++ b/client/src/hooks/queries/useSearchedProjectsDetails.ts @@ -0,0 +1,95 @@ +import { useQueries } from '@tanstack/react-query'; +import { useCallback } from 'react'; + +import { apiGetProjectIpfsData, apiGetProjects } from 'api/calls/projects'; +import { + apiGetEstimatedMatchedProjectRewards, + apiGetMatchedProjectRewards, +} from 'api/calls/rewards'; +import { apiGetAllocationsPerProject } from 'api/calls/userAllocations'; +import { QUERY_KEYS } from 'api/queryKeys'; +import { parseUnitsBigInt } from 'utils/parseUnitsBigInt'; + +import useCurrentEpoch from './useCurrentEpoch'; +import { ProjectIpfsWithRewards } from './useProjectsIpfsWithRewards'; +import { ProjectsSearchResults } from './useSearchedProjects'; + +export interface ProjectsIpfsWithRewardsAndEpochs extends ProjectIpfsWithRewards { + address: string; + epoch: number; +} + +export interface ProjectsDetails { + data: ProjectsIpfsWithRewardsAndEpochs[]; + isFetching: boolean; + refetch: () => void; +} + +export default function useSearchedProjectsDetails( + projectsSearchResults: ProjectsSearchResults | undefined, +): ProjectsDetails { + const { data: currentEpoch } = useCurrentEpoch(); + + const queries = useQueries({ + queries: (projectsSearchResults || []).map(projectsSearchResult => ({ + queryFn: async () => { + const projectsEpoch = await apiGetProjects(Number(projectsSearchResult.epoch)); + return Promise.all([ + apiGetProjectIpfsData(`${projectsEpoch?.projectsCid}/${projectsSearchResult.address}`), + projectsSearchResult.epoch === currentEpoch + ? apiGetEstimatedMatchedProjectRewards() + : apiGetMatchedProjectRewards(projectsSearchResult.epoch), + apiGetAllocationsPerProject(projectsSearchResult.address, projectsSearchResult.epoch), + projectsSearchResult.epoch, + projectsSearchResult.address, + ]); + }, + queryKey: QUERY_KEYS.searchResultsDetails( + projectsSearchResult.address, + projectsSearchResult.epoch, + ), + retry: false, + })), + }); + + // Trick from https://github.com/TanStack/query/discussions/3364#discussioncomment-2287991. + const refetch = useCallback(() => { + queries.forEach(result => result.refetch()); + }, [queries]); + + const isFetchingQueries = queries.some(({ isFetching }) => isFetching); + + if (isFetchingQueries) { + return { + data: [], + isFetching: isFetchingQueries, + refetch, + }; + } + + return { + data: queries.map(({ data }) => { + const rewards = data && data[1] ? data[1].rewards : []; + const address = data && data[4] ? data[4] : undefined; + const rewardsOfProject = rewards.find(element => element.address === address); + const rewardsOfProjectMatched = rewardsOfProject + ? parseUnitsBigInt(rewardsOfProject.matched, 'wei') + : BigInt(0); + const rewardsOfProjectAllocated = rewardsOfProject + ? parseUnitsBigInt(rewardsOfProject.allocated, 'wei') + : BigInt(0); + + return { + address: data && data[4] ? data[4] : undefined, + donations: rewardsOfProjectAllocated, + epoch: data && data[3] ? data[3] : undefined, + matchedRewards: rewardsOfProjectMatched, + numberOfDonors: data && data[2] ? data[2].length : 0, + totalValueOfAllocations: rewardsOfProjectMatched + rewardsOfProjectAllocated, + ...(data && data[0] ? data[0] : {}), + }; + }), + isFetching: false, + refetch, + }; +} diff --git a/client/src/hooks/queries/useUserAllocations.ts b/client/src/hooks/queries/useUserAllocations.ts index 0d354ab2cd..328f2c169c 100644 --- a/client/src/hooks/queries/useUserAllocations.ts +++ b/client/src/hooks/queries/useUserAllocations.ts @@ -1,7 +1,10 @@ import { UseQueryOptions, UseQueryResult, useQuery } from '@tanstack/react-query'; import { useAccount } from 'wagmi'; -import { apiGetUserAllocations, Response as ApiResponse } from 'api/calls/userAllocations'; +import { + apiGetUserAllocations, + GetUserAllocationsResponse as ApiResponse, +} from 'api/calls/userAllocations'; import { QUERY_KEYS } from 'api/queryKeys'; import { parseUnitsBigInt } from 'utils/parseUnitsBigInt'; diff --git a/client/src/views/ProjectsView/ProjectsView.tsx b/client/src/views/ProjectsView/ProjectsView.tsx index 2e3b989af0..9340f2116f 100644 --- a/client/src/views/ProjectsView/ProjectsView.tsx +++ b/client/src/views/ProjectsView/ProjectsView.tsx @@ -13,6 +13,7 @@ import { useTranslation } from 'react-i18next'; import InfiniteScroll from 'react-infinite-scroller'; import ProjectsList from 'components/Projects/ProjectsList'; +import ProjectsSearchResults from 'components/Projects/ProjectsSearchResults'; import ViewTitle from 'components/shared/ViewTitle/ViewTitle'; import InputSelect from 'components/ui/InputSelect'; import InputText from 'components/ui/InputText'; @@ -26,11 +27,12 @@ import useAreCurrentEpochsProjectsHiddenOutsideAllocationWindow from 'hooks/help import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; import useIsDecisionWindowOpen from 'hooks/queries/useIsDecisionWindowOpen'; import useSearchedProjects from 'hooks/queries/useSearchedProjects'; +import useSearchedProjectsDetails from 'hooks/queries/useSearchedProjectsDetails'; import { magnifyingGlass } from 'svg/misc'; import { ethAddress as ethAddressRegExp, testRegexp } from 'utils/regExp'; import styles from './ProjectsView.module.scss'; -import { OrderOption, ProjectsDetailsSearchParameters } from './types'; +import { OrderOption, ProjectsSearchParameters } from './types'; import { ORDER_OPTIONS } from './utils'; const ProjectsView = (): ReactElement => { @@ -41,8 +43,8 @@ const ProjectsView = (): ReactElement => { const { data: currentEpoch } = useCurrentEpoch(); const [searchQuery, setSearchQuery] = useState(''); - const [projectsDetailsSearchParameters, setProjectsDetailsSearchParameters] = useState< - ProjectsDetailsSearchParameters | undefined + const [projectsSearchParameters, setProjectsSearchParameters] = useState< + ProjectsSearchParameters | undefined >(undefined); const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen({ @@ -65,22 +67,27 @@ const ProjectsView = (): ReactElement => { return projectsLoadedArchivedEpochsNumber ?? 0; }); - const { data: searchedProjects, refetch: refetchSearchedProjects } = useSearchedProjects( - projectsDetailsSearchParameters, - ); + const { + data: searchedProjects, + refetch: refetchSearchedProjects, + status: statusSearchedProjects, + isFetching: isFetchingSearchedProjects, + } = useSearchedProjects(projectsSearchParameters); + const { + data: searchedProjectsDetails, + refetch: refetchSearchedProjectsDetails, + isFetching: isFetchingSearchedProjectsDetails, + } = useSearchedProjectsDetails(searchedProjects); useEffect(() => { // Refetch is not required when no data already fetched. - if (!searchedProjects || searchedProjects.length === 0) { + if (statusSearchedProjects !== 'success') { return; } refetchSearchedProjects(); + refetchSearchedProjectsDetails(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - searchedProjects, - projectsDetailsSearchParameters?.epochs, - projectsDetailsSearchParameters?.searchPhrases, - ]); + }, [searchedProjects, projectsSearchParameters?.epochs, projectsSearchParameters?.searchPhrases]); const setProjectsDetailsSearchParametersWrapper = (query: string) => { const epochNumbersMatched = [...query.matchAll(testRegexp)]; @@ -110,7 +117,7 @@ const ProjectsView = (): ReactElement => { }); queryFiltered = queryFiltered.trim(); - setProjectsDetailsSearchParameters({ + setProjectsSearchParameters({ epochs: epochNumbers.length > 0 ? epochNumbers @@ -207,7 +214,7 @@ const ProjectsView = (): ReactElement => { variant="underselect" />
- {!areCurrentEpochsProjectsHiddenOutsideAllocationWindow && ( + {searchQuery === '' && !areCurrentEpochsProjectsHiddenOutsideAllocationWindow && ( { orderOption={orderOption} /> )} - {archivedEpochs.length > 0 && ( + {searchQuery !== '' && ( + + )} + {searchQuery === '' && archivedEpochs.length > 0 && ( Date: Fri, 4 Oct 2024 11:19:07 +0200 Subject: [PATCH 3/8] style: extract momoized sorting to useSortedProjects --- client/src/hooks/helpers/useIdsInAllocation/useSortedProjects.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 client/src/hooks/helpers/useIdsInAllocation/useSortedProjects.ts diff --git a/client/src/hooks/helpers/useIdsInAllocation/useSortedProjects.ts b/client/src/hooks/helpers/useIdsInAllocation/useSortedProjects.ts new file mode 100644 index 0000000000..e69de29bb2 From e9c92ea5ca9705b9244f446e2be5099ca1393a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Fri, 4 Oct 2024 11:20:08 +0200 Subject: [PATCH 4/8] style: extract momoized sorting to useSortedProjects --- .../Projects/ProjectsList/ProjectsList.tsx | 65 ++--------------- .../ProjectsSearchResults.tsx | 69 ++----------------- .../useIdsInAllocation/useSortedProjects.ts | 68 ++++++++++++++++++ 3 files changed, 78 insertions(+), 124 deletions(-) diff --git a/client/src/components/Projects/ProjectsList/ProjectsList.tsx b/client/src/components/Projects/ProjectsList/ProjectsList.tsx index 028b651ce9..eacfcf54d3 100644 --- a/client/src/components/Projects/ProjectsList/ProjectsList.tsx +++ b/client/src/components/Projects/ProjectsList/ProjectsList.tsx @@ -1,17 +1,14 @@ import cx from 'classnames'; -import React, { FC, memo, useMemo } from 'react'; +import React, { FC, memo } from 'react'; import { useTranslation } from 'react-i18next'; import ProjectsListItem from 'components/Projects/ProjectsListItem'; import ProjectsListSkeletonItem from 'components/Projects/ProjectsListSkeletonItem'; import Grid from 'components/shared/Grid'; -import { PROJECTS_ADDRESSES_RANDOMIZED_ORDER } from 'constants/localStorageKeys'; import useEpochDurationLabel from 'hooks/helpers/useEpochDurationLabel'; +import useSortedProjects from 'hooks/helpers/useIdsInAllocation/useSortedProjects'; import useProjectsEpoch from 'hooks/queries/useProjectsEpoch'; -import useProjectsIpfsWithRewards, { - ProjectIpfsWithRewards, -} from 'hooks/queries/useProjectsIpfsWithRewards'; -import { ProjectsAddressesRandomizedOrder } from 'types/localStorage'; +import useProjectsIpfsWithRewards from 'hooks/queries/useProjectsIpfsWithRewards'; import styles from './ProjectsList.module.scss'; import ProjectsListProps from './types'; @@ -33,61 +30,7 @@ const ProjectsList: FC = ({ const isLoading = isFetchingProjectsEpoch || isFetchingProjectsWithRewards; - const projectsIpfsWithRewardsSorted = useMemo((): ProjectIpfsWithRewards[] => { - switch (orderOption) { - case 'randomized': { - const projectsAddressesRandomizedOrder = JSON.parse( - localStorage.getItem(PROJECTS_ADDRESSES_RANDOMIZED_ORDER)!, - ) as ProjectsAddressesRandomizedOrder; - - const { addressesRandomizedOrder } = projectsAddressesRandomizedOrder; - - return projectsIpfsWithRewards.sort((a, b) => { - return ( - addressesRandomizedOrder.indexOf(a.address) - - addressesRandomizedOrder.indexOf(b.address) - ); - }); - } - case 'alphabeticalAscending': { - const projectIpfsWithRewardsFiltered = projectsIpfsWithRewards.filter( - element => element.name !== undefined, - ); - return projectIpfsWithRewardsFiltered.sort((a, b) => a.name!.localeCompare(b.name!)); - } - case 'alphabeticalDescending': { - const projectIpfsWithRewardsFiltered = projectsIpfsWithRewards.filter( - element => element.name !== undefined, - ); - return projectIpfsWithRewardsFiltered.sort((a, b) => b.name!.localeCompare(a.name!)); - } - case 'donorsAscending': { - return projectsIpfsWithRewards.sort((a, b) => a.numberOfDonors - b.numberOfDonors); - } - case 'donorsDescending': { - return projectsIpfsWithRewards.sort((a, b) => b.numberOfDonors - a.numberOfDonors); - } - case 'totalsAscending': { - const projectIpfsWithRewardsFiltered = projectsIpfsWithRewards.filter( - element => element.totalValueOfAllocations !== undefined, - ); - return projectIpfsWithRewardsFiltered.sort((a, b) => - Number(a.totalValueOfAllocations! - b.totalValueOfAllocations!), - ); - } - case 'totalsDescending': { - const projectIpfsWithRewardsFiltered = projectsIpfsWithRewards.filter( - element => element.totalValueOfAllocations !== undefined, - ); - return projectIpfsWithRewardsFiltered.sort((a, b) => - Number(b.totalValueOfAllocations! - a.totalValueOfAllocations!), - ); - } - default: { - return projectsIpfsWithRewards; - } - } - }, [projectsIpfsWithRewards, orderOption]); + const projectsIpfsWithRewardsSorted = useSortedProjects(projectsIpfsWithRewards, orderOption); return (
= ({ const { data: currentEpoch } = useCurrentEpoch(); const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen(); - const projectsIpfsWithRewardsSorted = - useMemo((): ProjectsSearchResultsProps['projectsIpfsWithRewardsAndEpochs'] => { - switch (orderOption) { - case 'randomized': { - const projectsAddressesRandomizedOrder = JSON.parse( - localStorage.getItem(PROJECTS_ADDRESSES_RANDOMIZED_ORDER)!, - ) as ProjectsAddressesRandomizedOrder; - - const { addressesRandomizedOrder } = projectsAddressesRandomizedOrder; - - return projectsIpfsWithRewardsAndEpochs.sort((a, b) => { - return ( - addressesRandomizedOrder.indexOf(a.address) - - addressesRandomizedOrder.indexOf(b.address) - ); - }); - } - case 'alphabeticalAscending': { - const projectIpfsWithRewardsFiltered = projectsIpfsWithRewardsAndEpochs.filter( - element => element.name !== undefined, - ); - return projectIpfsWithRewardsFiltered.sort((a, b) => a.name!.localeCompare(b.name!)); - } - case 'alphabeticalDescending': { - const projectIpfsWithRewardsFiltered = projectsIpfsWithRewardsAndEpochs.filter( - element => element.name !== undefined, - ); - return projectIpfsWithRewardsFiltered.sort((a, b) => b.name!.localeCompare(a.name!)); - } - case 'donorsAscending': { - return projectsIpfsWithRewardsAndEpochs.sort( - (a, b) => a.numberOfDonors - b.numberOfDonors, - ); - } - case 'donorsDescending': { - return projectsIpfsWithRewardsAndEpochs.sort( - (a, b) => b.numberOfDonors - a.numberOfDonors, - ); - } - case 'totalsAscending': { - const projectIpfsWithRewardsFiltered = projectsIpfsWithRewardsAndEpochs.filter( - element => element.totalValueOfAllocations !== undefined, - ); - return projectIpfsWithRewardsFiltered.sort((a, b) => - Number(a.totalValueOfAllocations! - b.totalValueOfAllocations!), - ); - } - case 'totalsDescending': { - const projectIpfsWithRewardsFiltered = projectsIpfsWithRewardsAndEpochs.filter( - element => element.totalValueOfAllocations !== undefined, - ); - return projectIpfsWithRewardsFiltered.sort((a, b) => - Number(b.totalValueOfAllocations! - a.totalValueOfAllocations!), - ); - } - default: { - return projectsIpfsWithRewardsAndEpochs; - } - } - }, [projectsIpfsWithRewardsAndEpochs, orderOption]); + const projectsIpfsWithRewardsSorted = useSortedProjects( + projectsIpfsWithRewardsAndEpochs, + orderOption, + ); return (
diff --git a/client/src/hooks/helpers/useIdsInAllocation/useSortedProjects.ts b/client/src/hooks/helpers/useIdsInAllocation/useSortedProjects.ts index e69de29bb2..aa8deadbaa 100644 --- a/client/src/hooks/helpers/useIdsInAllocation/useSortedProjects.ts +++ b/client/src/hooks/helpers/useIdsInAllocation/useSortedProjects.ts @@ -0,0 +1,68 @@ +import { useMemo } from 'react'; + +import { PROJECTS_ADDRESSES_RANDOMIZED_ORDER } from 'constants/localStorageKeys'; +import { ProjectIpfsWithRewards } from 'hooks/queries/useProjectsIpfsWithRewards'; +import { ProjectsIpfsWithRewardsAndEpochs } from 'hooks/queries/useSearchedProjectsDetails'; +import { ProjectsAddressesRandomizedOrder } from 'types/localStorage'; +import { OrderOption } from 'views/ProjectsView/types'; + +export default function useSortedProjects( + projects: ProjectIpfsWithRewards[] | ProjectsIpfsWithRewardsAndEpochs[], + orderOption: OrderOption, +): ProjectIpfsWithRewards[] | ProjectsIpfsWithRewardsAndEpochs[] { + return useMemo((): ProjectIpfsWithRewards[] | ProjectsIpfsWithRewardsAndEpochs[] => { + switch (orderOption) { + case 'randomized': { + const projectsAddressesRandomizedOrder = JSON.parse( + localStorage.getItem(PROJECTS_ADDRESSES_RANDOMIZED_ORDER)!, + ) as ProjectsAddressesRandomizedOrder; + + const { addressesRandomizedOrder } = projectsAddressesRandomizedOrder; + + return projects.sort((a, b) => { + return ( + addressesRandomizedOrder.indexOf(a.address) - + addressesRandomizedOrder.indexOf(b.address) + ); + }); + } + case 'alphabeticalAscending': { + const projectIpfsWithRewardsFiltered = projects.filter( + element => element.name !== undefined, + ); + return projectIpfsWithRewardsFiltered.sort((a, b) => a.name!.localeCompare(b.name!)); + } + case 'alphabeticalDescending': { + const projectIpfsWithRewardsFiltered = projects.filter( + element => element.name !== undefined, + ); + return projectIpfsWithRewardsFiltered.sort((a, b) => b.name!.localeCompare(a.name!)); + } + case 'donorsAscending': { + return projects.sort((a, b) => a.numberOfDonors - b.numberOfDonors); + } + case 'donorsDescending': { + return projects.sort((a, b) => b.numberOfDonors - a.numberOfDonors); + } + case 'totalsAscending': { + const projectIpfsWithRewardsFiltered = projects.filter( + element => element.totalValueOfAllocations !== undefined, + ); + return projectIpfsWithRewardsFiltered.sort((a, b) => + Number(a.totalValueOfAllocations! - b.totalValueOfAllocations!), + ); + } + case 'totalsDescending': { + const projectIpfsWithRewardsFiltered = projects.filter( + element => element.totalValueOfAllocations !== undefined, + ); + return projectIpfsWithRewardsFiltered.sort((a, b) => + Number(b.totalValueOfAllocations! - a.totalValueOfAllocations!), + ); + } + default: { + return projects; + } + } + }, [projects, orderOption]); +} From 8be761aec59199eeacde72748a7de0cc3a2a798c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Fri, 4 Oct 2024 11:21:19 +0200 Subject: [PATCH 5/8] fix: reverse testnet indicator visibility --- .../components/shared/Layout/LayoutTopBar/LayoutTopBar.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/components/shared/Layout/LayoutTopBar/LayoutTopBar.tsx b/client/src/components/shared/Layout/LayoutTopBar/LayoutTopBar.tsx index bc9fb6c20a..c8585a7736 100644 --- a/client/src/components/shared/Layout/LayoutTopBar/LayoutTopBar.tsx +++ b/client/src/components/shared/Layout/LayoutTopBar/LayoutTopBar.tsx @@ -131,8 +131,9 @@ const LayoutTopBar: FC = ({ className }) => {
- {networkConfig.isTestnet || - (true && )} + {networkConfig.isTestnet && ( + + )}
{isDesktop && (
From 6a52a6c79a86cbb2e82652c6e38a54d693b3f84d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Fri, 4 Oct 2024 12:04:42 +0200 Subject: [PATCH 6/8] style: code cleanup --- .../ProjectsList/ProjectsList.module.scss | 14 ---- .../ProjectsSearchResults.module.scss | 37 +---------- .../ProjectsSearchResults.tsx | 64 ++++++++++++------- .../src/components/ui/TinyLabel/TinyLabel.tsx | 12 ++-- .../hooks/queries/useMatchedProjectRewards.ts | 2 +- .../src/hooks/queries/useSearchedProjects.ts | 5 +- client/src/locales/en/translation.json | 4 ++ client/src/utils/regExp.test.ts | 56 ++++++++++++++++ client/src/utils/regExp.ts | 2 +- .../src/views/ProjectsView/ProjectsView.tsx | 9 +-- 10 files changed, 115 insertions(+), 90 deletions(-) diff --git a/client/src/components/Projects/ProjectsList/ProjectsList.module.scss b/client/src/components/Projects/ProjectsList/ProjectsList.module.scss index 878d424018..3cfd63fdbb 100644 --- a/client/src/components/Projects/ProjectsList/ProjectsList.module.scss +++ b/client/src/components/Projects/ProjectsList/ProjectsList.module.scss @@ -1,19 +1,5 @@ $elementMargin: 1.6rem; -.noSearchResults { - display: flex; - flex-direction: column; - align-items: center; - flex: 1; - color: $color-octant-grey5; - margin-bottom: 1.6rem; - - .image { - width: 28rem; - margin-bottom: 3.2rem; - } -} - .list { display: flex; flex-wrap: wrap; diff --git a/client/src/components/Projects/ProjectsSearchResults/ProjectsSearchResults.module.scss b/client/src/components/Projects/ProjectsSearchResults/ProjectsSearchResults.module.scss index 878d424018..a0e3eaae79 100644 --- a/client/src/components/Projects/ProjectsSearchResults/ProjectsSearchResults.module.scss +++ b/client/src/components/Projects/ProjectsSearchResults/ProjectsSearchResults.module.scss @@ -6,7 +6,7 @@ $elementMargin: 1.6rem; align-items: center; flex: 1; color: $color-octant-grey5; - margin-bottom: 1.6rem; + margin: 1.6rem 0; .image { width: 28rem; @@ -28,38 +28,3 @@ $elementMargin: 1.6rem; @include flexBasisGutter(2, $elementMargin); } } - -.epochArchive { - width: 100%; - background: $color-white; - height: 6.4rem; - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 2.4rem; - color: $color-octant-dark; - font-size: $font-size-16; - font-weight: $font-weight-bold; - border-radius: $border-radius-16; - margin: 0 0 1.6rem; - - .epochDurationLabel { - color: $color-octant-grey5; - font-size: $font-size-12; - font-weight: $font-weight-medium; - - &.isFetching { - @include skeleton(); - text-indent: 1000%; // moves text outside of view. - white-space: nowrap; - overflow: hidden; - } - } -} - -.divider { - width: 100%; - height: 0.1rem; - background-color: $color-octant-grey1; - margin: 0 0 1.6rem; -} diff --git a/client/src/components/Projects/ProjectsSearchResults/ProjectsSearchResults.tsx b/client/src/components/Projects/ProjectsSearchResults/ProjectsSearchResults.tsx index e319a37bf7..ed1c981edb 100644 --- a/client/src/components/Projects/ProjectsSearchResults/ProjectsSearchResults.tsx +++ b/client/src/components/Projects/ProjectsSearchResults/ProjectsSearchResults.tsx @@ -1,8 +1,10 @@ import React, { FC, memo } from 'react'; +import { useTranslation } from 'react-i18next'; import ProjectsListItem from 'components/Projects/ProjectsListItem'; import ProjectsListSkeletonItem from 'components/Projects/ProjectsListSkeletonItem'; import Grid from 'components/shared/Grid'; +import Img from 'components/ui/Img'; import useSortedProjects from 'hooks/helpers/useIdsInAllocation/useSortedProjects'; import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; import useIsDecisionWindowOpen from 'hooks/queries/useIsDecisionWindowOpen'; @@ -15,6 +17,10 @@ const ProjectsSearchResults: FC = ({ projectsIpfsWithRewardsAndEpochs, isLoading, }) => { + const { t } = useTranslation('translation', { + keyPrefix: 'components.dedicated.projectsSearchResults', + }); + const { data: currentEpoch } = useCurrentEpoch(); const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen(); @@ -25,30 +31,42 @@ const ProjectsSearchResults: FC = ({ return (
+ {projectsIpfsWithRewardsAndEpochs.length === 0 && !isLoading && ( +
+ + {t('noSearchResults')} +
+ )} - {projectsIpfsWithRewardsAndEpochs.length > 0 && !isLoading - ? projectsIpfsWithRewardsSorted.map((projectIpfsWithRewards, index) => ( - - )) - : [...Array(5).keys()].map((_, index) => ( - // eslint-disable-next-line react/no-array-index-key - - ))} + {projectsIpfsWithRewardsAndEpochs.length > 0 && + !isLoading && + projectsIpfsWithRewardsSorted.map((projectIpfsWithRewards, index) => ( + + ))} + {isLoading && + [...Array(5).keys()].map((_, index) => ( + // eslint-disable-next-line react/no-array-index-key + + ))}
); diff --git a/client/src/components/ui/TinyLabel/TinyLabel.tsx b/client/src/components/ui/TinyLabel/TinyLabel.tsx index e715193650..b396e94442 100644 --- a/client/src/components/ui/TinyLabel/TinyLabel.tsx +++ b/client/src/components/ui/TinyLabel/TinyLabel.tsx @@ -4,12 +4,10 @@ import React, { FC } from 'react'; import styles from './TinyLabel.module.scss'; import TinyLabelProps from './types'; -const TinyLabel: FC = ({ className, text }) => { - return ( -
-
{text}
-
- ); -}; +const TinyLabel: FC = ({ className, text }) => ( +
+
{text}
+
+); export default TinyLabel; diff --git a/client/src/hooks/queries/useMatchedProjectRewards.ts b/client/src/hooks/queries/useMatchedProjectRewards.ts index 5c2cdcd11e..935b8d6e74 100644 --- a/client/src/hooks/queries/useMatchedProjectRewards.ts +++ b/client/src/hooks/queries/useMatchedProjectRewards.ts @@ -24,7 +24,7 @@ export type ProjectRewards = { }; function parseResponse(response: Response): ProjectRewards[] { - // TODO unify with totalValueOfAllocations. + // TODO OCT-2023 unify with totalValueOfAllocations (same thing). const totalDonations = response?.rewards.reduce( (acc, { allocated, matched }) => acc + parseUnitsBigInt(allocated, 'wei') + parseUnitsBigInt(matched, 'wei'), diff --git a/client/src/hooks/queries/useSearchedProjects.ts b/client/src/hooks/queries/useSearchedProjects.ts index 369f0c7eea..0bac36b27b 100644 --- a/client/src/hooks/queries/useSearchedProjects.ts +++ b/client/src/hooks/queries/useSearchedProjects.ts @@ -1,9 +1,6 @@ import { UseQueryResult, useQuery } from '@tanstack/react-query'; -import { - apiGetProjectsSearch, - // ProjectsSearchResults as ApiProjectsSearchResults, -} from 'api/calls/projects'; +import { apiGetProjectsSearch } from 'api/calls/projects'; import { QUERY_KEYS } from 'api/queryKeys'; import { ProjectsSearchParameters } from 'views/ProjectsView/types'; diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 0d5be0030e..fc833b60b7 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -313,6 +313,10 @@ "projectsList": { "epochArchive": "Epoch {{epoch}} Archive" }, + "projectsSearchResults": { + "searchResultsLabel": "Epoch {{epochNumber}}", + "noSearchResults": "No luck, please try again" + }, "timeCounter": { "hours": "Hours", "minutes": "Minutes", diff --git a/client/src/utils/regExp.test.ts b/client/src/utils/regExp.test.ts index f29d889573..14769be7dc 100644 --- a/client/src/utils/regExp.test.ts +++ b/client/src/utils/regExp.test.ts @@ -8,6 +8,7 @@ import { numbersOnly, percentageOnly, ethAddress, + epochNumberGrabber, } from './regExp'; const regExpTestCases = [ @@ -223,6 +224,8 @@ const regExpTestCases = [ regExp: ethAddress, testCases: [ { expectedValue: true, test: '0xb794f5ea0ba39494ce839613fffba74279579268' }, + { expectedValue: false, test: 'xb794f5ea0ba39494ce839613fffba74279579268' }, + { expectedValue: false, test: '0xb794f5ea0ba39494ce839613fffba7427957926' }, { expectedValue: false, test: '0' }, { expectedValue: false, test: '0,0' }, { expectedValue: false, test: 'abc' }, @@ -247,4 +250,57 @@ describe('regExp', () => { } }); } + + describe('special test cases for epochNumberGrabber', () => { + it('', () => { + expect(JSON.stringify([...'e1'.matchAll(epochNumberGrabber)])).toBe( + JSON.stringify([['e1', '1']]), + ); + expect(JSON.stringify([...'e 1'.matchAll(epochNumberGrabber)])).toBe( + JSON.stringify([['e 1', '1']]), + ); + expect(JSON.stringify([...'e1-2'.matchAll(epochNumberGrabber)])).toBe( + JSON.stringify([['e1-2', '1-2']]), + ); + expect(JSON.stringify([...'e 1-2'.matchAll(epochNumberGrabber)])).toBe( + JSON.stringify([['e 1-2', '1-2']]), + ); + expect(JSON.stringify([...'E1'.matchAll(epochNumberGrabber)])).toBe( + JSON.stringify([['E1', '1']]), + ); + expect(JSON.stringify([...'E 1'.matchAll(epochNumberGrabber)])).toBe( + JSON.stringify([['E 1', '1']]), + ); + expect(JSON.stringify([...'E1-2'.matchAll(epochNumberGrabber)])).toBe( + JSON.stringify([['E1-2', '1-2']]), + ); + expect(JSON.stringify([...'E 1-2'.matchAll(epochNumberGrabber)])).toBe( + JSON.stringify([['E 1-2', '1-2']]), + ); + expect(JSON.stringify([...'Epoch1'.matchAll(epochNumberGrabber)])).toBe( + JSON.stringify([['Epoch1', '1']]), + ); + expect(JSON.stringify([...'Epoch 1'.matchAll(epochNumberGrabber)])).toBe( + JSON.stringify([['Epoch 1', '1']]), + ); + expect(JSON.stringify([...'Epoch1-2'.matchAll(epochNumberGrabber)])).toBe( + JSON.stringify([['Epoch1-2', '1-2']]), + ); + expect(JSON.stringify([...'Epoch 1-2'.matchAll(epochNumberGrabber)])).toBe( + JSON.stringify([['Epoch 1-2', '1-2']]), + ); + expect(JSON.stringify([...'epoch1'.matchAll(epochNumberGrabber)])).toBe( + JSON.stringify([['epoch1', '1']]), + ); + expect(JSON.stringify([...'epoch 1'.matchAll(epochNumberGrabber)])).toBe( + JSON.stringify([['epoch 1', '1']]), + ); + expect(JSON.stringify([...'epoch1-2'.matchAll(epochNumberGrabber)])).toBe( + JSON.stringify([['epoch1-2', '1-2']]), + ); + expect(JSON.stringify([...'epoch 1-2'.matchAll(epochNumberGrabber)])).toBe( + JSON.stringify([['epoch 1-2', '1-2']]), + ); + }); + }); }); diff --git a/client/src/utils/regExp.ts b/client/src/utils/regExp.ts index 345142c1d5..ade7518a35 100644 --- a/client/src/utils/regExp.ts +++ b/client/src/utils/regExp.ts @@ -16,4 +16,4 @@ export const groupingNumbersUpTo3 = /\B(?=(\d{3})+(?!\d))/g; export const ethAddress = /0x[a-fA-F0-9]{40}/g; -export const testRegexp = /(?:^|\s)(?:epoch|Epoch|e|E)(?: ?)([0-9-]+)+/g; +export const epochNumberGrabber = /(?:^|\s)(?:epoch|Epoch|e|E)(?: ?)([0-9-]+)+/g; diff --git a/client/src/views/ProjectsView/ProjectsView.tsx b/client/src/views/ProjectsView/ProjectsView.tsx index 9340f2116f..2cf3607c84 100644 --- a/client/src/views/ProjectsView/ProjectsView.tsx +++ b/client/src/views/ProjectsView/ProjectsView.tsx @@ -29,7 +29,7 @@ import useIsDecisionWindowOpen from 'hooks/queries/useIsDecisionWindowOpen'; import useSearchedProjects from 'hooks/queries/useSearchedProjects'; import useSearchedProjectsDetails from 'hooks/queries/useSearchedProjectsDetails'; import { magnifyingGlass } from 'svg/misc'; -import { ethAddress as ethAddressRegExp, testRegexp } from 'utils/regExp'; +import { ethAddress as ethAddressRegExp, epochNumberGrabber } from 'utils/regExp'; import styles from './ProjectsView.module.scss'; import { OrderOption, ProjectsSearchParameters } from './types'; @@ -84,13 +84,14 @@ const ProjectsView = (): ReactElement => { if (statusSearchedProjects !== 'success') { return; } - refetchSearchedProjects(); - refetchSearchedProjectsDetails(); + refetchSearchedProjects().then(() => { + refetchSearchedProjectsDetails(); + }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchedProjects, projectsSearchParameters?.epochs, projectsSearchParameters?.searchPhrases]); const setProjectsDetailsSearchParametersWrapper = (query: string) => { - const epochNumbersMatched = [...query.matchAll(testRegexp)]; + const epochNumbersMatched = [...query.matchAll(epochNumberGrabber)]; const epochNumbers = epochNumbersMatched .map(match => match[1]) From 6be5ddf5dd8e874a2c52e57fdaeef7b7091db43b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Fri, 4 Oct 2024 12:46:47 +0200 Subject: [PATCH 7/8] fix: remove unused CSS --- .../src/components/Projects/ProjectsList/ProjectsList.tsx | 1 - .../ProjectsSearchResults.module.scss | 8 -------- .../ProjectsSearchResults/ProjectsSearchResults.tsx | 1 - 3 files changed, 10 deletions(-) diff --git a/client/src/components/Projects/ProjectsList/ProjectsList.tsx b/client/src/components/Projects/ProjectsList/ProjectsList.tsx index eacfcf54d3..ed20c36b2f 100644 --- a/client/src/components/Projects/ProjectsList/ProjectsList.tsx +++ b/client/src/components/Projects/ProjectsList/ProjectsList.tsx @@ -64,7 +64,6 @@ const ProjectsList: FC = ({ ? projectsIpfsWithRewardsSorted.map((projectIpfsWithRewards, index) => ( = ({ projectsIpfsWithRewardsSorted.map((projectIpfsWithRewards, index) => ( Date: Fri, 4 Oct 2024 14:43:06 +0200 Subject: [PATCH 8/8] style: CR adjustments --- .../ProjectsList/ProjectsList.module.scss | 2 -- .../ProjectsSearchResults.tsx | 3 ++- .../queries/useSearchedProjectsDetails.ts | 12 +++++----- .../src/views/ProjectsView/ProjectsView.tsx | 23 +++++++++++++++++-- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/client/src/components/Projects/ProjectsList/ProjectsList.module.scss b/client/src/components/Projects/ProjectsList/ProjectsList.module.scss index 551b3d8f27..8101c16dd5 100644 --- a/client/src/components/Projects/ProjectsList/ProjectsList.module.scss +++ b/client/src/components/Projects/ProjectsList/ProjectsList.module.scss @@ -1,5 +1,3 @@ -$elementMargin: 1.6rem; - .list { display: flex; flex-wrap: wrap; diff --git a/client/src/components/Projects/ProjectsSearchResults/ProjectsSearchResults.tsx b/client/src/components/Projects/ProjectsSearchResults/ProjectsSearchResults.tsx index 282cc447d1..7a3b04a281 100644 --- a/client/src/components/Projects/ProjectsSearchResults/ProjectsSearchResults.tsx +++ b/client/src/components/Projects/ProjectsSearchResults/ProjectsSearchResults.tsx @@ -61,7 +61,8 @@ const ProjectsSearchResults: FC = ({ } /> ))} - {isLoading && + {projectsIpfsWithRewardsAndEpochs.length === 0 && + isLoading && [...Array(5).keys()].map((_, index) => ( // eslint-disable-next-line react/no-array-index-key diff --git a/client/src/hooks/queries/useSearchedProjectsDetails.ts b/client/src/hooks/queries/useSearchedProjectsDetails.ts index 5d7d1a9d6b..96b3208b80 100644 --- a/client/src/hooks/queries/useSearchedProjectsDetails.ts +++ b/client/src/hooks/queries/useSearchedProjectsDetails.ts @@ -69,8 +69,8 @@ export default function useSearchedProjectsDetails( return { data: queries.map(({ data }) => { - const rewards = data && data[1] ? data[1].rewards : []; - const address = data && data[4] ? data[4] : undefined; + const rewards = data?.[1]?.rewards ?? []; + const address = data?.[4]; const rewardsOfProject = rewards.find(element => element.address === address); const rewardsOfProjectMatched = rewardsOfProject ? parseUnitsBigInt(rewardsOfProject.matched, 'wei') @@ -80,13 +80,13 @@ export default function useSearchedProjectsDetails( : BigInt(0); return { - address: data && data[4] ? data[4] : undefined, + address, donations: rewardsOfProjectAllocated, - epoch: data && data[3] ? data[3] : undefined, + epoch: data?.[3], matchedRewards: rewardsOfProjectMatched, - numberOfDonors: data && data[2] ? data[2].length : 0, + numberOfDonors: data?.[2].length ?? 0, totalValueOfAllocations: rewardsOfProjectMatched + rewardsOfProjectAllocated, - ...(data && data[0] ? data[0] : {}), + ...(data?.[0] ?? {}), }; }), isFetching: false, diff --git a/client/src/views/ProjectsView/ProjectsView.tsx b/client/src/views/ProjectsView/ProjectsView.tsx index 2cf3607c84..707402ed49 100644 --- a/client/src/views/ProjectsView/ProjectsView.tsx +++ b/client/src/views/ProjectsView/ProjectsView.tsx @@ -43,6 +43,8 @@ const ProjectsView = (): ReactElement => { const { data: currentEpoch } = useCurrentEpoch(); const [searchQuery, setSearchQuery] = useState(''); + // Helper hook, because actual fetch is called after debounce. Until then, loading state. + const [isProjectsSearchInProgress, setIsProjectsSearchInProgress] = useState(false); const [projectsSearchParameters, setProjectsSearchParameters] = useState< ProjectsSearchParameters | undefined >(undefined); @@ -79,6 +81,13 @@ const ProjectsView = (): ReactElement => { isFetching: isFetchingSearchedProjectsDetails, } = useSearchedProjectsDetails(searchedProjects); + useEffect(() => { + if (isFetchingSearchedProjects || isFetchingSearchedProjectsDetails) { + return; + } + setIsProjectsSearchInProgress(false); + }, [isFetchingSearchedProjects, isFetchingSearchedProjectsDetails]); + useEffect(() => { // Refetch is not required when no data already fetched. if (statusSearchedProjects !== 'success') { @@ -137,6 +146,10 @@ const ProjectsView = (): ReactElement => { const query = e.target.value; setSearchQuery(query); setProjectsDetailsSearchParametersWrapperDebounced(query); + + if (query !== '') { + setIsProjectsSearchInProgress(true); + } }; const lastArchivedEpochNumber = useMemo(() => { @@ -188,7 +201,13 @@ const ProjectsView = (): ReactElement => { return ( <> - {t('viewTitle', { epochNumber: currentEpoch })} + {t('viewTitle', { + epochNumber: + isDecisionWindowOpen || + (!isDecisionWindowOpen && areCurrentEpochsProjectsHiddenOutsideAllocationWindow) + ? currentEpoch! - 1 + : currentEpoch, + })}
{ )} {searchQuery !== '' && (