Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Yaki-dao-filters-011624 #48

Merged
merged 13 commits into from
Jan 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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