Skip to content

Commit

Permalink
Search section (homepage) Dark/Dim themes
Browse files Browse the repository at this point in the history
Fixes #1373
  • Loading branch information
tom2drum committed Dec 11, 2023
2 parents 613f566 + 4652c13 commit 30db8f2
Show file tree
Hide file tree
Showing 55 changed files with 399 additions and 164 deletions.
24 changes: 15 additions & 9 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,21 @@ And of course our premier language is [Typescript](https://www.typescriptlang.or
## Local development
1. Prepare your environment variables:
- clone `.env.example` into `configs/envs/.env.secrets` and fill it with necessary secrets for the [external services](./ENVS.md#external-services-configuration) integration; you can pick up only those that your needed
- choose one of the following options:
A. create `.env.local` file in the root folder with environment variables from the [list](./ENVS.md); all required variables should be present in the file;
B. pick up one of the predefined configurations located at `/configs/envs` folder; no actual action is needed at this stage;
2. Run your local dev server:
- if you picked up option "A" above, use `yarn dev` command
- if your options is "B", use `yarn dev:<config_name>` command
3. In browser navigate to the URL from the command output (by default, it is `http://localhost:3000`)
To develop locally, follow one of the two paths outlined below:
A. Custom configuration:
1. Create `.env.local` file in the root folder and include all required environment variables from the [list](./ENVS.md)
2. Optionally, clone `.env.example` and name it `.env.secrets`. Fill it with necessary secrets for integrating with [external services](./ENVS.md#external-services-configuration). Include only secrets your need.
3. Use `yarn dev` command to start the dev server.
4. Open your browser and navigate to the URL provided in the command line output (by default, it is `http://localhost:3000`).
B. Pre-defined configuration:
1. Optionally, clone `.env.example` file into `configs/envs/.env.secrets`. Fill it with necessary secrets for integrating with [external services](./ENVS.md#external-services-configuration). Include only secrets your need.
2. Choose one of the predefined configurations located in the `/configs/envs` folder.
3. Start your local dev server using the `yarn dev:<config_name>` command.
4. Open your browser and navigate to the URL provided in the command line output (by default, it is `http://localhost:3000`).
&nbsp;
Expand Down
4 changes: 4 additions & 0 deletions icons/verify-contract.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 12 additions & 3 deletions lib/hooks/useNavItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import topAccountsIcon from 'icons/top-accounts.svg';
import transactionsIcon from 'icons/transactions.svg';
import txnBatchIcon from 'icons/txn_batches.svg';
import verifiedIcon from 'icons/verified.svg';
import verifyContractIcon from 'icons/verify-contract.svg';
import watchlistIcon from 'icons/watchlist.svg';
import { rightLineArrow } from 'lib/html-entities';
import UserAvatar from 'ui/shared/UserAvatar';
Expand Down Expand Up @@ -176,11 +177,19 @@ export default function useNavItems(): ReturnType {
isActive: apiNavItems.some(item => isInternalItem(item) && item.isActive),
subItems: apiNavItems,
},
config.UI.sidebar.otherLinks.length > 0 ? {
{
text: 'Other',
icon: gearIcon,
subItems: config.UI.sidebar.otherLinks,
} : null,
subItems: [
{
text: 'Verify contract',
nextRoute: { pathname: '/contract-verification' as const },
icon: verifyContractIcon,
isActive: pathname.startsWith('/contract-verification'),
},
...config.UI.sidebar.otherLinks,
],
},
].filter(Boolean);

const accountNavItems: ReturnType['accountNavItems'] = [
Expand Down
1 change: 1 addition & 0 deletions lib/metadata/getPageOgType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/accounts': 'Root page',
'/address/[hash]': 'Regular page',
'/verified-contracts': 'Root page',
'/contract-verification': 'Root page',
'/address/[hash]/contract-verification': 'Regular page',
'/tokens': 'Root page',
'/token/[hash]': 'Regular page',
Expand Down
1 change: 1 addition & 0 deletions lib/metadata/templates/description.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/accounts': DEFAULT_TEMPLATE,
'/address/[hash]': 'View the account balance, transactions, and other data for %hash% on the %network_title%',
'/verified-contracts': DEFAULT_TEMPLATE,
'/contract-verification': DEFAULT_TEMPLATE,
'/address/[hash]/contract-verification': 'View the account balance, transactions, and other data for %hash% on the %network_title%',
'/tokens': DEFAULT_TEMPLATE,
'/token/[hash]': '%hash%, balances and analytics on the %network_title%',
Expand Down
1 change: 1 addition & 0 deletions lib/metadata/templates/title.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/accounts': 'top accounts',
'/address/[hash]': 'address details for %hash%',
'/verified-contracts': 'verified contracts',
'/contract-verification': 'verify contract',
'/address/[hash]/contract-verification': 'contract verification for %hash%',
'/tokens': 'tokens',
'/token/[hash]': '%symbol% token details',
Expand Down
3 changes: 2 additions & 1 deletion lib/mixpanel/getPageType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/accounts': 'Top accounts',
'/address/[hash]': 'Address details',
'/verified-contracts': 'Verified contracts',
'/address/[hash]/contract-verification': 'Contract verification',
'/contract-verification': 'Contract verification',
'/address/[hash]/contract-verification': 'Contract verification for address',
'/tokens': 'Tokens',
'/token/[hash]': 'Token details',
'/token/[hash]/instance/[id]': 'Token Instance',
Expand Down
1 change: 1 addition & 0 deletions nextjs/nextjs-routes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/auth/unverified-email">
| DynamicRoute<"/block/[height_or_hash]", { "height_or_hash": string }>
| StaticRoute<"/blocks">
| StaticRoute<"/contract-verification">
| StaticRoute<"/csv-export">
| StaticRoute<"/graphiql">
| StaticRoute<"/">
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"npm": "8"
},
"scripts": {
"dev": "next dev",
"dev": "./tools/scripts/dev.sh",
"dev:preset": "./tools/scripts/dev.preset.sh",
"build": "next build",
"build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse --short HEAD) --build-arg GIT_TAG=$(git describe --tags --abbrev=0) -t blockscout-frontend:local ./",
Expand Down
4 changes: 2 additions & 2 deletions pages/address/[hash]/contract-verification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import React from 'react';
import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs';

import ContractVerification from 'ui/pages/ContractVerification';
import ContractVerificationForAddress from 'ui/pages/ContractVerificationForAddress';

const Page: NextPage<Props> = (props: Props) => {
return (
<PageNextJs pathname="/address/[hash]/contract-verification" query={ props }>
<ContractVerification/>
<ContractVerificationForAddress/>
</PageNextJs>
);
};
Expand Down
19 changes: 19 additions & 0 deletions pages/contract-verification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { NextPage } from 'next';
import React from 'react';

import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs';

import ContractVerification from 'ui/pages/ContractVerification';

const Page: NextPage<Props> = (props: Props) => {
return (
<PageNextJs pathname="/contract-verification" query={ props }>
<ContractVerification/>
</PageNextJs>
);
};

export default Page;

export { base as getServerSideProps } from 'nextjs/getServerSideProps';
5 changes: 0 additions & 5 deletions tools/scripts/dev.preset.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ if [ ! -f "$config_file" ]; then
exit 1
fi

if [ ! -f "$secrets_file" ]; then
echo "Error: File '$secrets_file' not found."
exit 1
fi

# download assets for the running instance
dotenv \
-e $config_file \
Expand Down
21 changes: 21 additions & 0 deletions tools/scripts/dev.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash

# download assets for the running instance
dotenv \
-e .env.development.local \
-e .env.local \
-e .env.development \
-e .env \
-- bash -c './deploy/scripts/download_assets.sh ./public/assets'

# generate envs.js file and run the app
dotenv \
-v NEXT_PUBLIC_GIT_COMMIT_SHA=$(git rev-parse --short HEAD) \
-v NEXT_PUBLIC_GIT_TAG=$(git describe --tags --abbrev=0) \
-e .env.secrets \
-e .env.development.local \
-e .env.local \
-e .env.development \
-e .env \
-- bash -c './deploy/scripts/make_envs_script.sh && next dev -- -p $NEXT_PUBLIC_APP_PORT' |
pino-pretty
61 changes: 44 additions & 17 deletions ui/contractVerification/ContractVerificationForm.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { Button, chakra, useUpdateEffect } from '@chakra-ui/react';
import { Button, Grid, chakra, useUpdateEffect } from '@chakra-ui/react';
import React from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form';

import type { FormFields } from './types';
import type { SocketMessage } from 'lib/socket/types';
import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/api/contract';
import type { SmartContractVerificationMethod, SmartContractVerificationConfig, SmartContract } from 'types/api/contract';

import { route } from 'nextjs-routes';

import useApiFetch from 'lib/api/useApiFetch';
import delay from 'lib/delay';
import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode';
import useToast from 'lib/hooks/useToast';
import * as mixpanel from 'lib/mixpanel/index';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';

import ContractVerificationFieldAddress from './fields/ContractVerificationFieldAddress';
import ContractVerificationFieldMethod from './fields/ContractVerificationFieldMethod';
import ContractVerificationFlattenSourceCode from './methods/ContractVerificationFlattenSourceCode';
import ContractVerificationMultiPartFile from './methods/ContractVerificationMultiPartFile';
Expand All @@ -29,15 +31,15 @@ import { prepareRequestBody, formatSocketErrors, getDefaultValues, METHOD_LABELS
interface Props {
method?: SmartContractVerificationMethod;
config: SmartContractVerificationConfig;
hash: string;
hash?: string;
}

const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => {
const formApi = useForm<FormFields>({
mode: 'onBlur',
defaultValues: methodFromQuery ? getDefaultValues(methodFromQuery, config) : undefined,
defaultValues: methodFromQuery ? getDefaultValues(methodFromQuery, config, hash) : undefined,
});
const { control, handleSubmit, watch, formState, setError, reset } = formApi;
const { control, handleSubmit, watch, formState, setError, reset, getFieldState } = formApi;
const submitPromiseResolver = React.useRef<(value: unknown) => void>();
const methodNameRef = React.useRef<string>();

Expand All @@ -47,9 +49,28 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => {
const body = prepareRequestBody(data);

if (!hash) {
try {
const response = await apiFetch<'contract', SmartContract>('contract', {
pathParams: { hash: data.address.toLowerCase() },
});

const isVerifiedContract = 'is_verified' in response && response?.is_verified && !response.is_partially_verified;
if (isVerifiedContract) {
setError('address', { message: 'Contract has already been verified' });
return Promise.resolve();
}
} catch (error) {
const statusCode = getErrorObjStatusCode(error);
const message = statusCode === 404 ? 'Address is not a smart contract' : 'Something went wrong';
setError('address', { message });
return Promise.resolve();
}
}

try {
await apiFetch('contract_verification_via', {
pathParams: { method: data.method.value, hash: hash.toLowerCase() },
pathParams: { method: data.method.value, hash: data.address.toLowerCase() },
fetchParams: {
method: 'POST',
body,
Expand All @@ -62,7 +83,10 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
return new Promise((resolve) => {
submitPromiseResolver.current = resolve;
});
}, [ apiFetch, hash ]);
}, [ apiFetch, hash, setError ]);

const address = watch('address');
const addressState = getFieldState('address');

const handleNewSocketMessage: SocketMessage.ContractVerification['handler'] = React.useCallback(async(payload) => {
if (payload.status === 'error') {
Expand All @@ -88,8 +112,8 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
{ send_immediately: true },
);

window.location.assign(route({ pathname: '/address/[hash]', query: { hash, tab: 'contract' } }));
}, [ hash, setError, toast ]);
window.location.assign(route({ pathname: '/address/[hash]', query: { hash: address, tab: 'contract' } }));
}, [ setError, toast, address ]);

const handleSocketError = React.useCallback(() => {
if (!formState.isSubmitting) {
Expand All @@ -114,10 +138,10 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
}, [ toast ]);

const channel = useSocketChannel({
topic: `addresses:${ hash.toLowerCase() }`,
topic: `addresses:${ address?.toLowerCase() }`,
onSocketClose: handleSocketError,
onSocketError: handleSocketError,
isDisabled: false,
isDisabled: Boolean(address && addressState.error),
});
useSocketMessage({
channel,
Expand All @@ -142,7 +166,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro

useUpdateEffect(() => {
if (methodValue) {
reset(getDefaultValues(methodValue, config));
reset(getDefaultValues(methodValue, config, address || hash));

const methodName = METHOD_LABELS[methodValue];
mixpanel.logEvent(mixpanel.EventTypes.CONTRACT_VERIFICATION, { Status: 'Method selected', Method: methodName });
Expand All @@ -157,11 +181,14 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
noValidate
onSubmit={ handleSubmit(onFormSubmit) }
>
<ContractVerificationFieldMethod
control={ control }
methods={ config.verification_options }
isDisabled={ formState.isSubmitting }
/>
<Grid as="section" columnGap="30px" rowGap={{ base: 2, lg: 5 }} templateColumns={{ base: '1fr', lg: 'minmax(auto, 680px) minmax(0, 340px)' }}>
{ !hash && <ContractVerificationFieldAddress/> }
<ContractVerificationFieldMethod
control={ control }
methods={ config.verification_options }
isDisabled={ formState.isSubmitting }
/>
</Grid>
{ content }
{ Boolean(method) && (
<Button
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { FormControl, Input, chakra } from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';

import type { FormFields } from '../types';

import { ADDRESS_REGEXP, ADDRESS_LENGTH } from 'lib/validations/address';
import InputPlaceholder from 'ui/shared/InputPlaceholder';

import ContractVerificationFormRow from '../ContractVerificationFormRow';

interface Props {
isReadOnly?: boolean;
}

const ContractVerificationFieldAddress = ({ isReadOnly }: Props) => {
const { formState, control } = useFormContext<FormFields>();

const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'address'>}) => {
const error = 'address' in formState.errors ? formState.errors.address : undefined;

return (
<FormControl variant="floating" id={ field.name } isRequired size={{ base: 'md', lg: 'lg' }}>
<Input
{ ...field }
required
isInvalid={ Boolean(error) }
maxLength={ ADDRESS_LENGTH }
isDisabled={ formState.isSubmitting || isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Smart contract / Address (0x...)" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting, isReadOnly ]);

return (
<>
<ContractVerificationFormRow>
<chakra.span fontWeight={ 500 } fontSize="lg" fontFamily="heading">
Contract address to verify
</chakra.span>
</ContractVerificationFormRow>
<ContractVerificationFormRow mb={ 3 }>
<Controller
name="address"
control={ control }
render={ renderControl }
rules={{ required: true, pattern: ADDRESS_REGEXP }}
/>
</ContractVerificationFormRow>
</>
);
};

export default React.memo(ContractVerificationFieldAddress);
Loading

0 comments on commit 30db8f2

Please sign in to comment.