Commit 898b1a42 authored by tom's avatar tom

Merge branch 'main' of github.com:tom2drum/block-scout into mobile-view-pages

parents af2ec198 c0637440
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path d="M15 25c5.523 0 10-4.477 10-10S20.523 5 15 5 5 9.477 5 15s4.477 10 10 10Z" stroke="currentColor" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M21.667 22.333a6.666 6.666 0 1 0-13.334 0" stroke="currentColor" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M15 15.667a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z" stroke="currentColor" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
</svg>
\ No newline at end of file
import { useQuery } from '@tanstack/react-query';
import type { UserInfo } from 'types/api/account';
import fetch from 'lib/client/fetch';
export default function useFetchProfileInfo() {
return useQuery<unknown, unknown, UserInfo>([ 'profile' ], async() => {
return fetch('/api/account/profile');
}, { refetchOnMount: false });
}
...@@ -6,6 +6,7 @@ import appsIcon from 'icons/apps.svg'; ...@@ -6,6 +6,7 @@ import appsIcon from 'icons/apps.svg';
import blocksIcon from 'icons/block.svg'; import blocksIcon from 'icons/block.svg';
import gearIcon from 'icons/gear.svg'; import gearIcon from 'icons/gear.svg';
import privateTagIcon from 'icons/privattags.svg'; import privateTagIcon from 'icons/privattags.svg';
import profileIcon from 'icons/profile.svg';
import publicTagIcon from 'icons/publictags.svg'; import publicTagIcon from 'icons/publictags.svg';
import tokensIcon from 'icons/token.svg'; import tokensIcon from 'icons/token.svg';
import transactionsIcon from 'icons/transactions.svg'; import transactionsIcon from 'icons/transactions.svg';
...@@ -32,6 +33,8 @@ export default function useNavItems() { ...@@ -32,6 +33,8 @@ export default function useNavItems() {
{ text: 'Custom ABI', pathname: basePath + '/account/custom_abi', icon: abiIcon }, { text: 'Custom ABI', pathname: basePath + '/account/custom_abi', icon: abiIcon },
]; ];
return { mainNavItems, accountNavItems }; const profileItem = { text: 'My profile', pathname: basePath + '/auth/profile', icon: profileIcon };
return { mainNavItems, accountNavItems, profileItem };
}, [ basePath ]); }, [ basePath ]);
} }
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import MyProfile from 'ui/pages/MyProfile';
const MyProfilePage: NextPage = () => {
return (
<>
<Head><title>My profile</title></Head>
<MyProfile/>
</>
);
};
export default MyProfilePage;
import type { UserInfo } from 'types/api/account';
import handler from 'lib/api/handler';
const profileHandler = handler<UserInfo, unknown>(() => '/account/v1/user/info', [ 'GET' ]);
export default profileHandler;
...@@ -17,18 +17,20 @@ const variantPrimary = { ...@@ -17,18 +17,20 @@ const variantPrimary = {
}, },
}; };
const variantSecondary = { const variantSecondary: SystemStyleFunction = (props) => {
color: 'blue.600', return {
fontWeight: 600, color: mode('blue.600', 'blue.300')(props),
borderColor: 'blue.600', fontWeight: 600,
border: '2px solid', borderColor: mode('blue.600', 'blue.300')(props),
_hover: { border: '2px solid',
color: 'blue.400', _hover: {
borderColor: 'blue.400', color: 'blue.400',
}, borderColor: 'blue.400',
_disabled: { },
opacity: 0.2, _disabled: {
}, opacity: 0.2,
},
};
}; };
const variantIcon: SystemStyleFunction = (props) => { const variantIcon: SystemStyleFunction = (props) => {
......
...@@ -2,7 +2,11 @@ import type { drawerAnatomy as parts } from '@chakra-ui/anatomy'; ...@@ -2,7 +2,11 @@ import type { drawerAnatomy as parts } from '@chakra-ui/anatomy';
import type { SystemStyleFunction, PartsStyleFunction, SystemStyleObject } from '@chakra-ui/theme-tools'; import type { SystemStyleFunction, PartsStyleFunction, SystemStyleObject } from '@chakra-ui/theme-tools';
import { mode } from '@chakra-ui/theme-tools'; import { mode } from '@chakra-ui/theme-tools';
import getDefaultTransitionProps from '../utils/getDefaultTransitionProps';
const transitionProps = getDefaultTransitionProps();
const baseStyleOverlay: SystemStyleObject = { const baseStyleOverlay: SystemStyleObject = {
...transitionProps,
bg: 'blackAlpha.800', bg: 'blackAlpha.800',
zIndex: 'overlay', zIndex: 'overlay',
}; };
...@@ -12,6 +16,7 @@ const baseStyleDialog: SystemStyleFunction = (props) => { ...@@ -12,6 +16,7 @@ const baseStyleDialog: SystemStyleFunction = (props) => {
return { return {
...(isFullHeight && { height: '100vh' }), ...(isFullHeight && { height: '100vh' }),
...transitionProps,
zIndex: 'modal', zIndex: 'modal',
maxH: '100vh', maxH: '100vh',
bg: mode('white', 'gray.900')(props), bg: mode('white', 'gray.900')(props),
......
...@@ -51,7 +51,8 @@ export type Transactions = Array<Transaction> ...@@ -51,7 +51,8 @@ export type Transactions = Array<Transaction>
export interface UserInfo { export interface UserInfo {
name?: string; name?: string;
nickname?: string; nickname?: string;
email?: string; email: string;
avatar?: string;
} }
export interface WatchlistAddress { export interface WatchlistAddress {
......
...@@ -32,7 +32,7 @@ type Inputs = { ...@@ -32,7 +32,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255; const NAME_MAX_LENGTH = 255;
const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const { control, handleSubmit, formState: { errors }, setError } = useForm<Inputs>({ const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
mode: 'all', mode: 'all',
defaultValues: { defaultValues: {
token: data?.api_key || '', token: data?.api_key || '',
...@@ -143,7 +143,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -143,7 +143,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
size="lg" size="lg"
variant="primary" variant="primary"
onClick={ handleSubmit(onSubmit) } onClick={ handleSubmit(onSubmit) }
disabled={ Object.keys(errors).length > 0 } disabled={ !isValid }
isLoading={ mutation.isLoading } isLoading={ mutation.isLoading }
> >
{ data ? 'Save' : 'Generate API key' } { data ? 'Save' : 'Generate API key' }
......
...@@ -3,11 +3,12 @@ import React from 'react'; ...@@ -3,11 +3,12 @@ import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import NetworkLogo from 'ui/blocks/networkMenu/NetworkLogo'; import NetworkLogo from 'ui/blocks/networkMenu/NetworkLogo';
import ProfileMenuDesktop from 'ui/blocks/profileMenu/ProfileMenuDesktop';
import ProfileMenuMobile from 'ui/blocks/profileMenu/ProfileMenuMobile';
import SearchBar from 'ui/blocks/searchBar/SearchBar'; import SearchBar from 'ui/blocks/searchBar/SearchBar';
import Burger from './Burger'; import Burger from './Burger';
import ColorModeToggler from './ColorModeToggler'; import ColorModeToggler from './ColorModeToggler';
import ProfileMenu from './ProfileMenu';
const Header = () => { const Header = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -31,7 +32,7 @@ const Header = () => { ...@@ -31,7 +32,7 @@ const Header = () => {
> >
<Burger/> <Burger/>
<NetworkLogo/> <NetworkLogo/>
<ProfileMenu/> <ProfileMenuMobile/>
</Flex> </Flex>
<SearchBar/> <SearchBar/>
</Box> </Box>
...@@ -48,7 +49,7 @@ const Header = () => { ...@@ -48,7 +49,7 @@ const Header = () => {
> >
<SearchBar/> <SearchBar/>
<ColorModeToggler/> <ColorModeToggler/>
<ProfileMenu/> <ProfileMenuDesktop/>
</HStack> </HStack>
); );
}; };
......
import { Center, useColorModeValue, chakra } from '@chakra-ui/react';
import React from 'react';
import Identicon from 'react-identicons';
import useIsMobile from 'lib/hooks/useIsMobile';
const ProfileIcon = chakra(Identicon);
const ProfileMenu = () => {
const isMobile = useIsMobile();
const size = isMobile ? '24px' : '50px';
return (
<Center
flexShrink={ 0 }
padding={ isMobile ? 2 : 0 }
>
{ /* the displayed size is 48px, but we need to generate x2 for retina displays */ }
<ProfileIcon
maxWidth={ size }
maxHeight={ size }
string="randomness"
size={ 100 }
bg={ useColorModeValue('blackAlpha.100', 'white') }
borderRadius="50%"
overflow="hidden"
/>
</Center>
);
};
export default ProfileMenu;
...@@ -9,13 +9,14 @@ import useColors from './useColors'; ...@@ -9,13 +9,14 @@ import useColors from './useColors';
interface Props { interface Props {
isCollapsed?: boolean; isCollapsed?: boolean;
isActive: boolean; isActive?: boolean;
pathname: string; pathname: string;
text: string; text: string;
icon: React.FunctionComponent<React.SVGAttributes<SVGElement>>; icon: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
px?: string | number;
} }
const NavLink = ({ text, pathname, icon, isCollapsed, isActive }: Props) => { const NavLink = ({ text, pathname, icon, isCollapsed, isActive, px }: Props) => {
const colors = useColors(); const colors = useColors();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const width = (() => { const width = (() => {
...@@ -32,7 +33,7 @@ const NavLink = ({ text, pathname, icon, isCollapsed, isActive }: Props) => { ...@@ -32,7 +33,7 @@ const NavLink = ({ text, pathname, icon, isCollapsed, isActive }: Props) => {
as="li" as="li"
listStyleType="none" listStyleType="none"
w={ width } w={ width }
px={ isCollapsed ? '15px' : 3 } px={ px || (isCollapsed ? '15px' : 3) }
py={ 2.5 } py={ 2.5 }
color={ isActive ? colors.text.active : colors.text.default } color={ isActive ? colors.text.active : colors.text.default }
bgColor={ isActive ? colors.bg.active : colors.bg.default } bgColor={ isActive ? colors.bg.active : colors.bg.default }
......
import { Box, Button, Text, VStack, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { UserInfo } from 'types/api/account';
import useNavItems from 'lib/hooks/useNavItems';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NavLink from 'ui/blocks/navigation/NavLink';
type Props = UserInfo;
const ProfileMenuContent = ({ name, nickname, email }: Props) => {
const { accountNavItems, profileItem } = useNavItems();
const borderColor = useColorModeValue('gray.200', 'whiteAlpha.200');
const primaryTextColor = useColorModeValue('gray.600', 'whiteAlpha.800');
return (
<Box>
<Text
fontSize="sm"
fontWeight={ 500 }
color={ primaryTextColor }
{ ...getDefaultTransitionProps() }
>
Signed in as { name || nickname }
</Text>
<Text
fontSize="sm"
mb={ 1 }
fontWeight={ 500 }
color="gray.500"
{ ...getDefaultTransitionProps() }
>
{ email }
</Text>
<NavLink { ...profileItem } px="0px"/>
<Box as="nav" mt={ 2 } pt={ 2 } borderTopColor={ borderColor } borderTopWidth="1px" { ...getDefaultTransitionProps() }>
<VStack as="ul" spacing="0" alignItems="flex-start" overflow="hidden">
{ accountNavItems.map((item) => <NavLink key={ item.text } { ...item } px="0px"/>) }
</VStack>
</Box>
<Box mt={ 2 } pt={ 3 } borderTopColor={ borderColor } borderTopWidth="1px" { ...getDefaultTransitionProps() }>
<Button size="sm" width="full" variant="secondary">Sign Out</Button>
</Box>
</Box>
);
};
export default ProfileMenuContent;
import { Popover, PopoverContent, PopoverBody, PopoverTrigger, Button } from '@chakra-ui/react';
import React from 'react';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import ProfileMenuContent from 'ui/blocks/profileMenu/ProfileMenuContent';
import UserAvatar from 'ui/shared/UserAvatar';
const ProfileMenuDesktop = () => {
const { data } = useFetchProfileInfo();
return (
<Popover openDelay={ 300 } placement="bottom-end" gutter={ 10 } isLazy>
<PopoverTrigger>
<Button variant="unstyled" display="inline-flex" height="auto">
<UserAvatar size={ 50 } data={ data }/>
</Button>
</PopoverTrigger>
{ data && (
<PopoverContent w="212px">
<PopoverBody padding="24px 16px 16px 16px">
<ProfileMenuContent { ...data }/>
</PopoverBody>
</PopoverContent>
) }
</Popover>
);
};
export default ProfileMenuDesktop;
import { Flex, Box, Drawer, DrawerOverlay, DrawerContent, DrawerBody, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import ColorModeToggler from 'ui/blocks/header/ColorModeToggler';
import ProfileMenuContent from 'ui/blocks/profileMenu/ProfileMenuContent';
import UserAvatar from 'ui/shared/UserAvatar';
const ProfileMenuMobile = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { data } = useFetchProfileInfo();
return (
<>
<Box padding={ 2 } onClick={ onOpen }>
<UserAvatar size={ 24 } data={ data }/>
</Box>
{ data && (
<Drawer
isOpen={ isOpen }
placement="right"
onClose={ onClose }
autoFocus={ false }
>
<DrawerOverlay/>
<DrawerContent maxWidth="260px">
<DrawerBody p={ 6 }>
<Flex
justifyContent="space-between"
alignItems="center"
mb={ 6 }
>
<ColorModeToggler/>
<Box onClick={ onClose }>
<UserAvatar size={ 24 } data={ data }/>
</Box>
</Flex>
<ProfileMenuContent { ...data }/>
</DrawerBody>
</DrawerContent>
</Drawer>
) }
</>
);
};
export default ProfileMenuMobile;
...@@ -36,7 +36,7 @@ type Inputs = { ...@@ -36,7 +36,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255; const NAME_MAX_LENGTH = 255;
const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const { control, formState: { errors }, handleSubmit, setError } = useForm<Inputs>({ const { control, formState: { errors, isValid }, handleSubmit, setError } = useForm<Inputs>({
defaultValues: { defaultValues: {
contract_address_hash: data?.contract_address_hash || '', contract_address_hash: data?.contract_address_hash || '',
name: data?.name || '', name: data?.name || '',
...@@ -102,7 +102,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -102,7 +102,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
return ( return (
<AddressInput<Inputs, 'contract_address_hash'> <AddressInput<Inputs, 'contract_address_hash'>
field={ field } field={ field }
error={ errors.contract_address_hash?.message } error={ errors.contract_address_hash }
backgroundColor={ formBackgroundColor } backgroundColor={ formBackgroundColor }
placeholder="Smart contract address (0x...)" placeholder="Smart contract address (0x...)"
/> />
...@@ -164,7 +164,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -164,7 +164,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
size="lg" size="lg"
variant="primary" variant="primary"
onClick={ handleSubmit(onSubmit) } onClick={ handleSubmit(onSubmit) }
disabled={ Object.keys(errors).length > 0 } disabled={ !isValid }
isLoading={ mutation.isLoading } isLoading={ mutation.isLoading }
> >
{ data ? 'Save' : 'Create custom ABI' } { data ? 'Save' : 'Create custom ABI' }
......
import { VStack, FormControl, FormLabel, Input } from '@chakra-ui/react';
import React from 'react';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import AccountPageHeader from 'ui/shared/AccountPageHeader';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page';
import UserAvatar from 'ui/shared/UserAvatar';
const MyProfile = () => {
const { data, isLoading, isError } = useFetchProfileInfo();
const content = (() => {
if (isLoading) {
return <ContentLoader/>;
}
if (isError) {
return <DataFetchAlert/>;
}
return (
<VStack maxW="412px" mt={ 12 } gap={ 5 } alignItems="stretch">
<UserAvatar size={ 64 } data={ data }/>
<FormControl variant="floating" id="name" isRequired size="lg">
<Input
size="lg"
required
disabled
value={ data.name || '' }
/>
<FormLabel>Name</FormLabel>
</FormControl>
<FormControl variant="floating" id="nickname" isRequired size="lg">
<Input
size="lg"
required
disabled
value={ data.nickname || '' }
/>
<FormLabel>Nickname</FormLabel>
</FormControl>
<FormControl variant="floating" id="email" isRequired size="lg">
<Input
size="lg"
required
disabled
value={ data.email }
/>
<FormLabel>Email</FormLabel>
</FormControl>
</VStack>
);
})();
return (
<Page>
<AccountPageHeader text="My profile"/>
{ content }
</Page>
);
};
export default MyProfile;
...@@ -32,7 +32,7 @@ type Inputs = { ...@@ -32,7 +32,7 @@ type Inputs = {
const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const [ pending, setPending ] = useState(false); const [ pending, setPending ] = useState(false);
const { control, handleSubmit, formState: { errors }, setError } = useForm<Inputs>({ const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
mode: 'all', mode: 'all',
defaultValues: { defaultValues: {
address: data?.address_hash || '', address: data?.address_hash || '',
...@@ -83,11 +83,11 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -83,11 +83,11 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
}; };
const renderAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'address'>}) => { const renderAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'address'>}) => {
return <AddressInput<Inputs, 'address'> field={ field } error={ errors.address?.message } backgroundColor={ formBackgroundColor }/>; return <AddressInput<Inputs, 'address'> field={ field } error={ errors.address } backgroundColor={ formBackgroundColor }/>;
}, [ errors, formBackgroundColor ]); }, [ errors, formBackgroundColor ]);
const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => { const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => {
return <TagInput<Inputs, 'tag'> field={ field } error={ errors.tag?.message } backgroundColor={ formBackgroundColor }/>; return <TagInput<Inputs, 'tag'> field={ field } error={ errors.tag } backgroundColor={ formBackgroundColor }/>;
}, [ errors, formBackgroundColor ]); }, [ errors, formBackgroundColor ]);
return ( return (
...@@ -117,7 +117,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -117,7 +117,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
size="lg" size="lg"
variant="primary" variant="primary"
onClick={ handleSubmit(onSubmit) } onClick={ handleSubmit(onSubmit) }
disabled={ Object.keys(errors).length > 0 } disabled={ !isValid }
isLoading={ pending } isLoading={ pending }
> >
{ data ? 'Save changes' : 'Add tag' } { data ? 'Save changes' : 'Add tag' }
......
...@@ -34,7 +34,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => ...@@ -34,7 +34,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
const [ pending, setPending ] = useState(false); const [ pending, setPending ] = useState(false);
const formBackgroundColor = useColorModeValue('white', 'gray.900'); const formBackgroundColor = useColorModeValue('white', 'gray.900');
const { control, handleSubmit, formState: { errors }, setError } = useForm<Inputs>({ const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
mode: 'all', mode: 'all',
defaultValues: { defaultValues: {
transaction: data?.transaction_hash || '', transaction: data?.transaction_hash || '',
...@@ -82,11 +82,11 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => ...@@ -82,11 +82,11 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
}; };
const renderTransactionInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'transaction'>}) => { const renderTransactionInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'transaction'>}) => {
return <TransactionInput field={ field } error={ errors.transaction?.message } backgroundColor={ formBackgroundColor }/>; return <TransactionInput field={ field } error={ errors.transaction } backgroundColor={ formBackgroundColor }/>;
}, [ errors, formBackgroundColor ]); }, [ errors, formBackgroundColor ]);
const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => { const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => {
return <TagInput<Inputs, 'tag'> field={ field } error={ errors.tag?.message } backgroundColor={ formBackgroundColor }/>; return <TagInput<Inputs, 'tag'> field={ field } error={ errors.tag } backgroundColor={ formBackgroundColor }/>;
}, [ errors, formBackgroundColor ]); }, [ errors, formBackgroundColor ]);
return ( return (
...@@ -116,7 +116,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => ...@@ -116,7 +116,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
size="lg" size="lg"
variant="primary" variant="primary"
onClick={ handleSubmit(onSubmit) } onClick={ handleSubmit(onSubmit) }
disabled={ Object.keys(errors).length > 0 } disabled={ !isValid }
isLoading={ pending } isLoading={ pending }
> >
{ data ? 'Save changes' : 'Add tag' } { data ? 'Save changes' : 'Add tag' }
......
import { IconButton, Icon } from '@chakra-ui/react'; import { IconButton, Icon } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { ControllerRenderProps, Control } from 'react-hook-form'; import type { ControllerRenderProps, Control, FieldError } from 'react-hook-form';
import { Controller } from 'react-hook-form'; import { Controller } from 'react-hook-form';
import MinusIcon from 'icons/minus.svg'; import MinusIcon from 'icons/minus.svg';
...@@ -14,7 +14,7 @@ interface Props { ...@@ -14,7 +14,7 @@ interface Props {
control: Control<Inputs>; control: Control<Inputs>;
index: number; index: number;
fieldsLength: number; fieldsLength: number;
error?: string; error?: FieldError;
onAddFieldClick: (e: React.SyntheticEvent) => void; onAddFieldClick: (e: React.SyntheticEvent) => void;
onRemoveFieldClick: (index: number) => (e: React.SyntheticEvent) => void; onRemoveFieldClick: (index: number) => (e: React.SyntheticEvent) => void;
} }
......
import { FormControl, FormLabel, Textarea } from '@chakra-ui/react'; import { FormControl, FormLabel, Textarea } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { ControllerRenderProps, Control } from 'react-hook-form'; import type { ControllerRenderProps, Control, FieldError } from 'react-hook-form';
import { Controller } from 'react-hook-form'; import { Controller } from 'react-hook-form';
import getPlaceholderWithError from 'lib/getPlaceholderWithError'; import getPlaceholderWithError from 'lib/getPlaceholderWithError';
...@@ -11,7 +11,7 @@ const TEXT_INPUT_MAX_LENGTH = 255; ...@@ -11,7 +11,7 @@ const TEXT_INPUT_MAX_LENGTH = 255;
interface Props { interface Props {
control: Control<Inputs>; control: Control<Inputs>;
error?: string; error?: FieldError;
} }
export default function PublicTagFormComment({ control, error }: Props) { export default function PublicTagFormComment({ control, error }: Props) {
...@@ -24,7 +24,7 @@ export default function PublicTagFormComment({ control, error }: Props) { ...@@ -24,7 +24,7 @@ export default function PublicTagFormComment({ control, error }: Props) {
size="lg" size="lg"
/> />
<FormLabel> <FormLabel>
{ getPlaceholderWithError('Specify the reason for adding tags and color preference(s)', error) } { getPlaceholderWithError('Specify the reason for adding tags and color preference(s)', error?.message) }
</FormLabel> </FormLabel>
</FormControl> </FormControl>
); );
......
...@@ -8,7 +8,7 @@ import { ...@@ -8,7 +8,7 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { Path, SubmitHandler } from 'react-hook-form'; import type { FieldError, Path, SubmitHandler } from 'react-hook-form';
import { useForm, useFieldArray } from 'react-hook-form'; import { useForm, useFieldArray } from 'react-hook-form';
import type { PublicTags, PublicTag, PublicTagNew, PublicTagErrors } from 'types/api/account'; import type { PublicTags, PublicTag, PublicTagNew, PublicTagErrors } from 'types/api/account';
...@@ -57,7 +57,7 @@ const ADDRESS_INPUT_BUTTONS_WIDTH = 100; ...@@ -57,7 +57,7 @@ const ADDRESS_INPUT_BUTTONS_WIDTH = 100;
const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { control, handleSubmit, formState: { errors }, setError } = useForm<Inputs>({ const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
defaultValues: { defaultValues: {
fullName: data?.full_name || '', fullName: data?.full_name || '',
email: data?.email || '', email: data?.email || '',
...@@ -158,7 +158,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -158,7 +158,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
fieldName="fullName" fieldName="fullName"
control={ control } control={ control }
label={ placeholders.fullName } label={ placeholders.fullName }
error={ errors.fullName?.message } error={ errors.fullName }
required required
/> />
</GridItem> </GridItem>
...@@ -167,7 +167,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -167,7 +167,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
fieldName="companyName" fieldName="companyName"
control={ control } control={ control }
label={ placeholders.companyName } label={ placeholders.companyName }
error={ errors.companyName?.message } error={ errors.companyName }
/> />
</GridItem> </GridItem>
<GridItem> <GridItem>
...@@ -176,7 +176,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -176,7 +176,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
control={ control } control={ control }
label={ placeholders.email } label={ placeholders.email }
pattern={ EMAIL_REGEXP } pattern={ EMAIL_REGEXP }
error={ errors.email?.message } error={ errors.email }
required required
/> />
</GridItem> </GridItem>
...@@ -185,7 +185,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -185,7 +185,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
fieldName="companyUrl" fieldName="companyUrl"
control={ control } control={ control }
label={ placeholders.companyUrl } label={ placeholders.companyUrl }
error={ errors?.companyUrl?.message } error={ errors?.companyUrl }
/> />
</GridItem> </GridItem>
</Grid> </Grid>
...@@ -198,7 +198,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -198,7 +198,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
fieldName="tags" fieldName="tags"
control={ control } control={ control }
label={ placeholders.tags } label={ placeholders.tags }
error={ errors.tags?.message } error={ errors.tags }
required/> required/>
</Box> </Box>
{ fields.map((field, index) => { { fields.map((field, index) => {
...@@ -206,7 +206,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -206,7 +206,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
<Box position="relative" key={ field.id } marginBottom={ 4 }> <Box position="relative" key={ field.id } marginBottom={ 4 }>
<PublicTagFormAddressInput <PublicTagFormAddressInput
control={ control } control={ control }
error={ errors?.addresses?.[index]?.message } error={ errors?.addresses?.[index] as FieldError }
index={ index } index={ index }
fieldsLength={ fields.length } fieldsLength={ fields.length }
onAddFieldClick={ onAddFieldClick } onAddFieldClick={ onAddFieldClick }
...@@ -216,14 +216,14 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -216,14 +216,14 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
); );
}) } }) }
<Box marginBottom={ 8 }> <Box marginBottom={ 8 }>
<PublicTagFormComment control={ control } error={ errors.comment?.message }/> <PublicTagFormComment control={ control } error={ errors.comment }/>
</Box> </Box>
<HStack spacing={ 6 }> <HStack spacing={ 6 }>
<Button <Button
size="lg" size="lg"
variant="primary" variant="primary"
onClick={ handleSubmit(onSubmit) } onClick={ handleSubmit(onSubmit) }
disabled={ Object.keys(errors).length > 0 } disabled={ !isValid }
isLoading={ mutation.isLoading } isLoading={ mutation.isLoading }
> >
Send request Send request
......
import { FormControl, FormLabel, Input } from '@chakra-ui/react'; import { FormControl, FormLabel, Input } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { ControllerRenderProps, FieldValues, Path, Control } from 'react-hook-form'; import type { ControllerRenderProps, FieldError, FieldValues, Path, Control } from 'react-hook-form';
import { Controller } from 'react-hook-form'; import { Controller } from 'react-hook-form';
import getPlaceholderWithError from 'lib/getPlaceholderWithError'; import getPlaceholderWithError from 'lib/getPlaceholderWithError';
...@@ -13,7 +13,7 @@ interface Props<TInputs extends FieldValues> { ...@@ -13,7 +13,7 @@ interface Props<TInputs extends FieldValues> {
required?: boolean; required?: boolean;
control: Control<TInputs, object>; control: Control<TInputs, object>;
pattern?: RegExp; pattern?: RegExp;
error?: string; error?: FieldError;
} }
export default function PublicTagsFormInput<Inputs extends FieldValues>({ export default function PublicTagsFormInput<Inputs extends FieldValues>({
...@@ -34,7 +34,7 @@ export default function PublicTagsFormInput<Inputs extends FieldValues>({ ...@@ -34,7 +34,7 @@ export default function PublicTagsFormInput<Inputs extends FieldValues>({
isInvalid={ Boolean(error) } isInvalid={ Boolean(error) }
maxLength={ TEXT_INPUT_MAX_LENGTH } maxLength={ TEXT_INPUT_MAX_LENGTH }
/> />
<FormLabel>{ getPlaceholderWithError(label, error) }</FormLabel> <FormLabel>{ getPlaceholderWithError(label, error?.message) }</FormLabel>
</FormControl> </FormControl>
); );
}, [ label, required, error ]); }, [ label, required, error ]);
......
...@@ -4,7 +4,7 @@ import { ...@@ -4,7 +4,7 @@ import {
FormLabel, FormLabel,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps, FieldValues, Path } from 'react-hook-form'; import type { ControllerRenderProps, FieldError, FieldValues, Path } from 'react-hook-form';
import getPlaceholderWithError from 'lib/getPlaceholderWithError'; import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import { ADDRESS_LENGTH } from 'lib/validations/address'; import { ADDRESS_LENGTH } from 'lib/validations/address';
...@@ -14,7 +14,7 @@ type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = { ...@@ -14,7 +14,7 @@ type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = {
size?: string; size?: string;
placeholder?: string; placeholder?: string;
backgroundColor?: string; backgroundColor?: string;
error?: string; error?: FieldError;
} }
export default function AddressInput<Inputs extends FieldValues, Name extends Path<Inputs>>( export default function AddressInput<Inputs extends FieldValues, Name extends Path<Inputs>>(
...@@ -33,7 +33,7 @@ export default function AddressInput<Inputs extends FieldValues, Name extends Pa ...@@ -33,7 +33,7 @@ export default function AddressInput<Inputs extends FieldValues, Name extends Pa
maxLength={ ADDRESS_LENGTH } maxLength={ ADDRESS_LENGTH }
size={ size } size={ size }
/> />
<FormLabel>{ getPlaceholderWithError(placeholder, error) }</FormLabel> <FormLabel>{ getPlaceholderWithError(placeholder, error?.message) }</FormLabel>
</FormControl> </FormControl>
); );
} }
...@@ -3,7 +3,7 @@ import React from 'react'; ...@@ -3,7 +3,7 @@ import React from 'react';
const DataFetchAlert = () => { const DataFetchAlert = () => {
return ( return (
<Alert status="warning" as="span"> <Alert status="warning" width="fit-content">
<AlertDescription> <AlertDescription>
Something went wrong. Try refreshing the page or come back later. Something went wrong. Try refreshing the page or come back later.
</AlertDescription> </AlertDescription>
......
...@@ -4,7 +4,7 @@ import { ...@@ -4,7 +4,7 @@ import {
FormLabel, FormLabel,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps, FieldValues, Path } from 'react-hook-form'; import type { ControllerRenderProps, FieldError, FieldValues, Path } from 'react-hook-form';
import getPlaceholderWithError from 'lib/getPlaceholderWithError'; import getPlaceholderWithError from 'lib/getPlaceholderWithError';
...@@ -12,7 +12,7 @@ const TAG_MAX_LENGTH = 35; ...@@ -12,7 +12,7 @@ const TAG_MAX_LENGTH = 35;
type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = { type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = {
field: ControllerRenderProps<TInputs, TInputName>; field: ControllerRenderProps<TInputs, TInputName>;
error?: string; error?: FieldError;
backgroundColor?: string; backgroundColor?: string;
} }
...@@ -24,7 +24,7 @@ function TagInput<Inputs extends FieldValues, Name extends Path<Inputs>>({ field ...@@ -24,7 +24,7 @@ function TagInput<Inputs extends FieldValues, Name extends Path<Inputs>>({ field
isInvalid={ Boolean(error) } isInvalid={ Boolean(error) }
maxLength={ TAG_MAX_LENGTH } maxLength={ TAG_MAX_LENGTH }
/> />
<FormLabel>{ getPlaceholderWithError(`Private tag (max 35 characters)`, error) }</FormLabel> <FormLabel>{ getPlaceholderWithError(`Private tag (max 35 characters)`, error?.message) }</FormLabel>
</FormControl> </FormControl>
); );
} }
......
...@@ -4,14 +4,14 @@ import { ...@@ -4,14 +4,14 @@ import {
FormLabel, FormLabel,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps, FieldValues } from 'react-hook-form'; import type { ControllerRenderProps, FieldError, FieldValues } from 'react-hook-form';
import getPlaceholderWithError from 'lib/getPlaceholderWithError'; import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import { TRANSACTION_HASH_LENGTH } from 'lib/validations/transaction'; import { TRANSACTION_HASH_LENGTH } from 'lib/validations/transaction';
type Props<Field> = { type Props<Field> = {
field: Field; field: Field;
error?: string; error?: FieldError;
backgroundColor?: string; backgroundColor?: string;
} }
...@@ -23,7 +23,7 @@ function TransactionInput<Field extends Partial<ControllerRenderProps<FieldValue ...@@ -23,7 +23,7 @@ function TransactionInput<Field extends Partial<ControllerRenderProps<FieldValue
isInvalid={ Boolean(error) } isInvalid={ Boolean(error) }
maxLength={ TRANSACTION_HASH_LENGTH } maxLength={ TRANSACTION_HASH_LENGTH }
/> />
<FormLabel>{ getPlaceholderWithError('Transaction hash (0x...)', error) }</FormLabel> <FormLabel>{ getPlaceholderWithError('Transaction hash (0x...)', error?.message) }</FormLabel>
</FormControl> </FormControl>
); );
} }
......
import { useColorModeValue, chakra, Image } from '@chakra-ui/react';
import React from 'react';
import Identicon from 'react-identicons';
import type { UserInfo } from 'types/api/account';
const ProfileIcon = chakra(Identicon);
interface Props {
size: number;
data?: UserInfo;
}
const UserAvatar = ({ size, data }: Props) => {
const sizeString = `${ size }px`;
const bgColor = useColorModeValue('blackAlpha.100', 'white');
if (data?.avatar) {
return (
<Image
flexShrink={ 0 }
src={ data.avatar }
alt={ `Profile picture of ${ data.name || data.nickname || '' }` }
w={ sizeString }
minW={ sizeString }
h={ sizeString }
minH={ sizeString }
borderRadius="full"
overflow="hidden"
/>
);
}
return (
<ProfileIcon
flexShrink={ 0 }
maxWidth={ sizeString }
maxHeight={ sizeString }
string={ data?.email || 'randomness' }
// the displayed size is doubled for retina displays
size={ size * 2 }
bg={ bgColor }
borderRadius="full"
overflow="hidden"
/>
);
};
export default React.memo(UserAvatar);
...@@ -72,7 +72,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -72,7 +72,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
notificationsDefault = data.notification_settings; notificationsDefault = data.notification_settings;
} }
const { control, handleSubmit, formState: { errors }, setError } = useForm<Inputs>({ const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
defaultValues: { defaultValues: {
address: data?.address_hash || '', address: data?.address_hash || '',
tag: data?.name || '', tag: data?.name || '',
...@@ -134,13 +134,13 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -134,13 +134,13 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<AddressInput<Inputs, 'address'> <AddressInput<Inputs, 'address'>
field={ field } field={ field }
backgroundColor={ formBackgroundColor } backgroundColor={ formBackgroundColor }
error={ errors.address?.message } error={ errors.address }
/> />
); );
}, [ errors, formBackgroundColor ]); }, [ errors, formBackgroundColor ]);
const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => { const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => {
return <TagInput<Inputs, 'tag'> field={ field } error={ errors.tag?.message } backgroundColor={ formBackgroundColor }/>; return <TagInput<Inputs, 'tag'> field={ field } error={ errors.tag } backgroundColor={ formBackgroundColor }/>;
}, [ errors, formBackgroundColor ]); }, [ errors, formBackgroundColor ]);
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
...@@ -156,6 +156,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -156,6 +156,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
control={ control } control={ control }
rules={{ rules={{
pattern: ADDRESS_REGEXP, pattern: ADDRESS_REGEXP,
required: true,
}} }}
render={ renderAddressInput } render={ renderAddressInput }
/> />
...@@ -166,6 +167,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -166,6 +167,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
control={ control } control={ control }
rules={{ rules={{
maxLength: TAG_MAX_LENGTH, maxLength: TAG_MAX_LENGTH,
required: true,
}} }}
render={ renderTagInput } render={ renderTagInput }
/> />
...@@ -188,7 +190,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -188,7 +190,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
variant="primary" variant="primary"
onClick={ handleSubmit(onSubmit) } onClick={ handleSubmit(onSubmit) }
isLoading={ pending } isLoading={ pending }
disabled={ Object.keys(errors).length > 0 } disabled={ !isValid }
> >
{ data ? 'Save changes' : 'Add address' } { data ? 'Save changes' : 'Add address' }
</Button> </Button>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment