Skip to content

Commit

Permalink
Merge pull request #48 from InvArch/yaki-dao-filters-011624
Browse files Browse the repository at this point in the history
Yaki-dao-filters-011624
  • Loading branch information
shibatales authored Jan 21, 2024
2 parents a846002 + 33ca5c6 commit 4f1d344
Show file tree
Hide file tree
Showing 16 changed files with 814 additions and 76 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"react-dom": "^18.2.0",
"react-hook-form": "^7.44.3",
"react-hot-toast": "^2.4.1",
"react-range": "^1.8.14",
"react-router-dom": "^6.12.0",
"tailwind-scrollbar": "^3.0.5",
"urql": "^4.0.3",
Expand Down
13 changes: 13 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/assets/filter-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
214 changes: 207 additions & 7 deletions src/components/DaoList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@ import useAccount from '../stores/account';
import { useQuery } from 'urql';
import { StakesInfo } from '../routes/claim';
import useModal, { modalName } from '../stores/modals';
import Input from './Input';
import FilterIcon from '../assets/filter-icon.svg';
import { CHOOSE_ONE, FilterStates, OrderByOption } from '../modals/DaoListFilters';
import { clearFiltersFromLocalStorage } from '../utils/filterStorage';

interface DaoListProps { mini: boolean; isOverview: boolean; }

const DaoList = (props: DaoListProps) => {
const { mini, isOverview } = props;
const api = useApi();
const initialCoresRef = useRef<StakingCore[]>([]);
const descriptionRef = useRef<HTMLDivElement | null>(null);
const projectCardRef = useRef<HTMLDivElement | null>(null);
const setOpenModal = useModal((state) => state.setOpenModal);
Expand All @@ -34,27 +39,161 @@ const DaoList = (props: DaoListProps) => {
const [totalUserStakedData, setTotalUserStakedData] = useState<TotalUserStakedData>({});
const [userStakedInfo, setUserStakedInfo] = useState<UserStakedInfoType[]
>([]);
const [searchTerm, setSearchTerm] = useState('');
const [debounceTimeout, setDebounceTimeout] = useState<NodeJS.Timeout | null>(null);
const [minStakeReward, setMinStakeReward] = useState<BigNumber>(new BigNumber(0));
const [activeFilterCount, setActiveFilterCount] = useState(0);

const [rewardsCoreClaimedQuery] = useQuery({
query: TotalRewardsCoreClaimedQuery,
variables: {},
pause: !selectedAccount,
});

const toggleViewMembers = (core: StakingCore, members: AnyJson[]) => {
const handleViewMembers = (core: StakingCore, members: AnyJson[]) => {
setOpenModal({
name: modalName.MEMBERS,
metadata: { ...core.metadata, members },
});
};

const toggleReadMore = (core: StakingCore) => {
const handleReadMore = (core: StakingCore) => {
setOpenModal({
name: modalName.READ_MORE,
metadata: core.metadata,
});
};

const handleFilters = () => {
setOpenModal({
name: modalName.FILTERS,
metadata: { updateFilters }
});
};

const filterStakingCores = useCallback((filters: FilterStates) => {
let cores = [...initialCoresRef.current];
let activeFilterCount = 0;

// Filter by total stakers
if (filters.totalStakersRange.minValue > 0 || filters.totalStakersRange.maxValue < 1000) {
activeFilterCount++;
cores = cores.filter(core => {
const coreInfo = coreEraStakeInfo.find(info => info.coreId === core.key);
const totalStakers = coreInfo ? coreInfo.numberOfStakers : 0;
return totalStakers >= filters.totalStakersRange.minValue && totalStakers <= filters.totalStakersRange.maxValue;
});
}

// Filter by total staked
if (filters.totalStakedRange.minValue > 0 || filters.totalStakedRange.maxValue < 99999) {
activeFilterCount++;
cores = cores.filter(core => {
const totalStaked = totalUserStakedData[core.key]?.dividedBy(1e12).toNumber() || 0;
if (filters.totalStakedRange.maxValue === 99999) {
return totalStaked >= filters.totalStakedRange.minValue;
} else {
return totalStaked >= filters.totalStakedRange.minValue && totalStaked <= filters.totalStakedRange.maxValue;
}
});
}

// Filter by min support met
if (filters.isMinSupportMet.isIndeterminate) {
activeFilterCount++;
cores = cores.filter(core => {
const coreInfo = coreEraStakeInfo.find(info => info.coreId === core.key);
const totalStaked = coreInfo ? new BigNumber(coreInfo.totalStaked) : new BigNumber(0);
return totalStaked.comparedTo(minStakeReward) < 0;
});
} else if (filters.isMinSupportMet.isChecked) {
activeFilterCount++;
cores = cores.filter(core => {
const coreInfo = coreEraStakeInfo.find(info => info.coreId === core.key);
const totalStaked = coreInfo ? new BigNumber(coreInfo.totalStaked) : new BigNumber(0);
return totalStaked.comparedTo(minStakeReward) >= 0;
});
}

// Filter by my staked DAOs
if (filters.isMyStakedDAOs.isIndeterminate) {
activeFilterCount++;
cores = cores.filter(core => {
const userStaked = totalUserStakedData[core.key] ?? new BigNumber(0);
return userStaked.isEqualTo(0);
});
} else if (filters.isMyStakedDAOs.isChecked) {
activeFilterCount++;
cores = cores.filter(core => {
const userStaked = totalUserStakedData[core.key] ?? new BigNumber(0);
return userStaked.isGreaterThan(0);
});
}

// Filter by OrderByOption
if (filters.orderBy !== CHOOSE_ONE) {
activeFilterCount++;
switch (filters.orderBy) {
case OrderByOption.NAME_ASCENDING:
cores = cores.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
break;
case OrderByOption.NAME_DESCENDING:
cores = cores.sort((a, b) => b.metadata.name.localeCompare(a.metadata.name));
break;
case OrderByOption.TOTAL_STAKED_HIGH:
cores = cores.sort((a, b) => {
const aCoreInfo = coreEraStakeInfo.find(info => info.coreId === a.key);
const bCoreInfo = coreEraStakeInfo.find(info => info.coreId === b.key);
const aTotalStaked = new BigNumber(aCoreInfo?.totalStaked ?? 0);
const bTotalStaked = new BigNumber(bCoreInfo?.totalStaked ?? 0);
return bTotalStaked.comparedTo(aTotalStaked);
});
break;
case OrderByOption.TOTAL_STAKED_LOW:
cores = cores.sort((a, b) => {
const aCoreInfo = coreEraStakeInfo.find(info => info.coreId === a.key);
const bCoreInfo = coreEraStakeInfo.find(info => info.coreId === b.key);
const aTotalStaked = new BigNumber(aCoreInfo?.totalStaked ?? 0);
const bTotalStaked = new BigNumber(bCoreInfo?.totalStaked ?? 0);
return aTotalStaked.comparedTo(bTotalStaked);
});
break;
case OrderByOption.SUPPORT_SHARE_HIGH:
cores = cores.sort((a, b) => {
const aCoreInfo = coreEraStakeInfo.find(info => info.coreId === a.key);
const aTotalStaked = aCoreInfo ? new BigNumber(aCoreInfo.totalStaked) : new BigNumber(0);
const aSupportShare = aTotalStaked.dividedBy(minStakeReward).multipliedBy(100);
const bCoreInfo = coreEraStakeInfo.find(info => info.coreId === b.key);
const bTotalStaked = bCoreInfo ? new BigNumber(bCoreInfo.totalStaked) : new BigNumber(0);
const bSupportShare = bTotalStaked.dividedBy(minStakeReward).multipliedBy(100);
return bSupportShare.minus(aSupportShare).toNumber();
});
break;
case OrderByOption.SUPPORT_SHARE_LOW:
cores = cores.sort((a, b) => {
const aCoreInfo = coreEraStakeInfo.find(info => info.coreId === a.key);
const aTotalStaked = aCoreInfo ? new BigNumber(aCoreInfo.totalStaked) : new BigNumber(0);
const aSupport = aTotalStaked.dividedBy(minStakeReward).multipliedBy(100);
const bCoreInfo = coreEraStakeInfo.find(info => info.coreId === b.key);
const bTotalStaked = bCoreInfo ? new BigNumber(bCoreInfo.totalStaked) : new BigNumber(0);
const bSupport = bTotalStaked.dividedBy(minStakeReward).multipliedBy(100);
return aSupport.minus(bSupport).toNumber();
});
break;
default:
console.log('Cannot sort by this option');
break;
}
}

setStakingCores(cores);
setActiveFilterCount(activeFilterCount);
}, [initialCoresRef, coreEraStakeInfo, totalUserStakedData, minStakeReward]);

const updateFilters = useCallback((filters: FilterStates) => {
filterStakingCores(filters);
}, [filterStakingCores]);

const handleViewDetails = (mini: boolean, children?: JSX.Element) => {
if (!mini || !children) return;
setOpenModal({
Expand All @@ -78,12 +217,39 @@ const DaoList = (props: DaoListProps) => {
});
};

const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value.toLowerCase();
setSearchTerm(value);

if (debounceTimeout) clearTimeout(debounceTimeout);

const newTimeout = setTimeout(() => {
if (value === '') {
setStakingCores(initialCoresRef.current);
} else {
const filteredCores = initialCoresRef.current.filter(core =>
core.metadata.name.toLowerCase().includes(value) ||
(core.metadata.description && core.metadata.description.toLowerCase().includes(value))
);
setStakingCores(filteredCores);
}
}, 300);

setDebounceTimeout(newTimeout);
};

const loadStakeRewardMinimum = useCallback(() => {
const minStakeReward = api.consts.ocifStaking.stakeThresholdForActiveCore.toPrimitive() as string;
setMinStakeReward(new BigNumber(minStakeReward));
}, [api]);

const loadCores = useCallback(async () => {
if (!selectedAccount) return;

const cores = await loadProjectCores(api);
if (cores) {
setStakingCores(cores);
initialCoresRef.current = cores; // Store the initial list in the ref
setStakingCores(cores); // Set the displayed cores to the initial list
}
}, [selectedAccount, api]);

Expand Down Expand Up @@ -158,10 +324,12 @@ const DaoList = (props: DaoListProps) => {
const initializeData = useCallback(async (selectedAccount: InjectedAccountWithMeta | null) => {
try {
if (selectedAccount) {
clearFiltersFromLocalStorage();
await loadAccountInfo();
await loadCores();
await loadStakingConstants();
await loadCoreEraStake();
loadStakeRewardMinimum();
}

} catch (error) {
Expand All @@ -170,7 +338,7 @@ const DaoList = (props: DaoListProps) => {
setLoading(false);
setDataLoaded(true);
}
}, [loadAccountInfo, loadCores, loadStakingConstants, loadCoreEraStake]);
}, [loadAccountInfo, loadCores, loadStakingConstants, loadCoreEraStake, loadStakeRewardMinimum]);

const setupSubscriptions = useCallback(async () => {
if (!selectedAccount) {
Expand Down Expand Up @@ -250,12 +418,15 @@ const DaoList = (props: DaoListProps) => {
await setupSubscriptions();
}
};

setup();

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedAccount, stakingCores]);

useEffect(() => {
initializeData(selectedAccount);

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedAccount]);

Expand All @@ -266,9 +437,12 @@ const DaoList = (props: DaoListProps) => {
useEffect(() => {
const loadDaos = async () => {
if (!selectedAccount) return;

const daos = await loadStakedDaos(stakingCores, selectedAccount.address, api);

setStakedDaos(daos);
};

if (!selectedAccount) return;
if (!stakingCores) return;

Expand All @@ -287,6 +461,18 @@ const DaoList = (props: DaoListProps) => {
setCoreIndexedRewards(uniqueCoreIndexedRewards);
}, [selectedAccount, stakingCores, rewardsCoreClaimedQuery.data]);

useEffect(() => {
return () => {
if (debounceTimeout) clearTimeout(debounceTimeout);
};
}, [debounceTimeout]);

useEffect(() => {
if (searchTerm === '') {
setStakingCores(initialCoresRef.current);
}
}, [searchTerm]);

const stakedCoresCount = useMemo(() => {
return isOverview
? stakingCores.filter(core =>
Expand All @@ -305,7 +491,21 @@ const DaoList = (props: DaoListProps) => {

return (
<div>
<h4 className="text-white text-md mb-2">{isOverview ? 'My Staked DAOs' : 'All Registered DAOs'} ({stakedCoresCount || '0'})</h4>
<div className='flex flex-col md:flex-row gap-2 md:gap-10 items-stretch md:items-center justify-between mb-4 mt-14 md:mt-0'>
<h4 className="text-white text-md">{isOverview ? 'My Staked DAOs' : 'All Registered DAOs'} ({stakedCoresCount || '0'})</h4>
<div className="bg-neutral-950 bg-opacity-50 rounded-lg flex flex-row items-stretch gap-2 p-4">
<div className='relative'>
<Input type="text" id="filterDaos" placeholder='Search' onChange={handleSearch} value={searchTerm} className='pr-12' />
{searchTerm && <button type='button' className='absolute -translate-x-10 pt-[4px] translate-y-1/2 hover:underline-offset-2 hover:underline text-tinkerYellow text-xxs' onClick={() => setSearchTerm('')}>clear</button>}
</div>
<div className='relative'>
<button type='button' className='rounded-lg bg-tinkerGrey hover:bg-tinkerDarkYellow p-3' onClick={handleFilters}>
<img src={FilterIcon} alt="Filter" className='h-5 w-5' />
</button>
{activeFilterCount > 0 && <span className='absolute -right-[8px] -top-[6px] rounded-full px-[6px] bg-tinkerYellow text-center text-black font-bold text-xxs'>{activeFilterCount}</span>}
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{stakingCores.map((core: StakingCore) => {
const coreInfo = coreEraStakeInfo.find((info) => info.coreId === core.key);
Expand All @@ -327,8 +527,8 @@ const DaoList = (props: DaoListProps) => {
coreRewards={coreRewards}
handleManageStaking={handleManageStaking}
handleViewDetails={(mini) => handleViewDetails(mini, projectCard(false))}
toggleExpanded={toggleReadMore}
toggleViewMembers={toggleViewMembers}
toggleExpanded={handleReadMore}
toggleViewMembers={handleViewMembers}
chainProperties={chainProperties}
availableBalance={availableBalance}
descriptionRef={minified ? projectCardRef : descriptionRef}
Expand Down
Loading

1 comment on commit 4f1d344

@vercel
Copy link

@vercel vercel bot commented on 4f1d344 Jan 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.