Skip to content

Commit

Permalink
OCT-1942 Projects search: client (#461)
Browse files Browse the repository at this point in the history
  • Loading branch information
aziolek authored Oct 4, 2024
2 parents e53152d + 54f6195 commit 39b9d2b
Show file tree
Hide file tree
Showing 35 changed files with 685 additions and 104 deletions.
17 changes: 17 additions & 0 deletions client/src/api/calls/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,20 @@ export type Projects = {
export async function apiGetProjects(epoch: number): Promise<Projects> {
return apiService.get(`${env.serverEndpoint}projects/epoch/${epoch}`).then(({ data }) => data);
}

export type ProjectsSearchResults = {
projectsDetails: {
address: string;
epoch: string;
name: string;
}[];
};

export async function apiGetProjectsSearch(
epochs: string,
searchPhrases: string,
): Promise<ProjectsSearchResults> {
return apiService
.get(`${env.serverEndpoint}projects/details?epochs=${epochs}&searchPhrases=${searchPhrases}`)
.then(({ data }) => data);
}
21 changes: 19 additions & 2 deletions client/src/api/calls/userAllocations.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -10,8 +10,25 @@ export type Response = {
isManuallyEdited: boolean | null;
};

export async function apiGetUserAllocations(address: string, epoch: number): Promise<Response> {
export async function apiGetUserAllocations(
address: string,
epoch: number,
): Promise<GetUserAllocationsResponse> {
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<AllocationsPerProjectResponse> {
return apiService
.get(`${env.serverEndpoint}allocations/project/${projectAddress}/epoch/${epoch}`)
.then(({ data }) => data);
}
3 changes: 3 additions & 0 deletions client/src/api/queryKeys/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const ROOTS: Root = {
projectsEpoch: 'projectsEpoch',
projectsIpfsResults: 'projectsIpfsResults',
rewardsRate: 'rewardsRate',
searchResultsDetails: 'searchResultsDetails',
upcomingBudget: 'upcomingBudget',
uqScore: 'uqScore',
userAllocationNonce: 'userAllocationNonce',
Expand Down Expand Up @@ -68,6 +69,8 @@ export const QUERY_KEYS: QueryKeys = {
projectsMetadataAccumulateds: ['projectsMetadataAccumulateds'],
projectsMetadataPerEpoches: ['projectsMetadataPerEpoches'],
rewardsRate: epochNumber => [ROOTS.rewardsRate, epochNumber.toString()],
searchResults: ['searchResults'],
searchResultsDetails: (address, epoch) => [ROOTS.searchResultsDetails, address, epoch.toString()],
syncStatus: ['syncStatus'],
totalAddresses: ['totalAddresses'],
totalWithdrawals: ['totalWithdrawals'],
Expand Down
6 changes: 6 additions & 0 deletions client/src/api/queryKeys/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type Root = {
projectsEpoch: 'projectsEpoch';
projectsIpfsResults: 'projectsIpfsResults';
rewardsRate: 'rewardsRate';
searchResultsDetails: 'searchResultsDetails';
upcomingBudget: 'upcomingBudget';
uqScore: 'uqScore';
userAllocationNonce: 'userAllocationNonce';
Expand Down Expand Up @@ -69,6 +70,11 @@ export type QueryKeys = {
projectsMetadataAccumulateds: ['projectsMetadataAccumulateds'];
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'];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,3 @@
.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;
Expand Down
66 changes: 4 additions & 62 deletions client/src/components/Projects/ProjectsList/ProjectsList.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -33,61 +30,7 @@ const ProjectsList: FC<ProjectsListProps> = ({

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 (
<div
Expand Down Expand Up @@ -121,7 +64,6 @@ const ProjectsList: FC<ProjectsListProps> = ({
? projectsIpfsWithRewardsSorted.map((projectIpfsWithRewards, index) => (
<ProjectsListItem
key={projectIpfsWithRewards.address}
className={styles.element}
dataTest={
epoch
? `ProjectsView__ProjectsListItem--archive--${index}`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -26,6 +27,7 @@ const ProjectsListItem: FC<ProjectsListItemProps> = ({
dataTest,
epoch,
projectIpfsWithRewards,
searchResultsLabel,
}) => {
const { ipfsGateways } = env;
const { address, isLoadingError, profileImageSmall, name, introDescription } =
Expand Down Expand Up @@ -99,13 +101,20 @@ const ProjectsListItem: FC<ProjectsListItemProps> = ({
) : (
<Fragment>
<div className={styles.header}>
<Img
className={styles.imageProfile}
dataTest={
epoch ? 'ProjectsListItem__imageProfile--archive' : 'ProjectsListItem__imageProfile'
}
sources={ipfsGateways.split(',').map(element => `${element}${profileImageSmall}`)}
/>
<div className={styles.imageProfileWrapper}>
<Img
className={styles.imageProfile}
dataTest={
epoch
? 'ProjectsListItem__imageProfile--archive'
: 'ProjectsListItem__imageProfile'
}
sources={ipfsGateways.split(',').map(element => `${element}${profileImageSmall}`)}
/>
{searchResultsLabel && (
<TinyLabel className={styles.tinyLabel} text={searchResultsLabel} />
)}
</div>
{isAddToAllocateButtonVisible && (
<ButtonAddToAllocate
className={styles.button}
Expand Down
1 change: 1 addition & 0 deletions client/src/components/Projects/ProjectsListItem/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export default interface ProjectsListItemProps {
dataTest?: string;
epoch?: number;
projectIpfsWithRewards: ProjectIpfsWithRewards;
searchResultsLabel?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
$elementMargin: 1.6rem;

.noSearchResults {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
color: $color-octant-grey5;
margin: 1.6rem 0;

.image {
width: 28rem;
margin-bottom: 3.2rem;
}
}

.list {
display: flex;
flex-wrap: wrap;
width: 100%;
justify-content: space-between;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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';

import styles from './ProjectsSearchResults.module.scss';
import ProjectsSearchResultsProps from './types';

const ProjectsSearchResults: FC<ProjectsSearchResultsProps> = ({
orderOption,
projectsIpfsWithRewardsAndEpochs,
isLoading,
}) => {
const { t } = useTranslation('translation', {
keyPrefix: 'components.dedicated.projectsSearchResults',
});

const { data: currentEpoch } = useCurrentEpoch();
const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen();

const projectsIpfsWithRewardsSorted = useSortedProjects(
projectsIpfsWithRewardsAndEpochs,
orderOption,
);

return (
<div className={styles.list}>
{projectsIpfsWithRewardsAndEpochs.length === 0 && !isLoading && (
<div className={styles.noSearchResults}>
<Img
className={styles.image}
dataTest="ProjectsList__noSearchResults__Img"
src="images/swept.webp"
/>
{t('noSearchResults')}
</div>
)}
<Grid>
{projectsIpfsWithRewardsAndEpochs.length > 0 &&
!isLoading &&
projectsIpfsWithRewardsSorted.map((projectIpfsWithRewards, index) => (
<ProjectsListItem
key={`${projectIpfsWithRewards.address}--${projectIpfsWithRewards.epoch}`}
dataTest={
projectIpfsWithRewards.epoch
? `ProjectsView__ProjectsListItem--archive--${index}`
: `ProjectsView__ProjectsListItem--${index}`
}
epoch={projectIpfsWithRewards.epoch}
projectIpfsWithRewards={projectIpfsWithRewards}
searchResultsLabel={
isDecisionWindowOpen && currentEpoch === projectIpfsWithRewards.epoch
? ''
: t('searchResultsLabel', { epochNumber: projectIpfsWithRewards.epoch })
}
/>
))}
{projectsIpfsWithRewardsAndEpochs.length === 0 &&
isLoading &&
[...Array(5).keys()].map((_, index) => (
// eslint-disable-next-line react/no-array-index-key
<ProjectsListSkeletonItem key={index} className={styles.element} />
))}
</Grid>
</div>
);
};

export default memo(ProjectsSearchResults);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// eslint-disable-next-line no-restricted-exports
export { default } from './ProjectsSearchResults';
8 changes: 8 additions & 0 deletions client/src/components/Projects/ProjectsSearchResults/types.ts
Original file line number Diff line number Diff line change
@@ -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[];
}
Loading

0 comments on commit 39b9d2b

Please sign in to comment.