Commit b8789b81 authored by tom's avatar tom

token info form

parent 62d5b8e4
......@@ -20,12 +20,12 @@ export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
const { label, children, helperText, errorText, optionalText, ...rest } = props;
// A floating field cannot be without a label.
if (props.floating && label) {
if (rest.floating && label) {
const injectedProps = {
className: 'peer',
placeholder: ' ',
size: props.size,
floating: props.floating,
size: rest.size,
floating: rest.floating,
bgColor: rest.bgColor,
disabled: rest.disabled,
readOnly: rest.readOnly,
......@@ -79,6 +79,13 @@ export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
);
}
// Pass size value to the input component
const injectedProps = {
size: rest.size,
};
const child = React.Children.only<React.ReactElement<InputProps | InputGroupProps>>(children);
const clonedChild = React.cloneElement(child, injectedProps);
return (
<ChakraField.Root ref={ ref } { ...rest }>
{ label && (
......@@ -87,7 +94,7 @@ export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
<ChakraField.RequiredIndicator fallback={ optionalText }/>
</ChakraField.Label>
) }
{ children }
{ clonedChild }
{ helperText && (
<ChakraField.HelperText>{ helperText }</ChakraField.HelperText>
) }
......
......@@ -117,6 +117,9 @@ export const recipe = defineSlotRecipe({
floating: true,
css: {
label: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
padding: '10px 16px 0px 16px',
textStyle: 'xs',
_peerPlaceholderShown: {
......@@ -142,6 +145,9 @@ export const recipe = defineSlotRecipe({
floating: true,
css: {
label: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
// 16px = scrollbar width
width: 'calc(100% - 4px - 20px)',
padding: '20px 24px 0px 24px',
......
......@@ -12,7 +12,6 @@ import getQueryParamString from 'lib/router/getQueryParamString';
import { TOKEN_INFO_APPLICATION, VERIFIED_ADDRESS } from 'stubs/account';
import { Button } from 'toolkit/chakra/button';
import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import AddressVerificationModal from 'ui/addressVerification/AddressVerificationModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
......@@ -144,12 +143,12 @@ const VerifiedAddresses = () => {
return (
<>
<PageTitle title="Token info application form" backLink={ backLink }/>
{ /* <TokenInfoForm
<TokenInfoForm
address={ selectedAddress }
tokenName={ tokenName }
application={ applicationsQuery.data?.submissions.find(({ tokenAddress }) => tokenAddress.toLowerCase() === selectedAddress.toLowerCase()) }
onSubmit={ handleApplicationSubmit }
/> */ }
/>
</>
);
}
......
import type { ColorMode } from '@chakra-ui/react';
import { Image, chakra, DarkMode } from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react';
import React from 'react';
import Skeleton from 'ui/shared/chakra/Skeleton';
import type { ColorMode } from 'toolkit/chakra/color-mode';
import { Image } from 'toolkit/chakra/image';
import { Skeleton } from 'toolkit/chakra/skeleton';
interface Props {
src: string | undefined;
......@@ -23,11 +24,11 @@ const ImageUrlPreview = ({
fallback: fallbackProp,
colorMode,
}: Props) => {
const skeleton = <Skeleton className={ className } w="100%" h="100%"/>;
const skeleton = <Skeleton loading className={ [ className, colorMode === 'dark' ? 'dark' : undefined ].filter(Boolean).join(' ') } w="100%" h="100%"/>;
const fallback = (() => {
if (src && !isInvalid) {
return colorMode === 'dark' ? <DarkMode>{ skeleton }</DarkMode> : skeleton;
return skeleton;
}
return fallbackProp;
})();
......
......@@ -13,6 +13,7 @@ import FancySelect from 'ui/shared/forms/inputs/select/FancySelect';
// this type only works for plain objects, not for nested objects or arrays (e.g. ui/publicTags/submit/types.ts:FormFields)
// type SelectField<O> = { [K in keyof O]: NonNullable<O[K]> extends Option ? K : never }[keyof O];
// TODO @tom2drum remove this component
type Props<
FormFields extends FieldValues,
Name extends Path<FormFields>,
......
import React from 'react';
import type { Path, FieldValues } from 'react-hook-form';
import { useController, useFormContext } from 'react-hook-form';
import type { FormFieldPropsBase } from './types';
import type { SelectRootProps } from 'toolkit/chakra/select';
import { SelectContent, SelectControl, SelectItem, SelectRoot, SelectValueText } from 'toolkit/chakra/select';
type Props<
FormFields extends FieldValues,
Name extends Path<FormFields>,
> = FormFieldPropsBase<FormFields, Name> & SelectRootProps;
const FormFieldSelect = <
FormFields extends FieldValues,
Name extends Path<FormFields>,
>(props: Props<FormFields, Name>) => {
const { name, rules, collection, placeholder, ...rest } = props;
const { control } = useFormContext<FormFields>();
const { field, fieldState, formState } = useController<FormFields, typeof name>({
control,
name,
rules,
});
const isDisabled = formState.isSubmitting;
const handleChange = React.useCallback(({ value }: { value: Array<string> }) => {
field.onChange(value);
}, [ field ]);
const handleBlur = React.useCallback(() => {
field.onBlur();
}, [ field ]);
// TODO @tom2drum: fix initial value is not displayed in the select
return (
<SelectRoot
ref={ field.ref }
name={ field.name }
value={ field.value }
onValueChange={ handleChange }
onInteractOutside={ handleBlur }
collection={ collection }
disabled={ isDisabled }
invalid={ Boolean(fieldState.error) }
{ ...rest }
>
<SelectControl>
<SelectValueText placeholder={ placeholder }/>
</SelectControl>
<SelectContent>
{ collection.items.map((item) => (
<SelectItem item={ item } key={ item.value }>
{ item.label }
</SelectItem>
)) }
</SelectContent>
</SelectRoot>
);
};
export default React.memo(FormFieldSelect) as typeof FormFieldSelect;
......@@ -15,32 +15,32 @@ const FieldShowcase = () => {
{ ([ 'sm', 'md', 'lg' ] as const).map((size) => (
<Sample label={ `size: ${ size }` } w="100%" key={ size } alignItems="flex-start">
<Field label="Email" required size={ size } helperText="Helper text" maxWidth="200px">
<Input size={ size }/>
<Input/>
</Field>
<Field label="Email (disabled)" required size={ size } maxWidth="200px">
<Input size={ size } disabled value="me@example.com"/>
<Input disabled value="me@example.com"/>
</Field>
<Field label="Email (readOnly)" required size={ size } maxWidth="200px">
<Input size={ size } readOnly value="me@example.com"/>
<Input readOnly value="me@example.com"/>
</Field>
<Field label="Email (invalid)" required size={ size } errorText="Something went wrong" invalid maxWidth="200px">
<Input size={ size } value="duck"/>
<Input value="duck"/>
</Field>
</Sample>
)) }
<Sample label="size: xl" w="100%" alignItems="flex-start">
<Field label="Email" required floating size="xl" helperText="Helper text" maxWidth="300px">
<Input size="xl"/>
<Input/>
</Field>
<Field label="Email (disabled)" required floating disabled size="xl" maxWidth="300px">
<Input size="xl" value="me@example.com"/>
<Input value="me@example.com"/>
</Field>
<Field label="Email (readOnly)" required floating readOnly size="xl" maxWidth="300px">
<Input size="xl" value="me@example.com"/>
<Input value="me@example.com"/>
</Field>
<Field label="Email (invalid)" required floating size="xl" errorText="Something went wrong" invalid maxWidth="300px">
<Input size="xl" value="duck"/>
<Input value="duck"/>
</Field>
</Sample>
</SamplesStack>
......@@ -63,16 +63,16 @@ const FieldShowcase = () => {
</Sample>
<Sample label="floating label" p={ 4 } bgColor={{ _light: 'blackAlpha.200', _dark: 'whiteAlpha.200' }} alignItems="flex-start">
<Field label="Email" required floating size="xl" helperText="Helper text" maxWidth="300px">
<Input size="xl"/>
<Input/>
</Field>
<Field label="Email (disabled)" required disabled floating size="xl" maxWidth="300px">
<Input size="xl" value="me@example.com"/>
<Input value="me@example.com"/>
</Field>
<Field label="Email (readOnly)" required readOnly floating size="xl" maxWidth="300px">
<Input size="xl" value="me@example.com"/>
<Input value="me@example.com"/>
</Field>
<Field label="Email (invalid)" required floating size="xl" errorText="Something went wrong" invalid maxWidth="300px">
<Input size="xl" value="duck"/>
<Input value="duck"/>
</Field>
</Sample>
</SamplesStack>
......
import { Button, Grid, GridItem, Text } from '@chakra-ui/react';
import { Grid, GridItem, Text } from '@chakra-ui/react';
import React from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { FormProvider, useForm } from 'react-hook-form';
......@@ -10,9 +10,10 @@ import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import useApiQuery from 'lib/api/useApiQuery';
import useToast from 'lib/hooks/useToast';
import useUpdateEffect from 'lib/hooks/useUpdateEffect';
import * as mixpanel from 'lib/mixpanel/index';
import { Button } from 'toolkit/chakra/button';
import { toaster } from 'toolkit/chakra/toaster';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import FormFieldAddress from 'ui/shared/forms/fields/FormFieldAddress';
......@@ -42,7 +43,6 @@ const TokenInfoForm = ({ address, tokenName, application, onSubmit }: Props) =>
const openEventSent = React.useRef<boolean>(false);
const apiFetch = useApiFetch();
const toast = useToast();
const configQuery = useApiQuery('token_info_applications_config', {
pathParams: { chainId: config.chain.id },
......@@ -85,16 +85,12 @@ const TokenInfoForm = ({ address, tokenName, application, onSubmit }: Props) =>
throw result;
}
} catch (error) {
toast({
position: 'top-right',
toaster.error({
title: 'Error',
description: (error as ResourceError<{ message: string }>)?.payload?.message || 'Something went wrong. Try again later.',
status: 'error',
variant: 'subtle',
isClosable: true,
});
}
}, [ apiFetch, application?.id, application?.status, onSubmit, toast ]);
}, [ apiFetch, application?.id, application?.status, onSubmit ]);
useUpdateEffect(() => {
if (formState.submitCount > 0 && !formState.isValid) {
......@@ -119,8 +115,8 @@ const TokenInfoForm = ({ address, tokenName, application, onSubmit }: Props) =>
}
const fieldProps = {
size: { base: 'md', lg: 'lg' },
isReadOnly: application?.status === 'IN_PROCESS',
size: 'xl' as const,
readOnly: application?.status === 'IN_PROCESS',
};
return (
......@@ -129,16 +125,16 @@ const TokenInfoForm = ({ address, tokenName, application, onSubmit }: Props) =>
<TokenInfoFormStatusText application={ application }/>
<Grid mt={ 8 } gridTemplateColumns={{ base: '1fr', lg: '1fr 1fr' }} columnGap={ 5 } rowGap={ 5 }>
<FormFieldText<Fields> name="token_name" isRequired placeholder="Token name" { ...fieldProps } isReadOnly/>
<FormFieldAddress<Fields> name="address" isRequired placeholder="Token contract address" { ...fieldProps } isReadOnly/>
<FormFieldText<Fields> name="requester_name" isRequired placeholder="Requester name" { ...fieldProps }/>
<FormFieldEmail<Fields> name="requester_email" isRequired placeholder="Requester email" { ...fieldProps }/>
<FormFieldText<Fields> name="token_name" required placeholder="Token name" { ...fieldProps } readOnly/>
<FormFieldAddress<Fields> name="address" required placeholder="Token contract address" { ...fieldProps } readOnly/>
<FormFieldText<Fields> name="requester_name" required placeholder="Requester name" { ...fieldProps }/>
<FormFieldEmail<Fields> name="requester_email" required placeholder="Requester email" { ...fieldProps }/>
<TokenInfoFormSectionHeader>Project info</TokenInfoFormSectionHeader>
<FormFieldText<Fields> name="project_name" placeholder="Project name" { ...fieldProps } rules={ nonWhitespaceFieldRules }/>
<TokenInfoFieldProjectSector { ...fieldProps } config={ configQuery.data.projectSectors }/>
<FormFieldEmail<Fields> name="project_email" isRequired placeholder="Official project email address" { ...fieldProps }/>
<FormFieldUrl<Fields> name="project_website" isRequired placeholder="Official project website" { ...fieldProps }/>
<FormFieldEmail<Fields> name="project_email" required placeholder="Official project email address" { ...fieldProps }/>
<FormFieldUrl<Fields> name="project_website" required placeholder="Official project website" { ...fieldProps }/>
<FormFieldUrl<Fields> name="docs" placeholder="Docs" { ...fieldProps }/>
<TokenInfoFieldSupport { ...fieldProps }/>
<GridItem colSpan={{ base: 1, lg: 2 }}>
......@@ -147,14 +143,14 @@ const TokenInfoForm = ({ address, tokenName, application, onSubmit }: Props) =>
<GridItem colSpan={{ base: 1, lg: 2 }}>
<FormFieldText<Fields>
name="project_description"
isRequired
required
placeholder="Project description"
maxH="160px"
rules={{ maxLength: 300, ...nonWhitespaceFieldRules }}
asComponent="Textarea"
{ ...fieldProps }
/>
<Text variant="secondary" fontSize="sm" mt={ 1 }>
<Text color="text.secondary" fontSize="sm" mt={ 1 }>
Introduce or summarize the project’s operation/goals in a maximum of 300 characters.
The description should be written in a neutral point of view and must exclude unsubstantiated claims unless proven otherwise.
</Text>
......@@ -194,9 +190,9 @@ const TokenInfoForm = ({ address, tokenName, application, onSubmit }: Props) =>
type="submit"
size="lg"
mt={ 8 }
isLoading={ formState.isSubmitting }
loading={ formState.isSubmitting }
loadingText="Send request"
isDisabled={ application?.status === 'IN_PROCESS' }
disabled={ application?.status === 'IN_PROCESS' }
>
Send request
</Button>
......
import { GridItem } from '@chakra-ui/react';
import React from 'react';
import { Heading } from 'toolkit/chakra/heading';
interface Props {
children: React.ReactNode;
}
const TokenInfoFormSectionHeader = ({ children }: Props) => {
return (
<GridItem colSpan={{ base: 1, lg: 2 }} fontFamily="heading" fontSize="lg" fontWeight={ 500 } mt={ 3 }>
<GridItem colSpan={{ base: 1, lg: 2 }} mt={ 3 }>
<Heading level="2">
{ children }
</Heading>
</GridItem>
);
};
......
import { Alert } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfoApplication } from 'types/api/account';
import { Alert } from 'toolkit/chakra/alert';
interface Props {
application?: TokenInfoApplication;
}
......
import { Center, useColorModeValue } from '@chakra-ui/react';
import { Center } from '@chakra-ui/react';
import React from 'react';
interface Props {
......@@ -8,13 +8,13 @@ interface Props {
}
const TokenInfoIconPreview = ({ url, isInvalid, children }: Props) => {
const borderColor = useColorModeValue('gray.100', 'gray.700');
const borderColorFilled = useColorModeValue('gray.300', 'gray.600');
const borderColor = { _light: 'gray.100', _dark: 'gray.700' };
const borderColorFilled = { _light: 'gray.300', _dark: 'gray.600' };
const borderColorActive = isInvalid ? 'error' : borderColorFilled;
return (
<Center
boxSize={{ base: '60px', lg: '80px' }}
boxSize="60px"
flexShrink={ 0 }
borderWidth="2px"
borderColor={ url ? borderColorActive : borderColor }
......
import type { FormControlProps } from '@chakra-ui/react';
import { Flex } from '@chakra-ui/react';
import React from 'react';
import type { Fields } from '../types';
import { times } from 'lib/html-entities';
import type { FieldProps } from 'toolkit/chakra/field';
import ImageUrlPreview from 'ui/shared/forms/components/ImageUrlPreview';
import FormFieldUrl from 'ui/shared/forms/fields/FormFieldUrl';
import useFieldWithImagePreview from 'ui/shared/forms/utils/useFieldWithImagePreview';
......@@ -13,11 +13,11 @@ import TokenLogoPlaceholder from 'ui/shared/TokenLogoPlaceholder';
import TokenInfoIconPreview from '../TokenInfoIconPreview';
interface Props {
isReadOnly?: boolean;
size?: FormControlProps['size'];
readOnly?: boolean;
size?: FieldProps['size'];
}
const TokenInfoFieldIconUrl = ({ isReadOnly, size }: Props) => {
const TokenInfoFieldIconUrl = ({ readOnly, size }: Props) => {
const previewUtils = useFieldWithImagePreview({ name: 'icon_url', isRequired: true });
......@@ -26,15 +26,15 @@ const TokenInfoFieldIconUrl = ({ isReadOnly, size }: Props) => {
<FormFieldUrl<Fields>
name="icon_url"
placeholder={ `Link to icon URL, link to download a SVG or 48${ times }48 PNG icon logo` }
isReadOnly={ isReadOnly }
readOnly={ readOnly }
size={ size }
{ ...previewUtils.input }
/>
<TokenInfoIconPreview url={ previewUtils.preview.src } isInvalid={ previewUtils.preview.isInvalid }>
<ImageUrlPreview
{ ...previewUtils.preview }
fallback={ <TokenLogoPlaceholder boxSize={{ base: 10, lg: 12 }}/> }
boxSize={{ base: 10, lg: 12 }}
fallback={ <TokenLogoPlaceholder boxSize={ 10 }/> }
boxSize={ 10 }
borderRadius="base"
/>
</TokenInfoIconPreview>
......
import { createListCollection } from '@chakra-ui/react';
import React from 'react';
import type { Fields } from '../types';
import type { TokenInfoApplicationConfig } from 'types/api/account';
import FormFieldFancySelect from 'ui/shared/forms/fields/FormFieldFancySelect';
import FormFieldSelect from 'ui/shared/forms/fields/FormFieldSelect';
interface Props {
isReadOnly?: boolean;
readOnly?: boolean;
config: TokenInfoApplicationConfig['projectSectors'];
}
const TokenInfoFieldProjectSector = ({ isReadOnly, config }: Props) => {
const TokenInfoFieldProjectSector = ({ readOnly, config }: Props) => {
const options = React.useMemo(() => {
return config.map((option) => ({ label: option, value: option }));
const collection = React.useMemo(() => {
const items = config.map((option) => ({ label: option, value: option }));
return createListCollection({ items });
}, [ config ]);
return (
<FormFieldFancySelect<Fields, 'project_sector'>
<FormFieldSelect<Fields, 'project_sector'>
name="project_sector"
placeholder="Project industry"
options={ options }
isReadOnly={ isReadOnly }
collection={ collection }
readOnly={ readOnly }
/>
);
};
......
import type { FormControlProps } from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import type { Fields, SocialLinkFields } from '../types';
import type { FieldProps } from 'toolkit/chakra/field';
import FormFieldUrl from 'ui/shared/forms/fields/FormFieldUrl';
import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
......@@ -14,36 +14,38 @@ interface Item {
color: string;
}
const SETTINGS: Record<keyof SocialLinkFields, Item> = {
github: { label: 'GitHub', icon: 'social/github_filled', color: 'inherit' },
github: { label: 'GitHub', icon: 'social/github_filled', color: 'text.primary' },
telegram: { label: 'Telegram', icon: 'social/telegram_filled', color: 'telegram' },
linkedin: { label: 'LinkedIn', icon: 'social/linkedin_filled', color: 'linkedin' },
discord: { label: 'Discord', icon: 'social/discord_filled', color: 'discord' },
slack: { label: 'Slack', icon: 'social/slack_filled', color: 'slack' },
twitter: { label: 'X (ex-Twitter)', icon: 'social/twitter_filled', color: 'inherit' },
twitter: { label: 'X (ex-Twitter)', icon: 'social/twitter_filled', color: 'text.primary' },
opensea: { label: 'OpenSea', icon: 'social/opensea_filled', color: 'opensea' },
facebook: { label: 'Facebook', icon: 'social/facebook_filled', color: 'facebook' },
medium: { label: 'Medium', icon: 'social/medium_filled', color: 'inherit' },
medium: { label: 'Medium', icon: 'social/medium_filled', color: 'text.primary' },
reddit: { label: 'Reddit', icon: 'social/reddit_filled', color: 'reddit' },
};
interface Props {
isReadOnly?: boolean;
size?: FormControlProps['size'];
readOnly?: boolean;
size?: FieldProps['size'];
name: keyof SocialLinkFields;
}
const TokenInfoFieldSocialLink = ({ isReadOnly, size, name }: Props) => {
const TokenInfoFieldSocialLink = ({ readOnly, size, name }: Props) => {
const rightElement = React.useCallback(({ field }: { field: ControllerRenderProps<Fields, keyof SocialLinkFields> }) => {
return <IconSvg name={ SETTINGS[name].icon } boxSize={ 6 } color={ field.value ? SETTINGS[name].color : '#718096' }/>;
const endElement = React.useCallback(({ field }: { field: ControllerRenderProps<Fields> }) => {
return <IconSvg name={ SETTINGS[name].icon } boxSize="60px" px={ 4 } color={ field.value ? SETTINGS[name].color : '#718096' }/>;
}, [ name ]);
return (
<FormFieldUrl<Fields, keyof SocialLinkFields>
<FormFieldUrl<Fields>
name={ name }
placeholder={ SETTINGS[name].label }
rightElement={ rightElement }
isReadOnly={ isReadOnly }
group={{
endElement,
}}
readOnly={ readOnly }
size={ size }
/>
);
......
import type { InputProps } from '@chakra-ui/react';
import React from 'react';
import type { Fields } from '../types';
import type { FieldProps } from 'toolkit/chakra/field';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
import { validator as emailValidator } from 'ui/shared/forms/validators/email';
import { urlValidator } from 'ui/shared/forms/validators/url';
interface Props {
isReadOnly?: boolean;
size?: InputProps['size'];
readOnly?: boolean;
size?: FieldProps['size'];
}
const TokenInfoFieldSupport = (props: Props) => {
......
......@@ -6,7 +6,7 @@ export interface Fields extends SocialLinkFields, TickerUrlFields {
requester_name: string;
requester_email: string;
project_name?: string;
project_sector: Option | null;
project_sector: Array<Option> | null;
project_email: string;
project_website: string;
project_description: string;
......
......@@ -12,7 +12,7 @@ export function getFormDefaultValues(address: string, tokenName: string, applica
requester_name: application.requesterName,
requester_email: application.requesterEmail,
project_name: application.projectName,
project_sector: application.projectSector ? { value: application.projectSector, label: application.projectSector } : null,
project_sector: application.projectSector ? [ { value: application.projectSector, label: application.projectSector } ] : null,
project_email: application.projectEmail,
project_website: application.projectWebsite,
project_description: application.projectDescription || '',
......@@ -52,7 +52,7 @@ export function prepareRequestBody(data: Fields): Omit<TokenInfoApplication, 'id
projectDescription: data.project_description,
projectEmail: data.project_email,
projectName: data.project_name,
projectSector: data.project_sector?.value,
projectSector: data.project_sector?.[0]?.value,
projectWebsite: data.project_website,
reddit: data.reddit,
requesterEmail: data.requester_email,
......
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