Commit be2ce560 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into refactor-address

parents 7c3797ef 0cfe93cf
API_AUTHORIZATION_TOKEN=xxx
SENTRY_DSN=xxx SENTRY_DSN=xxx
NEXT_PUBLIC_SENTRY_DSN=xxx NEXT_PUBLIC_SENTRY_DSN=xxx
SENTRY_ORG=block-scout SENTRY_ORG=block-scout
...@@ -11,4 +10,4 @@ NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout ...@@ -11,4 +10,4 @@ NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_SUPPORTED_NETWORKS=[{"name":"Gnosis Chain","type":"xdai","subType":"mainnet","group":"mainnets","isAccountSupported":true},{"name":"Optimism on Gnosis Chain","shortName":"OoG","type":"xdai","subType":"optimism","group":"mainnets","icon":"https://www.fillmurray.com/60/60"},{"name":"Arbitrum on xDai","type":"xdai","subType":"aox","group":"mainnets"},{"name":"Ethereum","shortName":"ETH","type":"eth","subType":"mainnet","group":"mainnets"},{"name":"Ethereum Classic","shortName":"ETC","type":"etc","subType":"mainnet","group":"mainnets"},{"name":"POA","shortName":"POA","type":"poa","subType":"core","group":"mainnets"},{"name":"RSK","shortName":"RBTC","type":"rsk","subType":"mainnet","group":"mainnets"},{"name":"Gnosis Chain Testnet","type":"xdai","subType":"testnet","group":"testnets","isAccountSupported":true},{"name":"POA Sokol","shortName":"POA","type":"poa","subType":"sokol","group":"testnets"},{"name":"ARTIS Σ1","type":"artis","subType":"sigma1","group":"other"},{"name":"LUKSO L14","shortName":"POA","type":"lukso","subType":"l14","group":"other"}] NEXT_PUBLIC_SUPPORTED_NETWORKS=[{"name":"Gnosis Chain","type":"xdai","subType":"mainnet","group":"mainnets","isAccountSupported":true},{"name":"Optimism on Gnosis Chain","shortName":"OoG","type":"xdai","subType":"optimism","group":"mainnets","icon":"https://www.fillmurray.com/60/60"},{"name":"Arbitrum on xDai","type":"xdai","subType":"aox","group":"mainnets"},{"name":"Ethereum","shortName":"ETH","type":"eth","subType":"mainnet","group":"mainnets"},{"name":"Ethereum Classic","shortName":"ETC","type":"etc","subType":"mainnet","group":"mainnets"},{"name":"POA","shortName":"POA","type":"poa","subType":"core","group":"mainnets","isAccountSupported":true},{"name":"RSK","shortName":"RBTC","type":"rsk","subType":"mainnet","group":"mainnets"},{"name":"Gnosis Chain Testnet","type":"xdai","subType":"testnet","group":"testnets"},{"name":"POA Sokol","shortName":"POA","type":"poa","subType":"sokol","group":"testnets"},{"name":"ARTIS Σ1","type":"artis","subType":"sigma1","group":"other"},{"name":"LUKSO L14","shortName":"POA","type":"lukso","subType":"l14","group":"other"}]
\ No newline at end of file \ No newline at end of file
export const privateTagsTransaction = [
{
transaction: '0x44b51ef7746ff48f74f45699d33557faa96059eb8655fdd7bf14a5f529ea3528',
tag: 'some_tag',
},
{
transaction: '0x44b51ef7746ff48f74f45699d33557faa96059eb8655fdd7bf14a5f529ea9999',
tag: 'some_other_tag',
},
];
export type TPrivateTagsTransaction = Array<TPrivateTagsTransactionItem>
export type TPrivateTagsTransactionItem = {
transaction: string;
tag: string;
}
export const publicTags = [
{
addresses: [
{
address: '0x35317007D203b8a86CA727ad44E473E40450E377',
addressName: 'DarkForest',
},
{
address: '0x35317007D203b8a86CA727ad44E473E40450E378',
addressName: 'DarkForest2',
},
],
tags: [
{
name: 'darkforest',
// colorHex: '#4A5568',
// backgroundHex: '#E2E8F0',
},
],
date: 'Jun 10, 2022',
id: '123',
userName: 'Tatyana',
userEmail: 'sample@gmail.com',
companyName: 'Contract name',
companyUrl: 'contractname.com',
comment: 'Please use #ED8936 color for tag...',
},
{
addresses: [
{
address: '0x35317007D203b8a86CA727ad44E473E40450E377',
},
],
tags: [
{
name: 'OMNI',
colorHex: '#FFFFFF',
backgroundHex: '#1A202C',
},
{
name: '123456789012345678901237123123',
colorHex: '#FFFFFF',
backgroundHex: '#6B46C1',
},
],
date: 'Jun 5, 2022',
id: '456',
},
{
addresses: [
{
address: '0x35317007D203b8a86CA727ad44E473E40450E377',
addressName: 'Contract name',
},
],
tags: [
{
name: 'SANA',
colorHex: '#FFFFFF',
backgroundHex: '#ED8936',
},
],
date: 'Jun 1, 2022',
id: '789',
},
];
export type TPublicTags = Array<TPublicTagItem>
export type TPublicTagItem = {
addresses: Array<TPublicTagAddress>;
tags: Array<TPublicTag>;
// status: typeof STATUS;
date: string;
// id is for react element key, as tag or address may not be unique
id: string;
userName?: string;
userEmail?: string;
companyName?: string;
companyUrl?: string;
comment?: string;
}
export type TPublicTagAddress = {
address: string;
addressName?: string;
}
export type TPublicTag = {
name: string;
colorHex?: string;
backgroundHex?: string;
}
export const data = [
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
miner: 'KuCoin Pool',
after: {
balance: '0.012192910371186045',
nonce: '4',
},
before: {
balance: '0.008350264867549483',
nonce: '5',
},
diff: '0.003842645503636562',
storage: [
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
before: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
after: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
},
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
before: '0x730bc43aac5a6cf94a72f69a42adfa114fe119b5',
after: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
},
],
},
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
miner: 'KuCoin Pool',
after: {
balance: '0.012192910371186045',
nonce: '4',
},
before: {
balance: '0.008350264867549483',
nonce: '5',
},
diff: '0.003842645503636562',
storage: [
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
before: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
after: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
},
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
before: '0x730bc43aac5a6cf94a72f69a42adfa114fe119b5',
after: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
},
],
},
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
miner: 'KuCoin Pool',
after: {
balance: '0.012192910371186045',
},
before: {
balance: '0.008350264867549483',
},
diff: '-0.003842645503636562',
},
];
export type TTxState = Array<TTxStateItem>;
export type TTxStateItem = {
address: string;
miner: string;
after: {
balance: string;
nonce?: string;
};
before: {
balance: string;
nonce?: string;
};
diff: string;
storage?: Array<TTxStateItemStorage>;
}
export type TTxStateItemStorage = {
address: string;
before: string;
after: string;
}
export const watchlist = [
{
address: '0x4831c121879d3de0e2b181d9d55e9b0724f5d926',
tokenBalance: 100.1,
tokenBalanceUSD: 101.2,
tokensAmount: 2,
tokensUSD: 202.2,
totalUSD: 123123,
tag: 'some_tag',
notification: true,
},
{
address: '0x8c461F78760988c4135e363a87dd736f8b671ff0',
tokensAmount: 2,
tokensUSD: 2202.2,
totalUSD: 3000.5,
tag: 'some_other_tag',
notification: false,
},
{
address: '0x930F381E649c84579Ef58117E923714964C55D16',
tokenBalance: 200.2,
tokenBalanceUSD: 202.4,
totalUSD: 3000.5,
tag: '12345678901234567890123456789012345',
notification: false,
},
];
export type TWatchlist = Array<TWatchlistItem>
export type TWatchlistItem = {
address: string;
tokenBalance?: number;
tokenBalanceUSD?: number;
tokensAmount?: number;
tokensUSD?: number;
totalUSD?: number;
tag: string;
notification?: boolean;
}
import type { NextApiRequest } from 'next';
import type { RequestInit, Response } from 'node-fetch'; import type { RequestInit, Response } from 'node-fetch';
import nodeFetch from 'node-fetch'; import nodeFetch from 'node-fetch';
import * as cookies from 'lib/cookies';
// first arg can be only a string // first arg can be only a string
// FIXME migrate to RequestInfo later if needed // FIXME migrate to RequestInfo later if needed
export default function fetch(path: string, init?: RequestInit): Promise<Response> { export default function fetchFactory(_req: NextApiRequest) {
const headers = { return function fetch(path: string, init?: RequestInit): Promise<Response> {
accept: 'application/json', const headers = {
authorization: `Bearer ${ process.env.API_AUTHORIZATION_TOKEN }`, accept: 'application/json',
'content-type': 'application/json', 'content-type': 'application/json',
}; cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`,
const url = `https://blockscout.com${ path }`; };
const url = `https://blockscout.com${ path }`;
return nodeFetch(url, { return nodeFetch(url, {
headers, headers,
...init, ...init,
}); });
};
} }
import { withSentry } from '@sentry/nextjs'; import { withSentry } from '@sentry/nextjs';
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import fetch from 'lib/api/fetch'; import fetchFactory from 'lib/api/fetch';
import getUrlWithNetwork from 'lib/api/getUrlWithNetwork'; import getUrlWithNetwork from 'lib/api/getUrlWithNetwork';
type Methods = 'GET' | 'POST' | 'PUT' | 'DELETE'; type Methods = 'GET' | 'POST' | 'PUT' | 'DELETE';
...@@ -16,7 +16,8 @@ export default function createHandler(getUrl: (_req: NextApiRequest) => string, ...@@ -16,7 +16,8 @@ export default function createHandler(getUrl: (_req: NextApiRequest) => string,
const isBodyDisallowed = _req.method === 'GET' || _req.method === 'HEAD'; const isBodyDisallowed = _req.method === 'GET' || _req.method === 'HEAD';
const url = getUrlWithNetwork(_req, `/api${ getUrl(_req) }`); const url = getUrlWithNetwork(_req, `api${ getUrl(_req) }`);
const fetch = fetchFactory(_req);
const response = await fetch(url, { const response = await fetch(url, {
method: _req.method, method: _req.method,
body: isBodyDisallowed ? undefined : _req.body, body: isBodyDisallowed ? undefined : _req.body,
...@@ -29,12 +30,13 @@ export default function createHandler(getUrl: (_req: NextApiRequest) => string, ...@@ -29,12 +30,13 @@ export default function createHandler(getUrl: (_req: NextApiRequest) => string,
} }
let responseError; let responseError;
const defaultError = { statusText: response.statusText, status: response.status };
try { try {
const error = await response.json() as { errors: unknown }; const error = await response.json() as { errors: unknown };
responseError = error?.errors || {}; responseError = error?.errors || defaultError;
} catch (error) { } catch (error) {
responseError = { statusText: response.statusText, status: response.status }; responseError = defaultError;
} }
res.status(500).json(responseError); res.status(500).json(responseError);
......
import * as Sentry from '@sentry/nextjs';
export interface ErrorType<T> {
error?: T;
status: Response['status'];
statusText: Response['statusText'];
}
export default function clientFetch<Success, Error>(path: string, init?: RequestInit): Promise<Success | ErrorType<Error>> {
return fetch(path, init).then(response => {
if (!response.ok) {
return response.json().then(
(jsonError) => Promise.reject({
error: jsonError as Error,
status: response.status,
statusText: response.statusText,
}),
() => {
const error = {
status: response.status,
statusText: response.statusText,
};
Sentry.captureException(new Error('Client fetch failed'), { extra: error, tags: { source: 'fetch' } });
return Promise.reject(error);
},
);
} else {
return response.json() as Promise<Success>;
}
});
}
...@@ -7,6 +7,7 @@ export enum NAMES { ...@@ -7,6 +7,7 @@ export enum NAMES {
NAV_BAR_COLLAPSED='nav_bar_collapsed', NAV_BAR_COLLAPSED='nav_bar_collapsed',
NETWORK_TYPE='network_type', NETWORK_TYPE='network_type',
NETWORK_SUB_TYPE='network_sub_type', NETWORK_SUB_TYPE='network_sub_type',
API_TOKEN='_explorer_key',
} }
export function get(name?: string | undefined | null) { export function get(name?: string | undefined | null) {
......
import * as Sentry from '@sentry/nextjs';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import type { CsrfData } from 'types/client/account';
export interface ErrorType<T> {
error?: T;
status: Response['status'];
statusText: Response['statusText'];
}
interface Params {
method?: RequestInit['method'];
body?: Record<string, unknown>;
}
export default function useFetch() {
const queryClient = useQueryClient();
const { token } = queryClient.getQueryData<CsrfData>([ 'csrf' ]) || {};
return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ErrorType<Error>> => {
const reqParams = {
...params,
body: params?.method && ![ 'GET', 'HEAD' ].includes(params.method) ?
JSON.stringify({ ...params?.body, _csrf_token: token }) :
undefined,
};
return fetch(path, reqParams).then(response => {
if (!response.ok) {
return response.json().then(
(jsonError) => Promise.reject({
error: jsonError as Error,
status: response.status,
statusText: response.statusText,
}),
() => {
const error = {
status: response.status,
statusText: response.statusText,
};
Sentry.captureException(new Error('Client fetch failed'), { extra: error, tags: { source: 'fetch' } });
return Promise.reject(error);
},
);
} else {
return response.json() as Promise<Success>;
}
});
}, [ token ]);
}
...@@ -2,10 +2,21 @@ import { useQuery } from '@tanstack/react-query'; ...@@ -2,10 +2,21 @@ import { useQuery } from '@tanstack/react-query';
import type { UserInfo } from 'types/api/account'; import type { UserInfo } from 'types/api/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
interface Error {
error?: {
status?: number;
statusText?: string;
};
}
export default function useFetchProfileInfo() { export default function useFetchProfileInfo() {
return useQuery<unknown, unknown, UserInfo>([ 'profile' ], async() => { const fetch = useFetch();
return useQuery<unknown, Error, UserInfo>([ 'profile' ], async() => {
return fetch('/api/account/profile'); return fetch('/api/account/profile');
}, { refetchOnMount: false }); }, {
refetchOnMount: false,
});
} }
...@@ -6,7 +6,7 @@ export default function useNetwork() { ...@@ -6,7 +6,7 @@ export default function useNetwork() {
const router = useRouter(); const router = useRouter();
const selectedNetwork = findNetwork({ const selectedNetwork = findNetwork({
network_type: typeof router.query.network_type === 'string' ? router.query.network_type : '', network_type: typeof router.query.network_type === 'string' ? router.query.network_type : '',
network_sub_type: typeof router.query.network_type === 'string' ? router.query.network_type : undefined, network_sub_type: typeof router.query.network_sub_type === 'string' ? router.query.network_sub_type : undefined,
}); });
return selectedNetwork; return selectedNetwork;
} }
...@@ -101,6 +101,12 @@ export const ROUTES = { ...@@ -101,6 +101,12 @@ export const ROUTES = {
other: { other: {
pattern: `${ BASE_PATH }/search-results`, pattern: `${ BASE_PATH }/search-results`,
}, },
// AUTH
auth: {
// no slash required, it is correct
pattern: `${ BASE_PATH }auth/auth0`,
},
}; };
// !!! for development purpose only !!! // !!! for development purpose only !!!
......
const { NextResponse } = require('next/server');
const { NAMES } = require('lib/cookies');
const { link } = require('lib/link/link');
const findNetwork = require('lib/networks/findNetwork').default;
export function middleware(req) {
const [ , networkType, networkSubtype ] = req.nextUrl.pathname.split('/');
const networkParams = {
network_type: networkType,
network_sub_type: networkSubtype,
};
const selectedNetwork = findNetwork(networkParams);
if (selectedNetwork) {
const apiToken = req.cookies.get(NAMES.API_TOKEN);
if (!apiToken) {
const authUrl = link('auth', networkParams);
return NextResponse.redirect(authUrl);
}
}
}
export const config = {
matcher: '/:network_type/:network_sub_type/account/:path*',
};
...@@ -22,8 +22,8 @@ const moduleExports = { ...@@ -22,8 +22,8 @@ const moduleExports = {
return [ return [
{ {
source: '/', source: '/',
destination: '/xdai/testnet', destination: '/poa/core',
permanent: true, permanent: false,
}, },
]; ];
}, },
......
import { Center, VStack, Box } from '@chakra-ui/react'; import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Link, Code } from '@chakra-ui/react';
import type { NextPage, GetStaticPaths } from 'next'; import type { NextPage, GetStaticPaths } from 'next';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
import * as cookies from 'lib/cookies';
import useNetwork from 'lib/hooks/useNetwork';
import useToast from 'lib/hooks/useToast';
import getAvailablePaths from 'lib/networks/getAvailablePaths'; import getAvailablePaths from 'lib/networks/getAvailablePaths';
import Page from 'ui/shared/Page'; import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
const Home: NextPage = () => { const Home: NextPage = () => {
const router = useRouter(); const router = useRouter();
const selectedNetwork = useNetwork();
const toast = useToast();
const [ isFormVisible, setFormVisibility ] = React.useState(false);
const [ token, setToken ] = React.useState('');
React.useEffect(() => {
const token = cookies.get(cookies.NAMES.API_TOKEN);
setFormVisibility(Boolean(!token && selectedNetwork?.isAccountSupported));
}, [ selectedNetwork?.isAccountSupported ]);
const handleTokenChange = React.useCallback((event: ChangeEvent<HTMLTextAreaElement>) => {
setToken(event.target.value);
}, []);
const handleSetTokenClick = React.useCallback(() => {
cookies.set(cookies.NAMES.API_TOKEN, token);
setToken('');
toast({
position: 'top-right',
title: 'Success 🥳',
description: 'Successfully set cookie',
status: 'success',
variant: 'subtle',
isClosable: true,
onCloseComplete: () => {
setFormVisibility(false);
},
});
}, [ toast, token ]);
const prodUrl = new URL(`/${ router.query.network_type }/${ router.query.network_sub_type }`, 'https://blockscout.com').toString();
return ( return (
<Page> <Page>
<Center h="100%" fontSize={{ base: 'sm', lg: 'xl' }}> <VStack gap={ 4 } alignItems="flex-start" maxW="800px">
<VStack gap={ 4 }> <PageHeader text={
<Box> `Home Page for ${ selectedNetwork?.name } network`
<p>home page for { router.query.network_type } { router.query.network_sub_type } network</p> }/>
</Box> { /* will be deleted when we move to new CI */ }
</VStack> { isFormVisible && (
</Center> <>
<Alert status="error" flexDirection="column" alignItems="flex-start">
<AlertTitle fontSize="md">
!!! Temporary solution for authentication !!!
</AlertTitle>
<AlertDescription mt={ 3 }>
To Sign in go to <Link href={ prodUrl } target="_blank">{ prodUrl }</Link> first, sign in there, copy obtained API token from cookie
<Code ml={ 1 }>{ cookies.NAMES.API_TOKEN }</Code> and paste it in the form below. After submitting the form you should be successfully
authenticated in current environment
</AlertDescription>
</Alert>
<Textarea value={ token } onChange={ handleTokenChange } placeholder="API token"/>
<Button onClick={ handleSetTokenClick }>Set cookie</Button>
</>
) }
</VStack>
</Page> </Page>
); );
}; };
......
...@@ -4,6 +4,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; ...@@ -4,6 +4,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { AppProps } from 'next/app'; import type { AppProps } from 'next/app';
import React, { useState } from 'react'; import React, { useState } from 'react';
import type { ErrorType } from 'lib/hooks/useFetch';
import theme from 'theme'; import theme from 'theme';
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
...@@ -11,6 +12,16 @@ function MyApp({ Component, pageProps }: AppProps) { ...@@ -11,6 +12,16 @@ function MyApp({ Component, pageProps }: AppProps) {
defaultOptions: { defaultOptions: {
queries: { queries: {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
retry: (failureCount, _error) => {
const error = _error as ErrorType<{ status: number }>;
const status = error?.error?.status;
if (status && status >= 400 && status < 500) {
// don't do retry for client error responses
return false;
}
return failureCount < 2;
},
}, },
}, },
})); }));
......
import type { NextApiRequest, NextApiResponse } from 'next';
import fetchFactory from 'lib/api/fetch';
import getUrlWithNetwork from 'lib/api/getUrlWithNetwork';
export default async function csrfHandler(_req: NextApiRequest, res: NextApiResponse) {
const url = getUrlWithNetwork(_req, `api/account/v1/get_csrf`);
const fetch = fetchFactory(_req);
const response = await fetch(url);
if (response.status === 200) {
const token = response.headers.get('x-bs-account-csrf');
res.status(200).json({ token });
return;
}
res.status(500).json({ statusText: response.statusText, status: response.status });
}
...@@ -4,10 +4,11 @@ import type { WatchlistAddresses } from 'types/api/account'; ...@@ -4,10 +4,11 @@ import type { WatchlistAddresses } from 'types/api/account';
import type { Tokenlist } from 'types/api/tokenlist'; import type { Tokenlist } from 'types/api/tokenlist';
import type { TWatchlistItem } from 'types/client/account'; import type { TWatchlistItem } from 'types/client/account';
import fetch from 'lib/api/fetch'; import fetchFactory from 'lib/api/fetch';
import getUrlWithNetwork from 'lib/api/getUrlWithNetwork'; import getUrlWithNetwork from 'lib/api/getUrlWithNetwork';
const watchlistWithTokensHandler = async(_req: NextApiRequest, res: NextApiResponse<Array<TWatchlistItem>>) => { const watchlistWithTokensHandler = async(_req: NextApiRequest, res: NextApiResponse<Array<TWatchlistItem>>) => {
const fetch = fetchFactory(_req);
const url = getUrlWithNetwork(_req, 'api/account/v1/user/watchlist'); const url = getUrlWithNetwork(_req, 'api/account/v1/user/watchlist');
const watchlistResponse = await fetch(url, { method: 'GET' }); const watchlistResponse = await fetch(url, { method: 'GET' });
......
...@@ -75,6 +75,10 @@ const variantOutline = defineStyle((props) => { ...@@ -75,6 +75,10 @@ const variantOutline = defineStyle((props) => {
borderColor: props.isActive ? activeBg : 'blue.400', borderColor: props.isActive ? activeBg : 'blue.400',
color: props.isActive ? activeColor : 'blue.400', color: props.isActive ? activeColor : 'blue.400',
}, },
_disabled: {
color,
borderColor,
},
}, },
_disabled: { _disabled: {
opacity: 0.2, opacity: 0.2,
...@@ -83,6 +87,10 @@ const variantOutline = defineStyle((props) => { ...@@ -83,6 +87,10 @@ const variantOutline = defineStyle((props) => {
bg: activeBg, bg: activeBg,
borderColor: activeBg, borderColor: activeBg,
color: activeColor, color: activeColor,
_disabled: {
color,
borderColor,
},
}, },
}; };
}); });
......
...@@ -40,6 +40,19 @@ const sizes = { ...@@ -40,6 +40,19 @@ const sizes = {
py: 6, py: 6,
}, },
}), }),
sm: definePartsStyle({
th: {
px: '10px',
py: '10px',
fontSize: 'sm',
},
td: {
px: '10px',
py: 6,
fontSize: 'sm',
fontWeight: 500,
},
}),
}; };
const variants = { const variants = {
......
...@@ -3,3 +3,7 @@ import type { WatchlistAddress } from '../api/account'; ...@@ -3,3 +3,7 @@ import type { WatchlistAddress } from '../api/account';
export type TWatchlistItem = WatchlistAddress & {tokens_count: number}; export type TWatchlistItem = WatchlistAddress & {tokens_count: number};
export type TWatchlist = Array<TWatchlistItem>; export type TWatchlist = Array<TWatchlistItem>;
export interface CsrfData {
token: string;
}
...@@ -13,10 +13,10 @@ import { useForm, Controller } from 'react-hook-form'; ...@@ -13,10 +13,10 @@ import { useForm, Controller } from 'react-hook-form';
import type { ApiKey, ApiKeys, ApiKeyErrors } from 'types/api/account'; import type { ApiKey, ApiKeys, ApiKeyErrors } from 'types/api/account';
import type { ErrorType } from 'lib/client/fetch';
import fetch from 'lib/client/fetch';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import getPlaceholderWithError from 'lib/getPlaceholderWithError'; import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
type Props = { type Props = {
data?: ApiKey; data?: ApiKey;
...@@ -39,11 +39,12 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -39,11 +39,12 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
name: data?.name || '', name: data?.name || '',
}, },
}); });
const fetch = useFetch();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const formBackgroundColor = useColorModeValue('white', 'gray.900'); const formBackgroundColor = useColorModeValue('white', 'gray.900');
const updateApiKey = (data: Inputs) => { const updateApiKey = (data: Inputs) => {
const body = JSON.stringify({ name: data.name }); const body = { name: data.name };
if (!data.token) { if (!data.token) {
return fetch('/api/account/api-keys', { method: 'POST', body }); return fetch('/api/account/api-keys', { method: 'POST', body });
......
...@@ -4,7 +4,7 @@ import React, { useCallback } from 'react'; ...@@ -4,7 +4,7 @@ import React, { useCallback } from 'react';
import type { ApiKey, ApiKeys } from 'types/api/account'; import type { ApiKey, ApiKeys } from 'types/api/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
import DeleteModal from 'ui/shared/DeleteModal'; import DeleteModal from 'ui/shared/DeleteModal';
type Props = { type Props = {
...@@ -15,10 +15,11 @@ type Props = { ...@@ -15,10 +15,11 @@ type Props = {
const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => { const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const fetch = useFetch();
const mutationFn = useCallback(() => { const mutationFn = useCallback(() => {
return fetch(`/api/account/api-keys/${ data.api_key }`, { method: 'DELETE' }); return fetch(`/api/account/api-keys/${ data.api_key }`, { method: 'DELETE' });
}, [ data ]); }, [ data.api_key, fetch ]);
const onSuccess = useCallback(async() => { const onSuccess = useCallback(async() => {
queryClient.setQueryData([ 'api-keys' ], (prevData: ApiKeys | undefined) => { queryClient.setQueryData([ 'api-keys' ], (prevData: ApiKeys | undefined) => {
......
...@@ -39,7 +39,7 @@ const Burger = () => { ...@@ -39,7 +39,7 @@ const Burger = () => {
> >
<DrawerOverlay/> <DrawerOverlay/>
<DrawerContent maxWidth="260px"> <DrawerContent maxWidth="260px">
<DrawerBody p={ 6 }> <DrawerBody p={ 6 } display="flex" flexDirection="column">
<Flex alignItems="center" justifyContent="space-between"> <Flex alignItems="center" justifyContent="space-between">
<NetworkLogo onClick={ handleNetworkLogoClick }/> <NetworkLogo onClick={ handleNetworkLogoClick }/>
<NetworkMenuButton <NetworkMenuButton
......
...@@ -20,9 +20,10 @@ const VERSION_URL = `https://github.com/blockscout/blockscout/tree/${ BLOCKSCOUT ...@@ -20,9 +20,10 @@ const VERSION_URL = `https://github.com/blockscout/blockscout/tree/${ BLOCKSCOUT
interface Props { interface Props {
isCollapsed?: boolean; isCollapsed?: boolean;
hasAccount?: boolean;
} }
const NavFooter = ({ isCollapsed }: Props) => { const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const width = (() => { const width = (() => {
...@@ -33,6 +34,14 @@ const NavFooter = ({ isCollapsed }: Props) => { ...@@ -33,6 +34,14 @@ const NavFooter = ({ isCollapsed }: Props) => {
return isCollapsed ? '20px' : '180px'; return isCollapsed ? '20px' : '180px';
})(); })();
const marginTop = (() => {
if (!hasAccount) {
return 'auto';
}
return isMobile ? 6 : 20;
})();
return ( return (
<VStack <VStack
as="footer" as="footer"
...@@ -41,7 +50,7 @@ const NavFooter = ({ isCollapsed }: Props) => { ...@@ -41,7 +50,7 @@ const NavFooter = ({ isCollapsed }: Props) => {
borderColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') } borderColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
width={ width } width={ width }
paddingTop={ isMobile ? 6 : 8 } paddingTop={ isMobile ? 6 : 8 }
marginTop={ isMobile ? 6 : 20 } marginTop={ marginTop }
alignItems="flex-start" alignItems="flex-start"
alignSelf="center" alignSelf="center"
color="gray.500" color="gray.500"
......
...@@ -4,6 +4,7 @@ import React from 'react'; ...@@ -4,6 +4,7 @@ import React from 'react';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems'; import useNavItems from 'lib/hooks/useNavItems';
import useNetwork from 'lib/hooks/useNetwork';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NetworkLogo from 'ui/blocks/networkMenu/NetworkLogo'; import NetworkLogo from 'ui/blocks/networkMenu/NetworkLogo';
import NetworkMenu from 'ui/blocks/networkMenu/NetworkMenu'; import NetworkMenu from 'ui/blocks/networkMenu/NetworkMenu';
...@@ -13,16 +14,19 @@ import NavLink from './NavLink'; ...@@ -13,16 +14,19 @@ import NavLink from './NavLink';
const NavigationDesktop = () => { const NavigationDesktop = () => {
const { mainNavItems, accountNavItems } = useNavItems(); const { mainNavItems, accountNavItems } = useNavItems();
const selectedNetwork = useNetwork();
const isLargeScreen = useBreakpointValue({ base: false, xl: true }); const isLargeScreen = useBreakpointValue({ base: false, xl: true });
const cookieValue = cookies.get(cookies.NAMES.NAV_BAR_COLLAPSED); const navBarCollapsedCookie = cookies.get(cookies.NAMES.NAV_BAR_COLLAPSED);
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
const hasAccount = selectedNetwork?.isAccountSupported && isAuth;
const [ isCollapsed, setCollapsedState ] = React.useState(cookieValue === 'true'); const [ isCollapsed, setCollapsedState ] = React.useState(navBarCollapsedCookie === 'true');
React.useEffect(() => { React.useEffect(() => {
if (!cookieValue) { if (!navBarCollapsedCookie) {
setCollapsedState(!isLargeScreen); setCollapsedState(!isLargeScreen);
} }
}, [ isLargeScreen, cookieValue ]); }, [ isLargeScreen, navBarCollapsedCookie ]);
const handleTogglerClick = React.useCallback(() => { const handleTogglerClick = React.useCallback(() => {
setCollapsedState((flag) => !flag); setCollapsedState((flag) => !flag);
...@@ -66,12 +70,14 @@ const NavigationDesktop = () => { ...@@ -66,12 +70,14 @@ const NavigationDesktop = () => {
{ mainNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) } { mainNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) }
</VStack> </VStack>
</Box> </Box>
<Box as="nav" mt={ 12 }> { hasAccount && (
<VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden"> <Box as="nav" mt={ 12 }>
{ accountNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) } <VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden">
</VStack> { accountNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) }
</Box> </VStack>
<NavFooter isCollapsed={ isCollapsed }/> </Box>
) }
<NavFooter isCollapsed={ isCollapsed } hasAccount={ hasAccount }/>
<ChevronLeftIcon <ChevronLeftIcon
width={ 6 } width={ 6 }
height={ 6 } height={ 6 }
......
import { Box, VStack } from '@chakra-ui/react'; import { Box, VStack } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems'; import useNavItems from 'lib/hooks/useNavItems';
import useNetwork from 'lib/hooks/useNetwork';
import NavFooter from 'ui/blocks/navigation/NavFooter'; import NavFooter from 'ui/blocks/navigation/NavFooter';
import NavLink from 'ui/blocks/navigation/NavLink'; import NavLink from 'ui/blocks/navigation/NavLink';
const NavigationMobile = () => { const NavigationMobile = () => {
const { mainNavItems, accountNavItems } = useNavItems(); const { mainNavItems, accountNavItems } = useNavItems();
const selectedNetwork = useNetwork();
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
const hasAccount = selectedNetwork?.isAccountSupported && isAuth;
return ( return (
<> <>
...@@ -15,12 +21,14 @@ const NavigationMobile = () => { ...@@ -15,12 +21,14 @@ const NavigationMobile = () => {
{ mainNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) } { mainNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) }
</VStack> </VStack>
</Box> </Box>
<Box as="nav" mt={ 6 }> { isAuth && (
<VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden"> <Box as="nav" mt={ 6 }>
{ accountNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) } <VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden">
</VStack> { accountNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) }
</Box> </VStack>
<NavFooter/> </Box>
) }
<NavFooter hasAccount={ hasAccount }/>
</> </>
); );
}; };
......
...@@ -14,10 +14,10 @@ import { useForm, Controller } from 'react-hook-form'; ...@@ -14,10 +14,10 @@ import { useForm, Controller } from 'react-hook-form';
import type { CustomAbi, CustomAbis, CustomAbiErrors } from 'types/api/account'; import type { CustomAbi, CustomAbis, CustomAbiErrors } from 'types/api/account';
import type { ErrorType } from 'lib/client/fetch';
import fetch from 'lib/client/fetch';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import getPlaceholderWithError from 'lib/getPlaceholderWithError'; import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import { ADDRESS_REGEXP } from 'lib/validations/address'; import { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput'; import AddressInput from 'ui/shared/AddressInput';
...@@ -46,9 +46,10 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -46,9 +46,10 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
}); });
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const fetch = useFetch();
const customAbiKey = (data: Inputs & { id?: number }) => { const customAbiKey = (data: Inputs & { id?: number }) => {
const body = JSON.stringify({ name: data.name, contract_address_hash: data.contract_address_hash, abi: data.abi }); const body = { name: data.name, contract_address_hash: data.contract_address_hash, abi: data.abi };
if (!data.id) { if (!data.id) {
return fetch<CustomAbi, CustomAbiErrors>('/api/account/custom-abis', { method: 'POST', body }); return fetch<CustomAbi, CustomAbiErrors>('/api/account/custom-abis', { method: 'POST', body });
......
...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react'; ...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { ApiKey, ApiKeys } from 'types/api/account'; import type { ApiKey, ApiKeys } from 'types/api/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
import ApiKeyModal from 'ui/apiKey/ApiKeyModal/ApiKeyModal'; import ApiKeyModal from 'ui/apiKey/ApiKeyModal/ApiKeyModal';
...@@ -25,6 +25,7 @@ const ApiKeysPage: React.FC = () => { ...@@ -25,6 +25,7 @@ const ApiKeysPage: React.FC = () => {
const apiKeyModalProps = useDisclosure(); const apiKeyModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>(); const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>();
const [ deleteModalData, setDeleteModalData ] = useState<ApiKey>(); const [ deleteModalData, setDeleteModalData ] = useState<ApiKey>();
......
...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react'; ...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { CustomAbi, CustomAbis } from 'types/api/account'; import type { CustomAbi, CustomAbis } from 'types/api/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import CustomAbiModal from 'ui/customAbi/CustomAbiModal/CustomAbiModal'; import CustomAbiModal from 'ui/customAbi/CustomAbiModal/CustomAbiModal';
import CustomAbiListItem from 'ui/customAbi/CustomAbiTable/CustomAbiListItem'; import CustomAbiListItem from 'ui/customAbi/CustomAbiTable/CustomAbiListItem';
...@@ -22,6 +22,7 @@ const CustomAbiPage: React.FC = () => { ...@@ -22,6 +22,7 @@ const CustomAbiPage: React.FC = () => {
const customAbiModalProps = useDisclosure(); const customAbiModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
const [ customAbiModalData, setCustomAbiModalData ] = useState<CustomAbi>(); const [ customAbiModalData, setCustomAbiModalData ] = useState<CustomAbi>();
const [ deleteModalData, setDeleteModalData ] = useState<CustomAbi>(); const [ deleteModalData, setDeleteModalData ] = useState<CustomAbi>();
......
...@@ -16,6 +16,7 @@ import TxDetails from 'ui/tx/TxDetails'; ...@@ -16,6 +16,7 @@ import TxDetails from 'ui/tx/TxDetails';
import TxInternals from 'ui/tx/TxInternals'; import TxInternals from 'ui/tx/TxInternals';
import TxLogs from 'ui/tx/TxLogs'; import TxLogs from 'ui/tx/TxLogs';
import TxRawTrace from 'ui/tx/TxRawTrace'; import TxRawTrace from 'ui/tx/TxRawTrace';
import TxState from 'ui/tx/TxState';
interface Tab { interface Tab {
type: 'details' | 'internal_txn' | 'logs' | 'raw_trace' | 'state'; type: 'details' | 'internal_txn' | 'logs' | 'raw_trace' | 'state';
...@@ -29,7 +30,7 @@ const TABS: Array<Tab> = [ ...@@ -29,7 +30,7 @@ const TABS: Array<Tab> = [
{ type: 'details', routeName: 'tx_index', name: 'Details', component: <TxDetails/> }, { type: 'details', routeName: 'tx_index', name: 'Details', component: <TxDetails/> },
{ type: 'internal_txn', routeName: 'tx_internal', name: 'Internal txn', component: <TxInternals/> }, { type: 'internal_txn', routeName: 'tx_internal', name: 'Internal txn', component: <TxInternals/> },
{ type: 'logs', routeName: 'tx_logs', name: 'Logs', component: <TxLogs/> }, { type: 'logs', routeName: 'tx_logs', name: 'Logs', component: <TxLogs/> },
{ type: 'state', routeName: 'tx_state', name: 'State' }, { type: 'state', routeName: 'tx_state', name: 'State', component: <TxState/> },
{ type: 'raw_trace', routeName: 'tx_raw_trace', name: 'Raw trace', component: <TxRawTrace/> }, { type: 'raw_trace', routeName: 'tx_raw_trace', name: 'Raw trace', component: <TxRawTrace/> },
]; ];
......
...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react'; ...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { TWatchlist, TWatchlistItem } from 'types/client/account'; import type { TWatchlist, TWatchlistItem } from 'types/client/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
...@@ -24,6 +24,7 @@ const WatchList: React.FC = () => { ...@@ -24,6 +24,7 @@ const WatchList: React.FC = () => {
const addressModalProps = useDisclosure(); const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
const [ addressModalData, setAddressModalData ] = useState<TWatchlistItem>(); const [ addressModalData, setAddressModalData ] = useState<TWatchlistItem>();
const [ deleteModalData, setDeleteModalData ] = useState<TWatchlistItem>(); const [ deleteModalData, setDeleteModalData ] = useState<TWatchlistItem>();
......
...@@ -10,9 +10,9 @@ import { useForm, Controller } from 'react-hook-form'; ...@@ -10,9 +10,9 @@ import { useForm, Controller } from 'react-hook-form';
import type { AddressTag, AddressTagErrors } from 'types/api/account'; import type { AddressTag, AddressTagErrors } from 'types/api/account';
import type { ErrorType } from 'lib/client/fetch';
import fetch from 'lib/client/fetch';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import { ADDRESS_REGEXP } from 'lib/validations/address'; import { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput'; import AddressInput from 'ui/shared/AddressInput';
import TagInput from 'ui/shared/TagInput'; import TagInput from 'ui/shared/TagInput';
...@@ -31,6 +31,7 @@ type Inputs = { ...@@ -31,6 +31,7 @@ type Inputs = {
} }
const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const fetch = useFetch();
const [ pending, setPending ] = useState(false); const [ pending, setPending ] = useState(false);
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({ const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
mode: 'all', mode: 'all',
...@@ -45,10 +46,10 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -45,10 +46,10 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { mutate } = useMutation((formData: Inputs) => { const { mutate } = useMutation((formData: Inputs) => {
const body = JSON.stringify({ const body = {
name: formData?.tag, name: formData?.tag,
address_hash: formData?.address, address_hash: formData?.address,
}); };
const isEdit = data?.id; const isEdit = data?.id;
if (isEdit) { if (isEdit) {
......
...@@ -4,6 +4,7 @@ import React, { useCallback } from 'react'; ...@@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
import type { AddressTag, TransactionTag, AddressTags, TransactionTags } from 'types/api/account'; import type { AddressTag, TransactionTag, AddressTags, TransactionTags } from 'types/api/account';
import useFetch from 'lib/hooks/useFetch';
import DeleteModal from 'ui/shared/DeleteModal'; import DeleteModal from 'ui/shared/DeleteModal';
type Props = { type Props = {
...@@ -18,18 +19,19 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, data, type }) ...@@ -18,18 +19,19 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, data, type })
const id = data.id; const id = data.id;
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const fetch = useFetch();
const mutationFn = useCallback(() => { const mutationFn = useCallback(() => {
return fetch(`/api/account/private-tags/${ type }/${ id }`, { method: 'DELETE' }); return fetch(`/api/account/private-tags/${ type }/${ id }`, { method: 'DELETE' });
}, [ type, id ]); }, [ fetch, type, id ]);
const onSuccess = useCallback(async() => { const onSuccess = useCallback(async() => {
if (type === 'address') { if (type === 'address') {
queryClient.setQueryData([ type ], (prevData: AddressTags | undefined) => { queryClient.setQueryData([ 'address-tags' ], (prevData: AddressTags | undefined) => {
return prevData?.filter((item: AddressTag) => item.id !== id); return prevData?.filter((item: AddressTag) => item.id !== id);
}); });
} else { } else {
queryClient.setQueryData([ type ], (prevData: TransactionTags | undefined) => { queryClient.setQueryData([ 'transaction-tags' ], (prevData: TransactionTags | undefined) => {
return prevData?.filter((item: TransactionTag) => item.id !== id); return prevData?.filter((item: TransactionTag) => item.id !== id);
}); });
} }
......
...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react'; ...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { AddressTags, AddressTag } from 'types/api/account'; import type { AddressTags, AddressTag } from 'types/api/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
...@@ -23,6 +23,7 @@ const PrivateAddressTags = () => { ...@@ -23,6 +23,7 @@ const PrivateAddressTags = () => {
const addressModalProps = useDisclosure(); const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
const [ addressModalData, setAddressModalData ] = useState<AddressTag>(); const [ addressModalData, setAddressModalData ] = useState<AddressTag>();
const [ deleteModalData, setDeleteModalData ] = useState<AddressTag>(); const [ deleteModalData, setDeleteModalData ] = useState<AddressTag>();
......
...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react'; ...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { TransactionTags, TransactionTag } from 'types/api/account'; import type { TransactionTags, TransactionTag } from 'types/api/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
...@@ -23,6 +23,7 @@ const PrivateTransactionTags = () => { ...@@ -23,6 +23,7 @@ const PrivateTransactionTags = () => {
const transactionModalProps = useDisclosure(); const transactionModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
const [ transactionModalData, setTransactionModalData ] = useState<TransactionTag>(); const [ transactionModalData, setTransactionModalData ] = useState<TransactionTag>();
const [ deleteModalData, setDeleteModalData ] = useState<TransactionTag>(); const [ deleteModalData, setDeleteModalData ] = useState<TransactionTag>();
......
...@@ -10,9 +10,9 @@ import { useForm, Controller } from 'react-hook-form'; ...@@ -10,9 +10,9 @@ import { useForm, Controller } from 'react-hook-form';
import type { TransactionTag, TransactionTagErrors } from 'types/api/account'; import type { TransactionTag, TransactionTagErrors } from 'types/api/account';
import type { ErrorType } from 'lib/client/fetch';
import fetch from 'lib/client/fetch';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import { TRANSACTION_HASH_REGEXP } from 'lib/validations/transaction'; import { TRANSACTION_HASH_REGEXP } from 'lib/validations/transaction';
import TagInput from 'ui/shared/TagInput'; import TagInput from 'ui/shared/TagInput';
import TransactionInput from 'ui/shared/TransactionInput'; import TransactionInput from 'ui/shared/TransactionInput';
...@@ -43,12 +43,13 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => ...@@ -43,12 +43,13 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
}); });
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const fetch = useFetch();
const { mutate } = useMutation((formData: Inputs) => { const { mutate } = useMutation((formData: Inputs) => {
const body = JSON.stringify({ const body = {
name: formData?.tag, name: formData?.tag,
transaction_hash: formData?.transaction, transaction_hash: formData?.transaction,
}); };
const isEdit = data?.id; const isEdit = data?.id;
if (isEdit) { if (isEdit) {
......
...@@ -5,7 +5,7 @@ import type { ChangeEvent } from 'react'; ...@@ -5,7 +5,7 @@ import type { ChangeEvent } from 'react';
import type { PublicTags, PublicTag } from 'types/api/account'; import type { PublicTags, PublicTag } from 'types/api/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
import DeleteModal from 'ui/shared/DeleteModal'; import DeleteModal from 'ui/shared/DeleteModal';
type Props = { type Props = {
...@@ -22,12 +22,13 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete ...@@ -22,12 +22,13 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete
const tags = data.tags.split(';'); const tags = data.tags.split(';');
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const fetch = useFetch();
const formBackgroundColor = useColorModeValue('white', 'gray.900'); const formBackgroundColor = useColorModeValue('white', 'gray.900');
const deleteApiKey = useCallback(() => { const deleteApiKey = useCallback(() => {
const body = JSON.stringify({ remove_reason: reason }); const body = { remove_reason: reason };
return fetch(`/api/account/public-tags/${ data.id }`, { method: 'DELETE', body }); return fetch(`/api/account/public-tags/${ data.id }`, { method: 'DELETE', body });
}, [ data, reason ]); }, [ data.id, fetch, reason ]);
const onSuccess = useCallback(async() => { const onSuccess = useCallback(async() => {
onDeleteSuccess(); onDeleteSuccess();
......
...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react'; ...@@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react';
import type { PublicTags, PublicTag } from 'types/api/account'; import type { PublicTags, PublicTag } from 'types/api/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import PublicTagListItem from 'ui/publicTags/PublicTagTable/PublicTagListItem'; import PublicTagListItem from 'ui/publicTags/PublicTagTable/PublicTagListItem';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
...@@ -24,6 +24,7 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => { ...@@ -24,6 +24,7 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const [ deleteModalData, setDeleteModalData ] = useState<PublicTag>(); const [ deleteModalData, setDeleteModalData ] = useState<PublicTag>();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, PublicTags>([ 'public-tags' ], async() => await fetch('/api/account/public-tags')); const { data, isLoading, isError } = useQuery<unknown, unknown, PublicTags>([ 'public-tags' ], async() => await fetch('/api/account/public-tags'));
......
...@@ -13,9 +13,9 @@ import { useForm, useFieldArray } from 'react-hook-form'; ...@@ -13,9 +13,9 @@ 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';
import type { ErrorType } from 'lib/client/fetch';
import fetch from 'lib/client/fetch';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { EMAIL_REGEXP } from 'lib/validations/email'; import { EMAIL_REGEXP } from 'lib/validations/email';
import FormSubmitAlert from 'ui/shared/FormSubmitAlert'; import FormSubmitAlert from 'ui/shared/FormSubmitAlert';
...@@ -58,6 +58,7 @@ const ADDRESS_INPUT_BUTTONS_WIDTH = 100; ...@@ -58,6 +58,7 @@ const ADDRESS_INPUT_BUTTONS_WIDTH = 100;
const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
const inputSize = isMobile ? 'md' : 'lg'; const inputSize = isMobile ? 'md' : 'lg';
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({ const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
...@@ -87,7 +88,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -87,7 +88,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
const onRemoveFieldClick = useCallback((index: number) => () => remove(index), [ remove ]); const onRemoveFieldClick = useCallback((index: number) => () => remove(index), [ remove ]);
const updatePublicTag = (formData: Inputs) => { const updatePublicTag = (formData: Inputs) => {
const payload: PublicTagNew = { const body: PublicTagNew = {
full_name: formData.fullName || '', full_name: formData.fullName || '',
email: formData.email || '', email: formData.email || '',
company: formData.companyName || '', company: formData.companyName || '',
...@@ -97,7 +98,6 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -97,7 +98,6 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
tags: formData.tags?.split(';').map((s) => s.trim()).join(';') || '', tags: formData.tags?.split(';').map((s) => s.trim()).join(';') || '',
additional_comment: formData.comment || '', additional_comment: formData.comment || '',
}; };
const body = JSON.stringify(payload);
if (!data?.id) { if (!data?.id) {
return fetch<PublicTag, PublicTagErrors>('/api/account/public-tags', { method: 'POST', body }); return fetch<PublicTag, PublicTagErrors>('/api/account/public-tags', { method: 'POST', body });
......
import { Box, HStack, VStack } from '@chakra-ui/react'; import { Box, HStack, VStack } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import Header from 'ui/blocks/header/Header'; import Header from 'ui/blocks/header/Header';
import NavigationDesktop from 'ui/blocks/navigation/NavigationDesktop'; import NavigationDesktop from 'ui/blocks/navigation/NavigationDesktop';
...@@ -14,9 +16,13 @@ interface Props { ...@@ -14,9 +16,13 @@ interface Props {
const Page = ({ children }: Props) => { const Page = ({ children }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const router = useRouter(); const router = useRouter();
const fetch = useFetch();
const networkType = router.query.network_type; const networkType = router.query.network_type;
const networkSubType = router.query.network_sub_type; const networkSubType = router.query.network_sub_type;
useQuery<unknown, unknown, unknown>([ 'csrf' ], async() => await fetch('/api/account/csrf'));
React.useEffect(() => { React.useEffect(() => {
if (typeof networkType === 'string') { if (typeof networkType === 'string') {
cookies.set(cookies.NAMES.NETWORK_TYPE, networkType); cookies.set(cookies.NAMES.NETWORK_TYPE, networkType);
......
...@@ -10,7 +10,7 @@ const TxInternals = () => { ...@@ -10,7 +10,7 @@ const TxInternals = () => {
<Box> <Box>
<Filters/> <Filters/>
<TableContainer width="100%" mt={ 6 }> <TableContainer width="100%" mt={ 6 }>
<Table variant="simple" minWidth="950px"> <Table variant="simple" minWidth="950px" size="sm">
<Thead> <Thead>
<Tr> <Tr>
<Th width="20%">Type</Th> <Th width="20%">Type</Th>
......
import {
Accordion,
Text,
Table,
Thead,
Tbody,
Tr,
Th,
TableContainer,
} from '@chakra-ui/react';
import React from 'react';
import { data } from 'data/txState';
import TxStateTableItem from './state/TxStateTableItem';
const CURRENCY = 'ETH';
const TxState = () => {
return (
<>
<Text>
A set of information that represents the current state is updated when a transaction takes place on the network. The below is a summary of those changes
</Text>
<Accordion allowToggle allowMultiple>
<TableContainer width="100%" mt={ 6 }>
<Table variant="simple" minWidth="950px" size="sm">
<Thead>
<Tr>
<Th width="92px">Storage</Th>
<Th width="146px">Address</Th>
<Th width="120px">Miner</Th>
<Th width="33%" isNumeric>{ `After ${ CURRENCY }` }</Th>
<Th width="33%" isNumeric>{ `Before ${ CURRENCY }` }</Th>
<Th width="33%" isNumeric>{ `State difference ${ CURRENCY }` }</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item, index) => <TxStateTableItem txStateItem={ item } key={ index }/>) }
</Tbody>
</Table>
</TableContainer>
</Accordion>
</>
);
};
export default TxState;
import {
Grid,
GridItem,
Select,
useColorModeValue,
} from '@chakra-ui/react';
import React from 'react';
import type { TTxStateItemStorage } from 'data/txState';
const TxStateStorageItem = ({ storageItem }: {storageItem: TTxStateItemStorage}) => {
const gridData = [
{ name: 'Storage Address:', value: storageItem.address },
{ name: 'Before:', value: storageItem.before, select: true },
{ name: 'After:', value: storageItem.after, select: true },
];
const backgroundColor = useColorModeValue('white', 'gray.900');
const OPTIONS = [ 'Hex', 'Number', 'Text', 'Address' ];
return (
<Grid
gridTemplateColumns="auto 1fr"
columnGap={ 3 }
rowGap={ 4 }
px={ 6 }
py={ 4 }
background="blackAlpha.50"
borderRadius="12px"
mb={ 4 }
>
{ gridData.map((item) => (
<>
<GridItem alignSelf="center" fontWeight={ 600 } textAlign="end">{ item.name }</GridItem>
<GridItem>
{ item.select && (
<Select
size="sm"
borderRadius="base"
focusBorderColor="none"
display="inline-block"
w="auto"
mr={ 3 }
background={ backgroundColor }
>
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) }
</Select>
) }
{ item.value }
</GridItem>
</>
)) }
</Grid>
);
};
export default TxStateStorageItem;
import {
AccordionItem,
AccordionButton,
AccordionPanel,
AccordionIcon,
Text,
Box,
Tr,
Td,
Flex,
Stat,
StatArrow,
Portal,
Link,
Button,
} from '@chakra-ui/react';
import React, { useRef } from 'react';
import type { TTxStateItem } from 'data/txState';
import { nbsp } from 'lib/html-entities';
import AddressIcon from 'ui/shared/AddressIcon';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip';
import TxStateStorageItem from './TxStateStorageItem';
const TxStateTableItem = ({ txStateItem }: { txStateItem: TTxStateItem }) => {
const ref = useRef<HTMLTableDataCellElement>(null);
const hasStorageData = Boolean(txStateItem.storage?.length);
return (
<>
<AccordionItem as="tr" isDisabled={ !hasStorageData } fontWeight={ 500 } border={ 0 }>
{ ({ isExpanded }) => (
<>
<Td border={ 0 }>
<AccordionButton
_hover={{ background: 'unset' }}
padding="0"
>
<Button
variant="outline"
borderWidth="1px"
// button can't be inside button (AccordionButton)
as="div"
isActive={ isExpanded }
size="sm"
fontWeight={ 400 }
isDisabled={ !hasStorageData }
colorScheme="gray"
// AccordionButton has its own opacity rule when disabled
_disabled={{ opacity: 1 }}
>
{ txStateItem.storage?.length || '0' }
</Button>
<AccordionIcon color="blue.600" width="30px"/>
</AccordionButton>
</Td>
<Td border={ 0 }>
<Flex height="30px" alignItems="center">
<AddressIcon address={ txStateItem.address }/>
<AddressLinkWithTooltip address={ txStateItem.address } fontWeight="500" truncated withCopy={ false } ml={ 2 }/>
</Flex>
</Td>
<Td border={ 0 } lineHeight="30px"><Link>{ txStateItem.miner }</Link></Td>
<Td border={ 0 } isNumeric lineHeight="30px">
<Box>{ txStateItem.after.balance }</Box>
{ typeof txStateItem.after.nonce !== 'undefined' && (
<Box justifyContent="end" display="inline-flex">Nonce: <Text fontWeight={ 600 }>{ nbsp + txStateItem.after.nonce }</Text></Box>
) }
</Td>
<Td border={ 0 } isNumeric lineHeight="30px">{ txStateItem.before.balance }</Td>
<Td border={ 0 } isNumeric lineHeight="30px">
<Stat>
{ txStateItem.diff }
<StatArrow ml={ 2 } type={ Number(txStateItem.diff) > 0 ? 'increase' : 'decrease' }/>
</Stat>
</Td>
{ hasStorageData && (
<Portal containerRef={ ref }>
<AccordionPanel fontWeight={ 500 }>
{ txStateItem.storage?.map((storageItem, index) => <TxStateStorageItem key={ index } storageItem={ storageItem }/>) }
</AccordionPanel>
</Portal>
) }
</>
) }
</AccordionItem>
<Tr><Td colSpan={ 6 } ref={ ref } padding={ 0 }></Td></Tr>
</>
);
};
export default TxStateTableItem;
...@@ -12,9 +12,9 @@ import { useForm, Controller } from 'react-hook-form'; ...@@ -12,9 +12,9 @@ import { useForm, Controller } from 'react-hook-form';
import type { WatchlistErrors } from 'types/api/account'; import type { WatchlistErrors } from 'types/api/account';
import type { TWatchlistItem } from 'types/client/account'; import type { TWatchlistItem } from 'types/client/account';
import type { ErrorType } from 'lib/client/fetch';
import fetch from 'lib/client/fetch';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import { ADDRESS_REGEXP } from 'lib/validations/address'; import { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput'; import AddressInput from 'ui/shared/AddressInput';
import CheckboxInput from 'ui/shared/CheckboxInput'; import CheckboxInput from 'ui/shared/CheckboxInput';
...@@ -83,9 +83,10 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -83,9 +83,10 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
}); });
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const fetch = useFetch();
function updateWatchlist(formData: Inputs) { function updateWatchlist(formData: Inputs) {
const requestParams = { const body = {
name: formData?.tag, name: formData?.tag,
address_hash: formData?.address, address_hash: formData?.address,
notification_settings: formData.notification_settings, notification_settings: formData.notification_settings,
...@@ -95,11 +96,11 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -95,11 +96,11 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
}; };
if (data) { if (data) {
// edit address // edit address
return fetch<TWatchlistItem, WatchlistErrors>(`/api/account/watchlist/${ data.id }`, { method: 'PUT', body: JSON.stringify(requestParams) }); return fetch<TWatchlistItem, WatchlistErrors>(`/api/account/watchlist/${ data.id }`, { method: 'PUT', body });
} else { } else {
// add address // add address
return fetch<TWatchlistItem, WatchlistErrors>('/api/account/watchlist', { method: 'POST', body: JSON.stringify(requestParams) }); return fetch<TWatchlistItem, WatchlistErrors>('/api/account/watchlist', { method: 'POST', body });
} }
} }
......
...@@ -4,7 +4,7 @@ import React, { useCallback } from 'react'; ...@@ -4,7 +4,7 @@ import React, { useCallback } from 'react';
import type { TWatchlistItem, TWatchlist } from 'types/client/account'; import type { TWatchlistItem, TWatchlist } from 'types/client/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import DeleteModal from 'ui/shared/DeleteModal'; import DeleteModal from 'ui/shared/DeleteModal';
...@@ -17,10 +17,11 @@ type Props = { ...@@ -17,10 +17,11 @@ type Props = {
const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => { const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch();
const mutationFn = useCallback(() => { const mutationFn = useCallback(() => {
return fetch(`/api/account1/watchlist/${ data?.id }`, { method: 'DELETE' }); return fetch(`/api/account1/watchlist/${ data?.id }`, { method: 'DELETE' });
}, [ data ]); }, [ data?.id, fetch ]);
const onSuccess = useCallback(async() => { const onSuccess = useCallback(async() => {
queryClient.setQueryData([ 'watchlist' ], (prevData: TWatchlist | undefined) => { queryClient.setQueryData([ 'watchlist' ], (prevData: TWatchlist | undefined) => {
......
...@@ -9,7 +9,7 @@ import React, { useCallback, useState } from 'react'; ...@@ -9,7 +9,7 @@ import React, { useCallback, useState } from 'react';
import type { TWatchlistItem } from 'types/client/account'; import type { TWatchlistItem } from 'types/client/account';
import fetch from 'lib/client/fetch'; import useFetch from 'lib/hooks/useFetch';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
...@@ -34,6 +34,7 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -34,6 +34,7 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
}, [ item, onDeleteClick ]); }, [ item, onDeleteClick ]);
const toast = useToast(); const toast = useToast();
const fetch = useFetch();
const showToast = useCallback(() => { const showToast = useCallback(() => {
toast({ toast({
...@@ -49,9 +50,9 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -49,9 +50,9 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
const { mutate } = useMutation(() => { const { mutate } = useMutation(() => {
setSwitchDisabled(true); setSwitchDisabled(true);
const data = { ...item, notification_methods: { email: !notificationEnabled } }; const body = { ...item, notification_methods: { email: !notificationEnabled } };
setNotificationEnabled(prevState => !prevState); setNotificationEnabled(prevState => !prevState);
return fetch(`/api/account1/watchlist/${ item.id }`, { method: 'PUT', body: JSON.stringify(data) }); return fetch(`/api/account1/watchlist/${ item.id }`, { method: 'PUT', body });
}, { }, {
onError: () => { onError: () => {
showToast(); showToast();
......
...@@ -1380,10 +1380,10 @@ ...@@ -1380,10 +1380,10 @@
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.2.5.tgz#d908c57b35262b94db3e431e869b72ac3e1ad3e3" resolved "https://registry.yarnpkg.com/@next/env/-/env-12.2.5.tgz#d908c57b35262b94db3e431e869b72ac3e1ad3e3"
integrity sha512-vLPLV3cpPGjUPT3PjgRj7e3nio9t6USkuew3JE/jMeon/9Mvp1WyR18v3iwnCuX7eUAm1HmAbJHHLAbcu/EJcw== integrity sha512-vLPLV3cpPGjUPT3PjgRj7e3nio9t6USkuew3JE/jMeon/9Mvp1WyR18v3iwnCuX7eUAm1HmAbJHHLAbcu/EJcw==
"@next/eslint-plugin-next@12.1.6": "@next/eslint-plugin-next@12.3.0":
version "12.1.6" version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.1.6.tgz#dde3f98831f15923b25244588d924c716956292e" resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.3.0.tgz#302c1f03618d5001ce92ea6826c329268759128e"
integrity sha512-yNUtJ90NEiYFT6TJnNyofKMPYqirKDwpahcbxBgSIuABwYOdkGwzos1ZkYD51Qf0diYwpQZBeVqElTk7Q2WNqw== integrity sha512-jVdq1qYTNDjUtulnE8/hkPv0pHILV4jMg5La99iaY/FFm20WxVnsAZtbNnMvlPbf8dc010oO304SX9yXbg5PAw==
dependencies: dependencies:
glob "7.1.7" glob "7.1.7"
...@@ -2709,12 +2709,12 @@ escape-string-regexp@^4.0.0: ...@@ -2709,12 +2709,12 @@ escape-string-regexp@^4.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
eslint-config-next@12.1.6: eslint-config-next@^12.3.0:
version "12.1.6" version "12.3.0"
resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-12.1.6.tgz#55097028982dce49159d8753000be3916ac55254" resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-12.3.0.tgz#d887ab2d143fe1a2b308e9321e932a613e610800"
integrity sha512-qoiS3g/EPzfCTkGkaPBSX9W0NGE/B1wNO3oWrd76QszVGrdpLggNqcO8+LR6MB0CNqtp9Q8NoeVrxNVbzM9hqA== integrity sha512-guHSkNyKnTBB8HU35COgAMeMV0E026BiYRYvyEVVaTOeFcnU3i1EI8/Da0Rl7H3Sgua5FEvoA0vYd2s8kdIUXg==
dependencies: dependencies:
"@next/eslint-plugin-next" "12.1.6" "@next/eslint-plugin-next" "12.3.0"
"@rushstack/eslint-patch" "^1.1.3" "@rushstack/eslint-patch" "^1.1.3"
"@typescript-eslint/parser" "^5.21.0" "@typescript-eslint/parser" "^5.21.0"
eslint-import-resolver-node "^0.3.6" eslint-import-resolver-node "^0.3.6"
......
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