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: Remove the network selector #344

Closed
wants to merge 21 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
5 changes: 3 additions & 2 deletions apps/common/components/AppHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export function AppHeader(): ReactElement {

return (
<Header
showNetworkSelector
showNetworkSelector={false}
linkComponent={<Link href={''} />}
currentPathName={pathname}
onOpenMenuMobile={onOpenMenu}
Expand All @@ -179,6 +179,7 @@ export function AppHeader(): ReactElement {
<BalanceReminderPopover />
</div>
</Renderable>
} />
}
/>
);
}
14 changes: 9 additions & 5 deletions apps/common/components/ListHead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {TSortDirection} from '@common/types/types';

export type TListHead = {
items: {
label: string,
label: string | ReactElement,
value: string,
sortable?: boolean,
className?: string
Expand Down Expand Up @@ -37,18 +37,22 @@ export function ListHead({items, dataClassName, wrapperClassName, tokenClassName
return <IconChevronPlain className={'yearn--sort-chevron--off text-neutral-300 group-hover:text-neutral-500'} />;
}, [sortDirection]);

const [first, ...rest] = items;
const [chain, token, ...rest] = items;
return (
<div className={'mt-4 grid w-full grid-cols-1 md:mt-0'}>
<div className={cl('yearn--table-head-wrapper', wrapperClassName)}>
<p className={'yearn--table-head-label max-w-[32px]'}>
{chain.label}
</p>

<div className={cl('yearn--table-head-token-section', tokenClassName)}>
<button
onClick={(): void => onSort(first.value, toggleSortDirection(first.value))}
onClick={(): void => onSort(token.value, toggleSortDirection(token.value))}
className={'yearn--table-head-label-wrapper group'}>
<p className={'yearn--table-head-label'}>
{first.label}
{token.label}
</p>
{renderChevron(sortBy === first.value)}
{renderChevron(sortBy === token.value)}
</button>
</div>

Expand Down
55 changes: 55 additions & 0 deletions apps/common/components/ListHero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import {useEffect, useState} from 'react';
import {Switch as HeadlessSwitch} from '@headlessui/react';
import {useIsMounted} from '@react-hookz/web';
import {Button} from '@yearn-finance/web-lib/components/Button';
import {IconArbitrumChain} from '@yearn-finance/web-lib/icons/chains/IconArbitrumChain';
import {IconBaseChain} from '@yearn-finance/web-lib/icons/chains/IconBaseChain';
import {IconEtherumChain} from '@yearn-finance/web-lib/icons/chains/IconEtherumChain';
import {IconFantomChain} from '@yearn-finance/web-lib/icons/chains/IconFantomChain';
import {IconOptimismChain} from '@yearn-finance/web-lib/icons/chains/IconOptimismChain';
import {MultiSelectDropdown} from '@common/components/MultiSelectDropdown';
import {SearchBar} from '@common/components/SearchBar';
import {isValidCategory} from '@common/types/category';

Expand All @@ -25,6 +31,8 @@ export type TListHero<T> = {
searchPlaceholder: string;
categories: TListHeroCategory<T>[][];
onSelect: (category: T) => void;
selectedChains?: string;
set_selectedChains?: (chains: string) => void;
searchValue: string;
set_searchValue: (searchValue: string) => void;
}
Expand Down Expand Up @@ -103,9 +111,46 @@ export function ListHero<T extends string>({
categories,
onSelect,
searchValue,
selectedChains,
set_searchValue,
set_selectedChains,
switchProps
}: TListHero<T>): ReactElement {
const chains = JSON.parse(selectedChains || '[]') as number[];

const OPTIONS = [
{
label: 'Ethereum',
value: 1,
selected: chains.includes(1),
icon: <IconEtherumChain />
},
{
label: 'OP Mainnet',
value: 10,
selected: chains.includes(10),
icon: <IconOptimismChain />
},
{
label: 'Fantom',
value: 250,
selected: chains.includes(250),
icon: <IconFantomChain />
},
{
label: 'Base',
value: 8453,
selected: chains.includes(8453),
icon: <IconBaseChain />
},
{
label: 'Arbitrum One',
value: 42161,
selected: chains.includes(42161),
icon: <IconArbitrumChain />
}
];

const isMounted = useIsMounted();

return (
Expand All @@ -123,6 +168,16 @@ export function ListHero<T extends string>({
categories={categories}
onSelect={onSelect} />

<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));
}}
/>

<SearchBar
searchPlaceholder={searchPlaceholder}
searchValue={searchValue}
Expand Down
187 changes: 187 additions & 0 deletions apps/common/components/MultiSelectDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import {Fragment, useState} from 'react';
import {Combobox, Transition} from '@headlessui/react';
import {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';

import type {ReactElement} from 'react';

export type TMultiSelectOptionProps = {
label: string;
value: number | string;
selected: 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}>
<div className={'flex w-full items-center justify-between p-2'}>
<p className={'pl-0 font-normal text-neutral-400'}>
{option.label}
</p>
<input type={'checkbox'} checked={option.selected} className={'checked:bg-black'} readOnly />
</div>
</Combobox.Option>
);
}

function Option(option: TMultiSelectOptionProps): ReactElement {
return (
<Combobox.Option value={option}>
<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'} readOnly />
</div>
</Combobox.Option>
);
}

function DropdownEmpty({query}: {query: string}): ReactElement {
const {isActive, openLoginModal} = useWeb3();

if (!isActive) {
return (
<div
onClick={(): void => openLoginModal()}
className={'flex h-14 cursor-pointer flex-col items-center justify-center px-4 text-center transition-colors hover:bg-neutral-300'}>
<b className={'text-neutral-900'}>{'Connect Wallet'}</b>
</div>
);
}
if (query !== '') {
return (
<div className={'relative flex h-14 flex-col items-center justify-center px-4 text-center'}>
<div className={'flex h-10 items-center justify-center'}>
<p className={'text-sm text-neutral-900'}>{'Nothing found.'}</p>
</div>
</div>
);
}
return (
<div className={'relative flex h-14 flex-col items-center justify-center px-4 text-center'}>
<div className={'flex h-10 items-center justify-center'}>
<span className={'loader'} />
</div>
</div>
);
}

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 [query, set_query] = useState('');

const filteredOptions = query === ''
? currentOptions
: currentOptions.filter((option): boolean => {
return option.label.toLowerCase().includes(query.toLowerCase());
});

return (
<Combobox
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};
});
set_isSelectAll(!elementSelected.selected);
} else {
currentState = currentElements.map((option): TMultiSelectOptionProps => {
return option.value === elementSelected.value ? {...option, selected: !option.selected} : option;
});

set_isSelectAll(!currentState.some((option): boolean => !option.selected));
}

set_currentOptions(currentState);
onSelect(currentState);
}}
nullable
multiple
>
<div className={'relative w-[32rem]'}>
<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);
if (selectedOptions.length === 0) {
return 'Select chain';
}

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

return 'Multiple';
}}
placeholder={placeholder}
spellCheck={false}
onChange={(event): void => set_query(event.target.value)} />
<IconChevron
aria-hidden={'true'}
className={`h-6 w-6 transition-transform ${isOpen ? '-rotate-180' : 'rotate-0'}`}
/>
</Combobox.Button>
<Transition
as={Fragment}
show={isOpen}
enter={'transition duration-100 ease-out'}
enterFrom={'transform scale-95 opacity-0'}
enterTo={'transform scale-100 opacity-100'}
leave={'transition duration-75 ease-out'}
leaveFrom={'transform scale-100 opacity-100'}
leaveTo={'transform scale-95 opacity-0'}
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'}>
<SelectAllOption key={'select-all'} label={'Select all'} selected={isSelectAll} value={'select_all'} />
<Renderable
shouldRender={filteredOptions.length > 0}
fallback={<DropdownEmpty query={query} />}>
{filteredOptions.map((option): ReactElement => {
return <Option key={option.value} {...option} />;
}
)}
</Renderable>
</Combobox.Options>
</Transition>
</div>
</Combobox>
);
}
5 changes: 3 additions & 2 deletions apps/common/contexts/useYearn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const YearnContext = createContext<TYearnContext>({
export const YearnContextApp = memo(function YearnContextApp({children}: { children: ReactElement }): ReactElement {
const {safeChainID} = useChainID();
const {yDaemonBaseUri} = useYDaemonBaseURI({chainID: safeChainID});
const {yDaemonBaseUri: yDaemonBaseUriWithoutChain} = useYDaemonBaseURI();
const result = useYDaemonStatus({chainID: safeChainID});
const {address, currentPartner} = useWeb3();
const [zapSlippage, set_zapSlippage] = useLocalStorage<number>('yearn.fi/zap-slippage', DEFAULT_SLIPPAGE);
Expand All @@ -91,14 +92,14 @@ export const YearnContextApp = memo(function YearnContextApp({children}: { child
});

const {data: vaults, isLoading: isLoadingVaultList, mutate: mutateVaultList} = useFetch<TYDaemonVaults>({
endpoint: `${yDaemonBaseUri}/vaults/all?${new URLSearchParams({
endpoint: `${yDaemonBaseUriWithoutChain}/vaults/all?${new URLSearchParams({
hideAlways: 'true',
orderBy: 'apy.net_apy',
orderDirection: 'desc',
strategiesDetails: 'withDetails',
strategiesRisk: 'withRisk',
strategiesCondition: 'inQueue'
})}`,
})}&chainIDs=${[1, 10].join(',')}`,
schema: yDaemonVaultsSchema
});

Expand Down
13 changes: 9 additions & 4 deletions apps/common/utils/getYDaemonBaseURI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@ type TProps = {
chainID: number | string;
};

export function useYDaemonBaseURI({chainID}: TProps): {
export function useYDaemonBaseURI(props?: TProps): {
yDaemonBaseUri: string;
} {
// eslint-disable-next-line @typescript-eslint/naming-convention
const {settings} = useSettings();

const baseUri = settings.yDaemonBaseURI || process.env.YDAEMON_BASE_URI;
const yDaemonBaseUri =
settings.yDaemonBaseURI || process.env.YDAEMON_BASE_URI;

if (!baseUri) {
if (!yDaemonBaseUri) {
throw new Error('YDAEMON_BASE_URI is not defined');
}

return {yDaemonBaseUri: `${baseUri}/${chainID}`};
if (!props?.chainID) {
return {yDaemonBaseUri};
}

return {yDaemonBaseUri: `${yDaemonBaseUri}/${props.chainID}`};
}
4 changes: 1 addition & 3 deletions apps/vaults/components/details/VaultDetailsHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {useMemo} from 'react';
import {useStakingRewards} from '@vaults/contexts/useStakingRewards';
import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3';
import {useChainID} from '@yearn-finance/web-lib/hooks/useChainID';
import {toAddress} from '@yearn-finance/web-lib/utils/address';
import {toBigInt, toNormalizedBN} from '@yearn-finance/web-lib/utils/format.bigNumber';
import {formatUSD} from '@yearn-finance/web-lib/utils/format.number';
Expand Down Expand Up @@ -43,9 +42,8 @@ function VaultHeaderLineItem({label, children, legend}: TVaultHeaderLineItemProp
}

export function VaultDetailsHeader({vault}: { vault: TYDaemonVault }): ReactElement {
const {safeChainID} = useChainID();
const {address: userAddress} = useWeb3();
const {yDaemonBaseUri} = useYDaemonBaseURI({chainID: safeChainID});
const {yDaemonBaseUri} = useYDaemonBaseURI({chainID: vault.chainID});
const {address, apy, tvl, decimals, symbol = 'token', token} = vault;
const {data: earned} = useFetch<TYDaemonEarned>({
endpoint: (address && userAddress) ? `${yDaemonBaseUri}/earned/${userAddress}` : null,
Expand Down
Loading
Loading