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';
import blocksIcon from 'icons/block.svg';
import gearIcon from 'icons/gear.svg';
import privateTagIcon from 'icons/privattags.svg';
import profileIcon from 'icons/profile.svg';
import publicTagIcon from 'icons/publictags.svg';
import tokensIcon from 'icons/token.svg';
import transactionsIcon from 'icons/transactions.svg';
......@@ -32,6 +33,8 @@ export default function useNavItems() {
{ 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 ]);
}
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,10 +17,11 @@ const variantPrimary = {
},
};
const variantSecondary = {
color: 'blue.600',
const variantSecondary: SystemStyleFunction = (props) => {
return {
color: mode('blue.600', 'blue.300')(props),
fontWeight: 600,
borderColor: 'blue.600',
borderColor: mode('blue.600', 'blue.300')(props),
border: '2px solid',
_hover: {
color: 'blue.400',
......@@ -29,6 +30,7 @@ const variantSecondary = {
_disabled: {
opacity: 0.2,
},
};
};
const variantIcon: SystemStyleFunction = (props) => {
......
......@@ -2,7 +2,11 @@ import type { drawerAnatomy as parts } from '@chakra-ui/anatomy';
import type { SystemStyleFunction, PartsStyleFunction, SystemStyleObject } from '@chakra-ui/theme-tools';
import { mode } from '@chakra-ui/theme-tools';
import getDefaultTransitionProps from '../utils/getDefaultTransitionProps';
const transitionProps = getDefaultTransitionProps();
const baseStyleOverlay: SystemStyleObject = {
...transitionProps,
bg: 'blackAlpha.800',
zIndex: 'overlay',
};
......@@ -12,6 +16,7 @@ const baseStyleDialog: SystemStyleFunction = (props) => {
return {
...(isFullHeight && { height: '100vh' }),
...transitionProps,
zIndex: 'modal',
maxH: '100vh',
bg: mode('white', 'gray.900')(props),
......
......@@ -51,7 +51,8 @@ export type Transactions = Array<Transaction>
export interface UserInfo {
name?: string;
nickname?: string;
email?: string;
email: string;
avatar?: string;
}
export interface WatchlistAddress {
......
......@@ -32,7 +32,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255;
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',
defaultValues: {
token: data?.api_key || '',
......@@ -143,7 +143,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
size="lg"
variant="primary"
onClick={ handleSubmit(onSubmit) }
disabled={ Object.keys(errors).length > 0 }
disabled={ !isValid }
isLoading={ mutation.isLoading }
>
{ data ? 'Save' : 'Generate API key' }
......
......@@ -3,11 +3,12 @@ import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
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 Burger from './Burger';
import ColorModeToggler from './ColorModeToggler';
import ProfileMenu from './ProfileMenu';
const Header = () => {
const isMobile = useIsMobile();
......@@ -31,7 +32,7 @@ const Header = () => {
>
<Burger/>
<NetworkLogo/>
<ProfileMenu/>
<ProfileMenuMobile/>
</Flex>
<SearchBar/>
</Box>
......@@ -48,7 +49,7 @@ const Header = () => {
>
<SearchBar/>
<ColorModeToggler/>
<ProfileMenu/>
<ProfileMenuDesktop/>
</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';
interface Props {
isCollapsed?: boolean;
isActive: boolean;
isActive?: boolean;
pathname: string;
text: string;
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 isMobile = useIsMobile();
const width = (() => {
......@@ -32,7 +33,7 @@ const NavLink = ({ text, pathname, icon, isCollapsed, isActive }: Props) => {
as="li"
listStyleType="none"
w={ width }
px={ isCollapsed ? '15px' : 3 }
px={ px || (isCollapsed ? '15px' : 3) }
py={ 2.5 }
color={ isActive ? colors.text.active : colors.text.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 = {
const NAME_MAX_LENGTH = 255;
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: {
contract_address_hash: data?.contract_address_hash || '',
name: data?.name || '',
......@@ -102,7 +102,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
return (
<AddressInput<Inputs, 'contract_address_hash'>
field={ field }
error={ errors.contract_address_hash?.message }
error={ errors.contract_address_hash }
backgroundColor={ formBackgroundColor }
placeholder="Smart contract address (0x...)"
/>
......@@ -164,7 +164,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
size="lg"
variant="primary"
onClick={ handleSubmit(onSubmit) }
disabled={ Object.keys(errors).length > 0 }
disabled={ !isValid }
isLoading={ mutation.isLoading }
>
{ 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 = {
const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const [ pending, setPending ] = useState(false);
const { control, handleSubmit, formState: { errors }, setError } = useForm<Inputs>({
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
mode: 'all',
defaultValues: {
address: data?.address_hash || '',
......@@ -83,11 +83,11 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
};
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 ]);
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 ]);
return (
......@@ -117,7 +117,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
size="lg"
variant="primary"
onClick={ handleSubmit(onSubmit) }
disabled={ Object.keys(errors).length > 0 }
disabled={ !isValid }
isLoading={ pending }
>
{ data ? 'Save changes' : 'Add tag' }
......
......@@ -34,7 +34,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
const [ pending, setPending ] = useState(false);
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',
defaultValues: {
transaction: data?.transaction_hash || '',
......@@ -82,11 +82,11 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
};
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 ]);
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 ]);
return (
......@@ -116,7 +116,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
size="lg"
variant="primary"
onClick={ handleSubmit(onSubmit) }
disabled={ Object.keys(errors).length > 0 }
disabled={ !isValid }
isLoading={ pending }
>
{ data ? 'Save changes' : 'Add tag' }
......
import { IconButton, Icon } from '@chakra-ui/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 MinusIcon from 'icons/minus.svg';
......@@ -14,7 +14,7 @@ interface Props {
control: Control<Inputs>;
index: number;
fieldsLength: number;
error?: string;
error?: FieldError;
onAddFieldClick: (e: React.SyntheticEvent) => void;
onRemoveFieldClick: (index: number) => (e: React.SyntheticEvent) => void;
}
......
import { FormControl, FormLabel, Textarea } from '@chakra-ui/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 getPlaceholderWithError from 'lib/getPlaceholderWithError';
......@@ -11,7 +11,7 @@ const TEXT_INPUT_MAX_LENGTH = 255;
interface Props {
control: Control<Inputs>;
error?: string;
error?: FieldError;
}
export default function PublicTagFormComment({ control, error }: Props) {
......@@ -24,7 +24,7 @@ export default function PublicTagFormComment({ control, error }: Props) {
size="lg"
/>
<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>
</FormControl>
);
......
......@@ -8,7 +8,7 @@ import {
} from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
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 type { PublicTags, PublicTag, PublicTagNew, PublicTagErrors } from 'types/api/account';
......@@ -57,7 +57,7 @@ const ADDRESS_INPUT_BUTTONS_WIDTH = 100;
const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
const queryClient = useQueryClient();
const { control, handleSubmit, formState: { errors }, setError } = useForm<Inputs>({
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
defaultValues: {
fullName: data?.full_name || '',
email: data?.email || '',
......@@ -158,7 +158,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
fieldName="fullName"
control={ control }
label={ placeholders.fullName }
error={ errors.fullName?.message }
error={ errors.fullName }
required
/>
</GridItem>
......@@ -167,7 +167,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
fieldName="companyName"
control={ control }
label={ placeholders.companyName }
error={ errors.companyName?.message }
error={ errors.companyName }
/>
</GridItem>
<GridItem>
......@@ -176,7 +176,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
control={ control }
label={ placeholders.email }
pattern={ EMAIL_REGEXP }
error={ errors.email?.message }
error={ errors.email }
required
/>
</GridItem>
......@@ -185,7 +185,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
fieldName="companyUrl"
control={ control }
label={ placeholders.companyUrl }
error={ errors?.companyUrl?.message }
error={ errors?.companyUrl }
/>
</GridItem>
</Grid>
......@@ -198,7 +198,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
fieldName="tags"
control={ control }
label={ placeholders.tags }
error={ errors.tags?.message }
error={ errors.tags }
required/>
</Box>
{ fields.map((field, index) => {
......@@ -206,7 +206,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
<Box position="relative" key={ field.id } marginBottom={ 4 }>
<PublicTagFormAddressInput
control={ control }
error={ errors?.addresses?.[index]?.message }
error={ errors?.addresses?.[index] as FieldError }
index={ index }
fieldsLength={ fields.length }
onAddFieldClick={ onAddFieldClick }
......@@ -216,14 +216,14 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
);
}) }
<Box marginBottom={ 8 }>
<PublicTagFormComment control={ control } error={ errors.comment?.message }/>
<PublicTagFormComment control={ control } error={ errors.comment }/>
</Box>
<HStack spacing={ 6 }>
<Button
size="lg"
variant="primary"
onClick={ handleSubmit(onSubmit) }
disabled={ Object.keys(errors).length > 0 }
disabled={ !isValid }
isLoading={ mutation.isLoading }
>
Send request
......
import { FormControl, FormLabel, Input } from '@chakra-ui/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 getPlaceholderWithError from 'lib/getPlaceholderWithError';
......@@ -13,7 +13,7 @@ interface Props<TInputs extends FieldValues> {
required?: boolean;
control: Control<TInputs, object>;
pattern?: RegExp;
error?: string;
error?: FieldError;
}
export default function PublicTagsFormInput<Inputs extends FieldValues>({
......@@ -34,7 +34,7 @@ export default function PublicTagsFormInput<Inputs extends FieldValues>({
isInvalid={ Boolean(error) }
maxLength={ TEXT_INPUT_MAX_LENGTH }
/>
<FormLabel>{ getPlaceholderWithError(label, error) }</FormLabel>
<FormLabel>{ getPlaceholderWithError(label, error?.message) }</FormLabel>
</FormControl>
);
}, [ label, required, error ]);
......
......@@ -4,7 +4,7 @@ import {
FormLabel,
} from '@chakra-ui/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 { ADDRESS_LENGTH } from 'lib/validations/address';
......@@ -14,7 +14,7 @@ type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = {
size?: string;
placeholder?: string;
backgroundColor?: string;
error?: string;
error?: FieldError;
}
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
maxLength={ ADDRESS_LENGTH }
size={ size }
/>
<FormLabel>{ getPlaceholderWithError(placeholder, error) }</FormLabel>
<FormLabel>{ getPlaceholderWithError(placeholder, error?.message) }</FormLabel>
</FormControl>
);
}
......@@ -3,7 +3,7 @@ import React from 'react';
const DataFetchAlert = () => {
return (
<Alert status="warning" as="span">
<Alert status="warning" width="fit-content">
<AlertDescription>
Something went wrong. Try refreshing the page or come back later.
</AlertDescription>
......
......@@ -4,7 +4,7 @@ import {
FormLabel,
} from '@chakra-ui/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';
......@@ -12,7 +12,7 @@ const TAG_MAX_LENGTH = 35;
type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = {
field: ControllerRenderProps<TInputs, TInputName>;
error?: string;
error?: FieldError;
backgroundColor?: string;
}
......@@ -24,7 +24,7 @@ function TagInput<Inputs extends FieldValues, Name extends Path<Inputs>>({ field
isInvalid={ Boolean(error) }
maxLength={ TAG_MAX_LENGTH }
/>
<FormLabel>{ getPlaceholderWithError(`Private tag (max 35 characters)`, error) }</FormLabel>
<FormLabel>{ getPlaceholderWithError(`Private tag (max 35 characters)`, error?.message) }</FormLabel>
</FormControl>
);
}
......
......@@ -4,14 +4,14 @@ import {
FormLabel,
} from '@chakra-ui/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 { TRANSACTION_HASH_LENGTH } from 'lib/validations/transaction';
type Props<Field> = {
field: Field;
error?: string;
error?: FieldError;
backgroundColor?: string;
}
......@@ -23,7 +23,7 @@ function TransactionInput<Field extends Partial<ControllerRenderProps<FieldValue
isInvalid={ Boolean(error) }
maxLength={ TRANSACTION_HASH_LENGTH }
/>
<FormLabel>{ getPlaceholderWithError('Transaction hash (0x...)', error) }</FormLabel>
<FormLabel>{ getPlaceholderWithError('Transaction hash (0x...)', error?.message) }</FormLabel>
</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 }) => {
notificationsDefault = data.notification_settings;
}
const { control, handleSubmit, formState: { errors }, setError } = useForm<Inputs>({
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
defaultValues: {
address: data?.address_hash || '',
tag: data?.name || '',
......@@ -134,13 +134,13 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<AddressInput<Inputs, 'address'>
field={ field }
backgroundColor={ formBackgroundColor }
error={ errors.address?.message }
error={ errors.address }
/>
);
}, [ errors, formBackgroundColor ]);
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 ]);
// eslint-disable-next-line react/display-name
......@@ -156,6 +156,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
control={ control }
rules={{
pattern: ADDRESS_REGEXP,
required: true,
}}
render={ renderAddressInput }
/>
......@@ -166,6 +167,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
control={ control }
rules={{
maxLength: TAG_MAX_LENGTH,
required: true,
}}
render={ renderTagInput }
/>
......@@ -188,7 +190,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
variant="primary"
onClick={ handleSubmit(onSubmit) }
isLoading={ pending }
disabled={ Object.keys(errors).length > 0 }
disabled={ !isValid }
>
{ data ? 'Save changes' : 'Add address' }
</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