Commit ae7139d9 authored by tom's avatar tom

project info fields

parent 4f24e533
import type { UserInfo, CustomAbis, PublicTags, AddressTags, TransactionTags, ApiKeys, WatchlistAddress, VerifiedAddressResponse } from 'types/api/account'; import type {
UserInfo,
CustomAbis,
PublicTags,
AddressTags,
TransactionTags,
ApiKeys,
WatchlistAddress,
VerifiedAddressResponse,
TokenInfoApplicationConfig,
} from 'types/api/account';
import type { import type {
Address, Address,
AddressCounters, AddressCounters,
...@@ -101,6 +111,13 @@ export const RESOURCES = { ...@@ -101,6 +111,13 @@ export const RESOURCES = {
basePath: appConfig.contractInfoApi.basePath, basePath: appConfig.contractInfoApi.basePath,
}, },
token_info_application_config: {
path: '/api/v1/chains/:chainId/token-info-submissions/selectors',
pathParams: [ 'chainId' as const ],
endpoint: appConfig.adminServiceApi.endpoint,
basePath: appConfig.adminServiceApi.basePath,
},
// STATS // STATS
stats_counters: { stats_counters: {
path: '/api/v1/counters', path: '/api/v1/counters',
...@@ -500,6 +517,7 @@ Q extends 'private_tags_tx' ? TransactionTags : ...@@ -500,6 +517,7 @@ Q extends 'private_tags_tx' ? TransactionTags :
Q extends 'api_keys' ? ApiKeys : Q extends 'api_keys' ? ApiKeys :
Q extends 'watchlist' ? Array<WatchlistAddress> : Q extends 'watchlist' ? Array<WatchlistAddress> :
Q extends 'verified_addresses' ? VerifiedAddressResponse : Q extends 'verified_addresses' ? VerifiedAddressResponse :
Q extends 'token_info_application_config' ? TokenInfoApplicationConfig :
Q extends 'homepage_stats' ? HomeStats : Q extends 'homepage_stats' ? HomeStats :
Q extends 'homepage_chart_txs' ? ChartTransactionResponse : Q extends 'homepage_chart_txs' ? ChartTransactionResponse :
Q extends 'homepage_chart_market' ? ChartMarketResponse : Q extends 'homepage_chart_market' ? ChartMarketResponse :
......
export const validator = (value: string | undefined) => {
if (!value) {
return true;
}
try {
new URL(value);
return true;
} catch (error) {
return 'Incorrect URL';
}
};
...@@ -171,3 +171,7 @@ export interface VerifiedAddress { ...@@ -171,3 +171,7 @@ export interface VerifiedAddress {
export interface VerifiedAddressResponse { export interface VerifiedAddressResponse {
verifiedAddresses: Array<VerifiedAddress>; verifiedAddresses: Array<VerifiedAddress>;
} }
export interface TokenInfoApplicationConfig {
projectSectors: Array<string>;
}
import { Grid, GridItem } from '@chakra-ui/react'; import { Button, Grid, GridItem } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import type { Fields } from './types'; import type { Fields } from './types';
import appConfig from 'configs/app/config';
import useApiQuery from 'lib/api/useApiQuery';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TokenInfoFieldAddress from './fields/TokenInfoFieldAddress'; import TokenInfoFieldAddress from './fields/TokenInfoFieldAddress';
import TokenInfoFieldDocs from './fields/TokenInfoFieldDocs';
import TokenInfoFieldIconUrl from './fields/TokenInfoFieldIconUrl';
import TokenInfoFieldProjectDescription from './fields/TokenInfoFieldProjectDescription';
import TokenInfoFieldProjectEmail from './fields/TokenInfoFieldProjectEmail';
import TokenInfoFieldProjectName from './fields/TokenInfoFieldProjectName';
import TokenInfoFieldProjectSector from './fields/TokenInfoFieldProjectSector';
import TokenInfoFieldProjectWebsite from './fields/TokenInfoFieldProjectWebsite';
import TokenInfoFieldRequesterEmail from './fields/TokenInfoFieldRequesterEmail'; import TokenInfoFieldRequesterEmail from './fields/TokenInfoFieldRequesterEmail';
import TokenInfoFieldRequesterName from './fields/TokenInfoFieldRequesterName'; import TokenInfoFieldRequesterName from './fields/TokenInfoFieldRequesterName';
import TokenInfoFieldSupport from './fields/TokenInfoFieldSupport';
import TokenInfoFormSectionHeader from './TokenInfoFormSectionHeader';
interface Props { interface Props {
id: number; id: number;
} }
const TokenInfoForm = ({ id }: Props) => { const TokenInfoForm = ({ id }: Props) => {
const configQuery = useApiQuery('token_info_application_config', {
pathParams: { chainId: appConfig.network.id },
});
const formApi = useForm<Fields>({ const formApi = useForm<Fields>({
mode: 'onBlur', mode: 'onBlur',
defaultValues: { defaultValues: {
...@@ -29,22 +48,36 @@ const TokenInfoForm = ({ id }: Props) => { ...@@ -29,22 +48,36 @@ const TokenInfoForm = ({ id }: Props) => {
const onSubmit = handleSubmit(onFormSubmit); const onSubmit = handleSubmit(onFormSubmit);
if (configQuery.isError) {
return <DataFetchAlert/>;
}
if (configQuery.isLoading) {
return <ContentLoader/>;
}
const fieldProps = { control, formState };
return ( return (
<form noValidate onSubmit={ onSubmit }> <form noValidate onSubmit={ onSubmit }>
<div>Requests are sent to a moderator for review and approval. This process can take several days.</div> <div>Requests are sent to a moderator for review and approval. This process can take several days.</div>
<Grid mt={ 8 } gridTemplateColumns="1fr 1fr" columnGap={ 5 } rowGap={ 5 }> <Grid mt={ 8 } gridTemplateColumns="1fr 1fr" columnGap={ 5 } rowGap={ 5 }>
<GridItem colSpan={ 2 }> <GridItem colSpan={ 2 }><TokenInfoFieldAddress { ...fieldProps }/></GridItem>
<TokenInfoFieldAddress control={ control }/> <TokenInfoFieldRequesterName { ...fieldProps }/>
</GridItem> <TokenInfoFieldRequesterEmail { ...fieldProps }/>
<GridItem> <TokenInfoFormSectionHeader>Project info</TokenInfoFormSectionHeader>
<TokenInfoFieldRequesterName control={ control } formState={ formState }/> <TokenInfoFieldProjectName { ...fieldProps }/>
</GridItem> <TokenInfoFieldProjectSector { ...fieldProps } config={ configQuery.data.projectSectors }/>
<GridItem> <TokenInfoFieldProjectEmail { ...fieldProps }/>
<TokenInfoFieldRequesterEmail control={ control } formState={ formState }/> <TokenInfoFieldProjectWebsite { ...fieldProps }/>
</GridItem> <TokenInfoFieldDocs { ...fieldProps }/>
<TokenInfoFieldSupport { ...fieldProps }/>
<GridItem colSpan={ 2 }><TokenInfoFieldIconUrl { ...fieldProps }/></GridItem>
<GridItem colSpan={ 2 }><TokenInfoFieldProjectDescription { ...fieldProps }/></GridItem>
</Grid> </Grid>
<Button type="submit" size="lg" mt={ 8 }>Send request</Button>
</form> </form>
); );
}; };
export default TokenInfoForm; export default React.memo(TokenInfoForm);
import { GridItem } from '@chakra-ui/react';
import React from 'react';
interface Props {
children: React.ReactNode;
}
const TokenInfoFormSectionHeader = ({ children }: Props) => {
return (
<GridItem colSpan={ 2 } fontFamily="heading" fontSize="lg" fontWeight={ 500 } mt={ 3 }>
{ children }
</GridItem>
);
};
export default TokenInfoFormSectionHeader;
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields } from '../types';
import { validator } from 'lib/validations/url';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props {
formState: FormState<Fields>;
control: Control<Fields>;
isReadOnly?: boolean;
}
const TokenInfoFieldDocs = ({ formState, control, isReadOnly }: Props) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'docs'>}) => {
const error = 'docs' in formState.errors ? formState.errors.docs : undefined;
return (
<FormControl variant="floating" id={ field.name } size="lg">
<Input
{ ...field }
isInvalid={ Boolean(error) }
isDisabled={ formState.isSubmitting || isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Docs" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting, isReadOnly ]);
return (
<Controller
name="docs"
control={ control }
render={ renderControl }
rules={{ validate: validator }}
/>
);
};
export default React.memo(TokenInfoFieldDocs);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields } from '../types';
import { times } from 'lib/html-entities';
import { validator } from 'lib/validations/url';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props {
formState: FormState<Fields>;
control: Control<Fields>;
isReadOnly?: boolean;
}
const TokenInfoFieldIconUrl = ({ formState, control, isReadOnly }: Props) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'icon_url'>}) => {
const error = 'icon_url' in formState.errors ? formState.errors.icon_url : undefined;
return (
<FormControl variant="floating" id={ field.name } size="lg" isRequired>
<Input
{ ...field }
isInvalid={ Boolean(error) }
isDisabled={ formState.isSubmitting || isReadOnly }
autoComplete="off"
required
/>
<InputPlaceholder text={ `Link to icon URL, link to download a SVG or 48${ times }48 PNG icon logo` } error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting, isReadOnly ]);
return (
<Controller
name="icon_url"
control={ control }
render={ renderControl }
rules={{ required: true, validate: validator }}
/>
);
};
export default React.memo(TokenInfoFieldIconUrl);
import { FormControl, Text, Textarea } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields } from '../types';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props {
formState: FormState<Fields>;
control: Control<Fields>;
isReadOnly?: boolean;
}
const TokenInfoFieldProjectDescription = ({ formState, control, isReadOnly }: Props) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'project_description'>}) => {
const error = 'project_description' in formState.errors ? formState.errors.project_description : undefined;
return (
<FormControl variant="floating" id={ field.name } size="lg" isRequired>
<Textarea
{ ...field }
required
isInvalid={ Boolean(error) }
isDisabled={ formState.isSubmitting || isReadOnly }
autoComplete="off"
maxH="160px"
maxLength={ 300 }
/>
<InputPlaceholder text="Project description" error={ error }/>
<Text variant="secondary" fontSize="sm" mt={ 1 }>
Introduce or summarise 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>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting, isReadOnly ]);
return (
<Controller
name="project_description"
control={ control }
render={ renderControl }
rules={{ required: true, maxLength: 300 }}
/>
);
};
export default React.memo(TokenInfoFieldProjectDescription);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields } from '../types';
import { EMAIL_REGEXP } from 'lib/validations/email';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props {
formState: FormState<Fields>;
control: Control<Fields>;
isReadOnly?: boolean;
}
const TokenInfoFieldProjectEmail = ({ formState, control, isReadOnly }: Props) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'project_email'>}) => {
const error = 'project_email' in formState.errors ? formState.errors.project_email : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size="lg">
<Input
{ ...field }
required
isInvalid={ Boolean(error) }
isDisabled={ formState.isSubmitting || isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Official project email address" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting, isReadOnly ]);
return (
<Controller
name="project_email"
control={ control }
render={ renderControl }
rules={{ required: true, pattern: EMAIL_REGEXP }}
/>
);
};
export default React.memo(TokenInfoFieldProjectEmail);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields } from '../types';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props {
formState: FormState<Fields>;
control: Control<Fields>;
isReadOnly?: boolean;
}
const TokenInfoFieldProjectName = ({ formState, control, isReadOnly }: Props) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'project_name'>}) => {
const error = 'project_name' in formState.errors ? formState.errors.project_name : undefined;
return (
<FormControl variant="floating" id={ field.name } size="lg">
<Input
{ ...field }
isInvalid={ Boolean(error) }
isDisabled={ formState.isSubmitting || isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Project name" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting, isReadOnly ]);
return (
<Controller
name="project_name"
control={ control }
render={ renderControl }
/>
);
};
export default React.memo(TokenInfoFieldProjectName);
import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields } from '../types';
import type { TokenInfoApplicationConfig } from 'types/api/account';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
interface Props {
formState: FormState<Fields>;
control: Control<Fields>;
isReadOnly?: boolean;
config: TokenInfoApplicationConfig['projectSectors'];
}
const TokenInfoFieldProjectSector = ({ formState, control, isReadOnly, config }: Props) => {
const options = React.useMemo(() => {
return config.map((option) => ({ label: option, value: option }));
}, [ config ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'project_sector'>}) => {
const error = 'project_sector' in formState.errors ? formState.errors.project_sector : undefined;
return (
<FancySelect
{ ...field }
options={ options }
size="lg"
placeholder="Project industry"
isDisabled={ formState.isSubmitting || isReadOnly }
error={ error }
/>
);
}, [ formState.errors, formState.isSubmitting, isReadOnly, options ]);
return (
<Controller
name="project_sector"
control={ control }
render={ renderControl }
/>
);
};
export default React.memo(TokenInfoFieldProjectSector);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields } from '../types';
import { validator } from 'lib/validations/url';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props {
formState: FormState<Fields>;
control: Control<Fields>;
isReadOnly?: boolean;
}
const TokenInfoFieldProjectWebsite = ({ formState, control, isReadOnly }: Props) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'project_website'>}) => {
const error = 'project_website' in formState.errors ? formState.errors.project_website : undefined;
return (
<FormControl variant="floating" id={ field.name } size="lg" isRequired>
<Input
{ ...field }
isInvalid={ Boolean(error) }
isDisabled={ formState.isSubmitting || isReadOnly }
autoComplete="off"
required
/>
<InputPlaceholder text="Official project website" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting, isReadOnly ]);
return (
<Controller
name="project_website"
control={ control }
render={ renderControl }
rules={{ required: true, validate: validator }}
/>
);
};
export default React.memo(TokenInfoFieldProjectWebsite);
import { FormControl, Input } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { Fields } from '../types';
import { validator } from 'lib/validations/url';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props {
formState: FormState<Fields>;
control: Control<Fields>;
isReadOnly?: boolean;
}
const TokenInfoFieldSupport = ({ formState, control, isReadOnly }: Props) => {
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'support'>}) => {
const error = 'support' in formState.errors ? formState.errors.support : undefined;
return (
<FormControl variant="floating" id={ field.name } size="lg">
<Input
{ ...field }
isInvalid={ Boolean(error) }
isDisabled={ formState.isSubmitting || isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Support" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting, isReadOnly ]);
return (
<Controller
name="support"
control={ control }
render={ renderControl }
rules={{ validate: validator }}
/>
);
};
export default React.memo(TokenInfoFieldSupport);
import type { Option } from 'ui/shared/FancySelect/types';
export interface Fields { export interface Fields {
address: string; address: string;
requester_name: string; requester_name: string;
requester_email: string; requester_email: string;
project_name?: string;
project_sector: Option | null;
project_email: string;
project_website: string;
project_description: string;
docs?: string;
support?: string;
icon_url: string;
} }
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