feat: UI create functionality to resolve address (#227)

* feat: add ENS slice

* feat: add resolved address hook

* feat: add resolved address component
This commit is contained in:
Felipe Mendes 2023-04-17 14:23:17 -03:00 committed by GitHub
parent 9934ad1cfd
commit 5ae09105c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 175 additions and 1 deletions

View File

@ -7,3 +7,4 @@ export * from './toast';
export * from './step';
export * from './nfa-card';
export * from './nfa-preview';
export * from './resolved-address';

View File

@ -0,0 +1 @@
export * from './resolved-address';

View File

@ -0,0 +1,21 @@
import { keyframes, styled } from '@/theme';
const Loading = keyframes({
'0%': {
opacity: 1,
},
'50%': {
opacity: 0.5,
},
'100%': {
opacity: 1,
},
});
export const ResolvedAddressStyles = {
Container: styled('span', {
'&[data-loading="true"]': {
animation: `${Loading} 1s ease-in-out infinite`,
},
}),
};

View File

@ -0,0 +1,31 @@
import { useMemo } from 'react';
import { useResolvedAddress } from '@/store';
import { forwardStyledRef } from '@/theme';
import { ResolvedAddressStyles as RAS } from './resolved-address.styles';
export type ResolvedAddressProps = React.ComponentPropsWithRef<
typeof RAS.Container
> & {
children: string;
};
export const ResolvedAddress = forwardStyledRef<
HTMLSpanElement,
ResolvedAddressProps
>(({ children, ...props }, ref) => {
const [resolvedAddress, loading] = useResolvedAddress(children);
const text = useMemo(() => {
if (!resolvedAddress.endsWith('.eth'))
return `${resolvedAddress.slice(0, 6)}...${resolvedAddress.slice(-4)}`;
return resolvedAddress;
}, [resolvedAddress]);
return (
<RAS.Container {...props} ref={ref} data-loading={loading}>
{text}
</RAS.Container>
);
});

View File

@ -0,0 +1 @@
export * from './resolve-address';

View File

@ -0,0 +1,37 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { ethers } from 'ethers';
import { env } from '@/constants';
import { ENSActions, RootState } from '@/store';
import { AppLog } from '@/utils';
export const resolveAddress = createAsyncThunk<void, string>(
'ENS/fetchAddress',
async (address, { dispatch, getState }) => {
const { addressMap } = (getState() as RootState).ENS;
const stored = addressMap[address] || {};
if (stored.state === 'loading') return;
try {
dispatch(
ENSActions.setAddress({ key: address, value: { state: 'loading' } })
);
const provider = new ethers.providers.JsonRpcProvider(env.goerli.rpc);
const value = (await provider.lookupAddress(address)) || undefined;
dispatch(
ENSActions.setAddress({
key: address,
value: { state: 'success', value },
})
);
} catch (error) {
AppLog.error('Failed to resolve ENS name by address', error);
dispatch(
ENSActions.setAddress({ key: address, value: { state: 'loading' } })
);
}
}
);

View File

@ -0,0 +1,52 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState } from '@/store';
import { useAppSelector } from '@/store/hooks';
import * as asyncThunk from './async-thunk';
export namespace ENSState {
export type QueryState = undefined | 'loading' | 'failed' | 'success';
export type Address = {
state: QueryState;
value?: string;
};
export type AddressMap = Record<string, Address>;
}
export interface ENSState {
addressMap: ENSState.AddressMap;
}
const initialState: ENSState = {
addressMap: {},
};
export const ENSSlice = createSlice({
name: 'ENSSLice',
initialState,
reducers: {
setAddress: (
state,
action: PayloadAction<{
key: string;
value: ENSState.Address;
}>
) => {
state.addressMap[action.payload.key] = action.payload.value;
},
},
});
export const ENSActions = {
...ENSSlice.actions,
...asyncThunk,
};
const selectENSState = (state: RootState): ENSState => state.ENS;
export const useENSStore = (): ENSState => useAppSelector(selectENSState);
export default ENSSlice.reducer;

View File

@ -0,0 +1 @@
export * from './use-resolved-address';

View File

@ -0,0 +1,21 @@
import { useEffect, useMemo } from 'react';
import { useAppDispatch } from '@/store/hooks';
import { ENSActions, useENSStore } from '../ens-slice';
export const useResolvedAddress = (address: string): [string, boolean] => {
const { addressMap } = useENSStore();
const dispatch = useAppDispatch();
useEffect(() => {
const stored = addressMap[address] || {};
if (typeof stored.state !== 'undefined') return;
dispatch(ENSActions.resolveAddress(address));
}, [address, dispatch, addressMap]);
return useMemo(() => {
const stored = addressMap[address] || {};
return [stored.value || address, addressMap[address]?.state === 'loading'];
}, [address, addressMap]);
};

View File

@ -0,0 +1,2 @@
export * from './ens-slice';
export * from './hooks';

View File

@ -1,3 +1,4 @@
export * from './fleek-erc721';
export * from './github';
export * from './toasts';
export * from './ens';

View File

@ -1,5 +1,6 @@
import { configureStore } from '@reduxjs/toolkit';
import ENSReducer from './features/ens/ens-slice';
import fleekERC721Reducer from './features/fleek-erc721/fleek-erc721-slice';
import githubReducer from './features/github/github-slice';
import toastsReducer from './features/toasts/toasts-slice';
@ -9,6 +10,7 @@ export const store = configureStore({
fleekERC721: fleekERC721Reducer,
github: githubReducer,
toasts: toastsReducer,
ENS: ENSReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({

View File

@ -1,4 +1,4 @@
import { Flex } from '@/components';
import { Flex, ResolvedAddress } from '@/components';
import { ColorPickerTest } from './color-picker';
import { ComboboxTest } from './combobox-test';
@ -7,6 +7,9 @@ import { ToastTest } from './toast-test';
export const ComponentsTest: React.FC = () => {
return (
<Flex css={{ flexDirection: 'column' }}>
<ResolvedAddress css={{ alignSelf: 'center' }}>
{'0x7ed735b7095c05d78df169f991f2b7f1a1f1a049'}
</ResolvedAddress>
<ComboboxTest />
<ColorPickerTest />
<ToastTest />