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

Feat/343 #368

Closed
wants to merge 9 commits into from
Closed
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
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module.exports = {
extends: ['./node_modules/@yearn-finance/web-lib/.eslintrc.cjs', 'prettier'],
extends: ['./node_modules/@yearn-finance/web-lib/.eslintrc.cjs', 'plugin:@next/next/recommended', 'prettier'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
Expand Down
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"editor.wordWrap": "off",
"editor.autoIndent": "keep",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": false
"source.fixAll": true
},
"editor.quickSuggestions": {
"strings": true
Expand Down
61 changes: 36 additions & 25 deletions apps/common/components/BalanceReminderPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ import {IconAddToMetamask} from '@yearn-finance/web-lib/icons/IconAddToMetamask'
import {IconCross} from '@yearn-finance/web-lib/icons/IconCross';
import {IconWallet} from '@yearn-finance/web-lib/icons/IconWallet';
import {toAddress, truncateHex} from '@yearn-finance/web-lib/utils/address';
import {toBigInt} from '@yearn-finance/web-lib/utils/format.bigNumber';
import {formatAmount} from '@yearn-finance/web-lib/utils/format.number';
import {ImageWithFallback} from '@common/components/ImageWithFallback';
import {useWallet} from '@common/contexts/useWallet';
import {useYearn} from '@common/contexts/useYearn';
import {useBalance} from '@common/hooks/useBalance';

import type {ReactElement} from 'react';
import type {TAddress, TDict} from '@yearn-finance/web-lib/types';
import type {TBalanceData} from '@yearn-finance/web-lib/types/hooks';
import type {TAddress} from '@yearn-finance/web-lib/types';
import type {TChainTokens} from '@common/types/types';

type TBalanceReminderElement = {
address: TAddress;
chainID: number;
normalizedBalance: number;
decimals: number;
symbol: string;
Expand All @@ -29,7 +29,7 @@ type TBalanceReminderElement = {
function TokenItem({element}: {element: TBalanceReminderElement}): ReactElement {
const {provider} = useWeb3();
const {safeChainID} = useChainID();
const balance = useBalance(element.address);
const balance = useBalance({address: element.address, chainID: element.chainID});

async function addTokenToMetamask(address: TAddress, symbol: string, decimals: number, image: string): Promise<void> {
if (!provider) {
Expand Down Expand Up @@ -90,35 +90,46 @@ function TokenItem({element}: {element: TBalanceReminderElement}): ReactElement
}

export function BalanceReminderPopover(): ReactElement {
const {balances, isLoading} = useWallet();
const {balances: tokens, isLoading} = useWallet();
const {address, ens, isActive, onDesactivate} = useWeb3();
const {vaults} = useYearn();

const nonNullBalances = useMemo((): TDict<TBalanceData> => {
const nonNullBalances = Object.entries(balances).reduce((acc: TDict<TBalanceData>, [address, balance]): TDict<TBalanceData> => {
if (toBigInt(balance?.raw) > 0n) {
acc[toAddress(address)] = balance;
const nonNullBalances = useMemo((): TChainTokens => {
const nonNullBalances: TChainTokens = {};

for (const [chainIDStr, chainTokens] of Object.entries(tokens)) {
const chainID = Number(chainIDStr);
nonNullBalances[chainID] = {};
for (const [, token] of Object.entries(chainTokens)) {
if (token.balance.raw > 0n) {
nonNullBalances[chainID][token.address] = token;
}
}
return acc;
}, {});
}
return nonNullBalances;
}, [balances]);
}, [tokens]);

const nonNullBalancesForVault = useMemo((): TBalanceReminderElement[] => {
const nonNullBalancesForVault = Object.entries(nonNullBalances).reduce((acc: TBalanceReminderElement[], [address, balance]): TBalanceReminderElement[] => {
const currentVault = vaults?.[toAddress(address)];
if (currentVault) {
acc.push({
address: toAddress(address),
normalizedBalance: balance.normalized,
decimals: balance.decimals,
symbol: currentVault.symbol
});
const nonNullBalancesForVault: TBalanceReminderElement[] = [];

for (const [, chainBalanceData] of Object.entries(nonNullBalances)) {
for (const [address, token] of Object.entries(chainBalanceData)) {
if (token.balance.raw > 0n) {
const currentVault = vaults?.[toAddress(address)];
if (currentVault) {
nonNullBalancesForVault.push({
address: toAddress(address),
chainID: currentVault.chainID,
normalizedBalance: Number(token.balance.normalized),
decimals: token.decimals,
symbol: currentVault.symbol
});
}
}
}
return acc;
}, []);
}
return nonNullBalancesForVault;
}, [nonNullBalances, vaults]);
}, [nonNullBalances]);

function renderNoTokenFallback(isLoading: boolean): ReactElement {
if (isLoading) {
Expand Down Expand Up @@ -161,7 +172,7 @@ export function BalanceReminderPopover(): ReactElement {
{nonNullBalancesForVault.map(
(element): ReactElement => (
<TokenItem
key={element.address}
key={`${element.chainID}-${element.address}`}
element={element}
/>
)
Expand Down
30 changes: 13 additions & 17 deletions apps/common/components/ListHero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,31 +129,31 @@ export function ListHero<T extends string>({
{
label: 'Ethereum',
value: 1,
selected: chains.includes(1),
isSelected: chains.includes(1),
icon: <IconEtherumChain />
},
{
label: 'OP Mainnet',
value: 10,
selected: chains.includes(10),
isSelected: chains.includes(10),
icon: <IconOptimismChain />
},
{
label: 'Fantom',
value: 250,
selected: chains.includes(250),
isSelected: chains.includes(250),
icon: <IconFantomChain />
},
{
label: 'Base',
value: 8453,
selected: chains.includes(8453),
isSelected: chains.includes(8453),
icon: <IconBaseChain />
},
{
label: 'Arbitrum One',
value: 42161,
selected: chains.includes(42161),
isSelected: chains.includes(42161),
icon: <IconArbitrumChain />
}
];
Expand All @@ -176,18 +176,14 @@ export function ListHero<T extends string>({
onSelect={onSelect}
/>

<div>
<small>{'Select Blockchain'}</small>
<MultiSelectDropdown
defaultOption={OPTIONS[0]}
options={OPTIONS}
placeholder={'Select chain'}
onSelect={(options): void => {
const selectedChains = options.filter((o): boolean => o.selected).map((option): number => Number(option.value));
set_selectedChains?.(JSON.stringify(selectedChains));
}}
/>
</div>
<MultiSelectDropdown
options={OPTIONS}
placeholder={'Select chain'}
onSelect={(options): void => {
const selectedChains = options.filter((o): boolean => o.isSelected).map((option): number => Number(option.value));
set_selectedChains?.(JSON.stringify(selectedChains));
}}
/>

<SearchBar
searchPlaceholder={searchPlaceholder}
Expand Down
74 changes: 49 additions & 25 deletions apps/common/components/MultiSelectDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Fragment, useState} from 'react';
import {Fragment, useEffect, useRef, useState} from 'react';
import {Combobox, Transition} from '@headlessui/react';
import {useThrottledState} from '@react-hookz/web';
import {useClickOutside, useThrottledState} from '@react-hookz/web';
import {Renderable} from '@yearn-finance/web-lib/components/Renderable';
import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3';
import {IconChevron} from '@common/icons/IconChevron';
Expand All @@ -10,27 +10,27 @@ import type {ReactElement} from 'react';
export type TMultiSelectOptionProps = {
label: string;
value: number | string;
selected: boolean;
isSelected: boolean;
icon?: ReactElement;
};

type TMultiSelectProps = {
options: TMultiSelectOptionProps[];
defaultOption: TMultiSelectOptionProps;
placeholder?: string;
onSelect: (options: TMultiSelectOptionProps[]) => void;
};

function SelectAllOption(option: TMultiSelectOptionProps): ReactElement {
return (
<Combobox.Option value={option}>
<Combobox.Option
value={option}
className={option.isSelected ? 'cursor-default opacity-60' : 'transition-colors hover:bg-neutral-100'}>
<div className={'flex w-full items-center justify-between p-2'}>
<p className={'pl-0 font-normal text-neutral-400'}>{option.label}</p>
<p className={'pl-0 font-normal text-neutral-900'}>{option.label}</p>
<input
type={'checkbox'}
checked={option.selected}
className={'checked:bg-black'}
readOnly
checked={option.isSelected}
className={'checkbox'}
/>
</div>
</Combobox.Option>
Expand All @@ -39,16 +39,18 @@ function SelectAllOption(option: TMultiSelectOptionProps): ReactElement {

function Option(option: TMultiSelectOptionProps): ReactElement {
return (
<Combobox.Option value={option}>
<Combobox.Option
value={option}
className={'transition-colors hover:bg-neutral-100'}>
<div className={'flex w-full items-center justify-between p-2'}>
<div className={'flex items-center'}>
{option?.icon ? <div className={'h-8 w-8 rounded-full'}>{option.icon}</div> : null}
<p className={`${option.icon ? 'pl-2' : 'pl-0'} font-normal text-neutral-900`}>{option.label}</p>
</div>
<input
type={'checkbox'}
checked={option.selected}
className={'checked:bg-black'}
checked={option.isSelected}
className={'checkbox'}
readOnly
/>
</div>
Expand Down Expand Up @@ -89,8 +91,17 @@ function DropdownEmpty({query}: {query: string}): ReactElement {
export function MultiSelectDropdown({options, onSelect, placeholder = ''}: TMultiSelectProps): ReactElement {
const [isOpen, set_isOpen] = useThrottledState(false, 400);
const [currentOptions, set_currentOptions] = useState<TMultiSelectOptionProps[]>(options);
const [isSelectAll, set_isSelectAll] = useState(false);
const [areAllSelected, set_areAllSelected] = useState(false);
const [query, set_query] = useState('');
const componentRef = useRef(null);

useEffect((): void => {
set_areAllSelected(currentOptions.every((option): boolean => option.isSelected));
}, [currentOptions]);

useClickOutside(componentRef, (): void => {
set_isOpen(false);
});

const filteredOptions =
query === ''
Expand All @@ -101,52 +112,65 @@ export function MultiSelectDropdown({options, onSelect, placeholder = ''}: TMult

return (
<Combobox
ref={componentRef}
value={currentOptions}
onChange={(options): void => {
// Hack(ish) because with this Combobox component we cannot unselect items
const lastIndex = options.length - 1;
const elementSelected = options[lastIndex];
const currentElements = options.slice(0, lastIndex);

let currentState: TMultiSelectOptionProps[] = [];

if (elementSelected.value === 'select_all') {
currentState = currentElements.map((option): TMultiSelectOptionProps => {
return {
...option,
selected: !elementSelected.selected
isSelected: !elementSelected.isSelected
};
});
set_isSelectAll(!elementSelected.selected);
set_areAllSelected(!elementSelected.isSelected);
} else {
currentState = currentElements.map((option): TMultiSelectOptionProps => {
return option.value === elementSelected.value ? {...option, selected: !option.selected} : option;
return option.value === elementSelected.value ? {...option, isSelected: !option.isSelected} : option;
});
set_areAllSelected(!currentState.some((option): boolean => !option.isSelected));
}

set_isSelectAll(!currentState.some((option): boolean => !option.selected));
// if none are selected in currentState, then select all
if (!currentState.some((option): boolean => option.isSelected)) {
currentState = currentOptions.map((option): TMultiSelectOptionProps => {
return {
...option,
isSelected: true
};
});
set_areAllSelected(true);
}

set_currentOptions(currentState);
onSelect(currentState);
}}
nullable
multiple>
<div className={'relative w-[32rem]'}>
<div className={'relative w-full'}>
<Combobox.Button
onClick={(): void => set_isOpen((o: boolean): boolean => !o)}
className={'flex h-10 w-full items-center justify-between bg-neutral-0 p-2 text-base text-neutral-900 md:px-3'}>
<Combobox.Input
className={'w-full cursor-default overflow-x-scroll border-none bg-transparent p-0 outline-none scrollbar-none'}
displayValue={(options: TMultiSelectOptionProps[]): string => {
const selectedOptions = options.filter((option): boolean => option.selected);
const selectedOptions = options.filter((option): boolean => option.isSelected);
if (selectedOptions.length === 0) {
return 'Select chain';
return placeholder;
}

if (selectedOptions.length === 1) {
return selectedOptions[0].label;
}

if (areAllSelected) {
return 'All';
}

return 'Multiple';
}}
placeholder={placeholder}
Expand All @@ -170,11 +194,11 @@ export function MultiSelectDropdown({options, onSelect, placeholder = ''}: TMult
afterLeave={(): void => {
set_query('');
}}>
<Combobox.Options className={'absolute top-12 z-50 flex w-full cursor-pointer flex-col overflow-y-auto bg-white px-2 py-3 scrollbar-none'}>
<Combobox.Options className={'absolute top-12 z-50 flex w-full cursor-pointer flex-col overflow-y-auto bg-neutral-0 px-2 py-3 scrollbar-none'}>
<SelectAllOption
key={'select-all'}
label={'Select all'}
selected={isSelectAll}
label={'All'}
isSelected={areAllSelected}
value={'select_all'}
/>
<Renderable
Expand Down
7 changes: 3 additions & 4 deletions apps/common/components/TokenDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {IconChevron} from '@common/icons/IconChevron';
import type {ReactElement} from 'react';
import type {TDropdownItemProps, TDropdownOption, TDropdownProps} from '@common/types/types';

function DropdownItem({option, balanceSource}: TDropdownItemProps): ReactElement {
const balance = useBalance(option.value, balanceSource);
function DropdownItem({option}: TDropdownItemProps): ReactElement {
const balance = useBalance({address: option.value, chainID: option.chainID});

return (
<Combobox.Option value={option}>
Expand Down Expand Up @@ -61,7 +61,7 @@ function DropdownEmpty({query}: {query: string}): ReactElement {
);
}

export function Dropdown({options, selected, onSelect, placeholder = '', balanceSource}: TDropdownProps): ReactElement {
export function Dropdown({options, selected, onSelect, placeholder = ''}: TDropdownProps): ReactElement {
const [isOpen, set_isOpen] = useThrottledState(false, 400);
const [query, set_query] = useState('');

Expand Down Expand Up @@ -149,7 +149,6 @@ export function Dropdown({options, selected, onSelect, placeholder = '', balance
<DropdownItem
key={option.label}
option={option}
balanceSource={balanceSource}
/>
)
)}
Expand Down
Loading
Loading