Commit dd5aa5bd authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into tom2drum/issue-1035

parents 16ba4c33 43637f9b
...@@ -22,7 +22,7 @@ jobs: ...@@ -22,7 +22,7 @@ jobs:
code_quality: code_quality:
name: Code quality name: Code quality
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ !contains(github.event.pull_request.labels.*.name, 'WIP') && !(github.event.action == 'unlabeled' && github.event.label.name != 'WIP') }} if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip checks') && !(github.event.action == 'unlabeled' && github.event.label.name != 'skip checks') }}
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v3
......
...@@ -4,7 +4,7 @@ on: ...@@ -4,7 +4,7 @@ on:
workflow_dispatch: workflow_dispatch:
push: push:
tags: tags:
- 'v[0-9]+.[0-9]+.[0-9]+-[a-z]+*' # e.g v1.2.3-alpha - 'v[0-9]+.[0-9]+.[0-9]+-[a-z]+*' # e.g v1.2.3-alpha.2
jobs: jobs:
checks: checks:
...@@ -24,9 +24,29 @@ jobs: ...@@ -24,9 +24,29 @@ jobs:
uses: "./.github/workflows/e2e-tests.yml" uses: "./.github/workflows/e2e-tests.yml"
secrets: inherit secrets: inherit
version:
name: Pre-release version info
runs-on: ubuntu-latest
outputs:
is_initial: ${{ steps.is_initial.outputs.result }}
steps:
- name: Determine if it is the initial version of the pre-release
id: is_initial
uses: actions/github-script@v6
env:
TAG: ${{ github.ref_name }}
with:
script: |
const tag = process.env.TAG;
const REGEXP = /^v[0-9]+.[0-9]+.[0-9]+-[a-z]+((\.|-)\d+)?$/i;
const match = tag.match(REGEXP);
return match && !match[1] ? 'true' : 'false';
label_issues: label_issues:
name: Add pre-release label to issues name: Add pre-release label to issues
uses: './.github/workflows/label-issues-in-release.yml' uses: './.github/workflows/label-issues-in-release.yml'
needs: [ version ]
if: ${{ needs.version.outputs.is_initial == 'true' }}
with: with:
tag: ${{ github.ref_name }} tag: ${{ github.ref_name }}
label_name: 'pre-release' label_name: 'pre-release'
......
...@@ -89,6 +89,10 @@ WORKDIR /app ...@@ -89,6 +89,10 @@ WORKDIR /app
RUN addgroup --system --gid 1001 nodejs RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs RUN adduser --system --uid 1001 nextjs
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
COPY --from=builder /app/next.config.js ./ COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json COPY --from=builder /app/package.json ./package.json
......
...@@ -29,6 +29,7 @@ See our [Contribution guide](./docs/CONTRIBUTING.md) for pull request protocol. ...@@ -29,6 +29,7 @@ See our [Contribution guide](./docs/CONTRIBUTING.md) for pull request protocol.
- [Contribution guide](./docs/CONTRIBUTING.md) - [Contribution guide](./docs/CONTRIBUTING.md)
- [Making a custom build](./docs/CUSTOM_BUILD.md) - [Making a custom build](./docs/CUSTOM_BUILD.md)
- [Frontend migration guide](https://docs.blockscout.com/for-developers/frontend-migration) - [Frontend migration guide](https://docs.blockscout.com/for-developers/frontend-migration)
- [Manual deployment guide with backend and microservices](https://docs.blockscout.com/for-developers/deployment/manual-deployment-guide)
## License ## License
......
...@@ -157,8 +157,8 @@ We have 3 pre-configured projects. You can run your test with the desired projec ...@@ -157,8 +157,8 @@ We have 3 pre-configured projects. You can run your test with the desired projec
### Opening PR and getting it accepted ### Opening PR and getting it accepted
1. Push your changes and create a Pull Request. If you are still working on the task, please use "Draft Pull Request" option, so we know that it is not ready yet. In addition, you can add label "WIP" to your PR, so all CI checks will not be triggered. 1. Push your changes and create a Pull Request. If you are still working on the task, please use "Draft Pull Request" option, so we know that it is not ready yet. In addition, you can add label "skip checks" to your PR, so all CI checks will not be triggered.
2. Once you finish your work, remove label "WIP" from PR, if it was added before, and publish PR if it was in the draft state 2. Once you finish your work, remove label "skip checks" from PR, if it was added before, and publish PR if it was in the draft state
3. Make sure that all code checks and tests are successfully passed 3. Make sure that all code checks and tests are successfully passed
4. Add description to your Pull Request and link an existing issue(s) that it is fixing 4. Add description to your Pull Request and link an existing issue(s) that it is fixing
5. Request review from one or all core team members: @tom2drum, @isstuev. Our core team is committed to reviewing patches in a timely manner. 5. Request review from one or all core team members: @tom2drum, @isstuev. Our core team is committed to reviewing patches in a timely manner.
......
## Description and Related Issue(s)
*[Provide a brief description of the changes or enhancements introduced by this pull request and explain motivation behind them. Cite any related issue(s) or bug(s) that it addresses using the [format](https://blog.github.com/2013-05-14-closing-issues-via-pull-requests/) `Fixes #123` or `Resolves #456`.]*
### Proposed Changes
*[Specify the changes or additions made in this pull request. Please mention if any changes were made to the ENV variables]*
### Breaking or Incompatible Changes
*[Describe any breaking or incompatible changes introduced by this pull request. Specify how users might need to modify their code or configurations to accommodate these changes.]*
### Additional Information
*[Include any additional information, context, or screenshots that may be helpful for reviewers.]*
## Checklist for PR author
- [ ] I have tested these changes locally.
- [ ] I added tests to cover any new functionality, following this [guide](./CONTRIBUTING.md#writing--running-tests)
- [ ] Whenever I fix a bug, I include a regression test to ensure that the bug does not reappear silently.
- [ ] If I have added, changed, renamed, or removed an environment variable, I have updated the list of environment variables in the [documentation](ENVS.md) and made the necessary changes to the validator script according to the [guide](./CONTRIBUTING.md#adding-new-env-variable)
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 1.593A8.407 8.407 0 0 0 3.332 15.12c1.465-2.162 3.89-3.556 6.668-3.556 2.784 0 5.2 1.461 6.66 3.567A8.407 8.407 0 0 0 10 1.593ZM0 10C0 4.477 4.477 0 10 0s10 4.477 10 10-4.477 10-10 10S0 15.523 0 10Zm4.476 6.287c1.443 1.295 3.412 2.039 5.524 2.039a8.529 8.529 0 0 0 5.51-2.045c-1.17-1.85-3.2-3.123-5.51-3.123-2.333 0-4.363 1.225-5.524 3.129Zm3.388-8.975a2.136 2.136 0 1 1 4.272 0 2.136 2.136 0 0 1-4.272 0ZM10 3.584a3.729 3.729 0 1 0 0 7.457 3.729 3.729 0 0 0 0-7.457Z" fill="currentColor"/>
</svg>
...@@ -58,9 +58,16 @@ import type { ...@@ -58,9 +58,16 @@ import type {
} from 'types/api/token'; } from 'types/api/token';
import type { TokensResponse, TokensFilters, TokensSorting, TokenInstanceTransferResponse, TokensBridgedFilters } from 'types/api/tokens'; import type { TokensResponse, TokensFilters, TokensSorting, TokenInstanceTransferResponse, TokensBridgedFilters } from 'types/api/tokens';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer'; import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction, TransactionsResponseWatchlist } from 'types/api/transaction'; import type {
TransactionsResponseValidated,
TransactionsResponsePending,
Transaction,
TransactionsResponseWatchlist,
TransactionsSorting,
} from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters'; import type { TTxsFilters } from 'types/api/txsFilters';
import type { TxStateChanges } from 'types/api/txStateChanges'; import type { TxStateChanges } from 'types/api/txStateChanges';
import type { VerifiedContractsSorting } from 'types/api/verifiedContracts';
import type { VisualizedContract } from 'types/api/visualization'; import type { VisualizedContract } from 'types/api/visualization';
import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals'; import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals';
import type { ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesItem, ZkEvmL2TxnBatchesResponse, ZkEvmL2TxnBatchTxs } from 'types/api/zkEvmL2TxnBatches'; import type { ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesItem, ZkEvmL2TxnBatchesResponse, ZkEvmL2TxnBatchTxs } from 'types/api/zkEvmL2TxnBatches';
...@@ -725,5 +732,7 @@ never; ...@@ -725,5 +732,7 @@ never;
export type PaginationSorting<Q extends PaginatedResources> = export type PaginationSorting<Q extends PaginatedResources> =
Q extends 'tokens' ? TokensSorting : Q extends 'tokens' ? TokensSorting :
Q extends 'tokens_bridged' ? TokensSorting : Q extends 'tokens_bridged' ? TokensSorting :
Q extends 'verified_contracts' ? VerifiedContractsSorting :
Q extends 'address_txs' ? TransactionsSorting :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
...@@ -61,6 +61,7 @@ Type extends EventTypes.VERIFY_TOKEN ? { ...@@ -61,6 +61,7 @@ Type extends EventTypes.VERIFY_TOKEN ? {
'Action': 'Form opened' | 'Submit'; 'Action': 'Form opened' | 'Submit';
} : } :
Type extends EventTypes.WALLET_CONNECT ? { Type extends EventTypes.WALLET_CONNECT ? {
'Source': 'Header' | 'Smart contracts';
'Status': 'Started' | 'Connected'; 'Status': 'Started' | 'Connected';
} : } :
Type extends EventTypes.CONTRACT_INTERACTION ? { Type extends EventTypes.CONTRACT_INTERACTION ? {
......
import type * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing'; import { BrowserTracing } from '@sentry/tracing';
import appConfig from 'configs/app'; import appConfig from 'configs/app';
...@@ -90,3 +90,12 @@ export function configureScope(scope: Sentry.Scope) { ...@@ -90,3 +90,12 @@ export function configureScope(scope: Sentry.Scope) {
} }
scope.setTag('app_instance', feature.instance); scope.setTag('app_instance', feature.instance);
} }
export function init() {
if (!config) {
return;
}
Sentry.init(config);
Sentry.configureScope(configureScope);
}
import * as Sentry from '@sentry/react';
import React from 'react';
import { config, configureScope } from './config';
export default function useConfigSentry() {
React.useEffect(() => {
if (!config) {
return;
}
// gotta init sentry in browser
Sentry.init(config);
Sentry.configureScope(configureScope);
}, []);
}
import type { Transaction } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort';
import compareBns from 'lib/bigint/compareBns';
const sortTxs = (sorting?: Sort) => (tx1: Transaction, tx2: Transaction) => {
switch (sorting) {
case 'val-desc':
return compareBns(tx1.value, tx2.value);
case 'val-asc':
return compareBns(tx2.value, tx1.value);
case 'fee-desc':
return compareBns(tx1.fee.value || 0, tx2.fee.value || 0);
case 'fee-asc':
return compareBns(tx2.fee.value || 0, tx1.fee.value || 0);
default:
return 0;
}
};
export default sortTxs;
...@@ -7,18 +7,19 @@ import useAdblockDetect from 'lib/hooks/useAdblockDetect'; ...@@ -7,18 +7,19 @@ import useAdblockDetect from 'lib/hooks/useAdblockDetect';
import useGetCsrfToken from 'lib/hooks/useGetCsrfToken'; import useGetCsrfToken from 'lib/hooks/useGetCsrfToken';
import * as metadata from 'lib/metadata'; import * as metadata from 'lib/metadata';
import * as mixpanel from 'lib/mixpanel'; import * as mixpanel from 'lib/mixpanel';
import useConfigSentry from 'lib/sentry/useConfigSentry'; import { init as initSentry } from 'lib/sentry/config';
type Props = Route & { type Props = Route & {
children: React.ReactNode; children: React.ReactNode;
} }
initSentry();
const PageNextJs = (props: Props) => { const PageNextJs = (props: Props) => {
const { title, description, opengraph } = metadata.generate(props); const { title, description, opengraph } = metadata.generate(props);
useGetCsrfToken(); useGetCsrfToken();
useAdblockDetect(); useAdblockDetect();
useConfigSentry();
const isMixpanelInited = mixpanel.useInit(); const isMixpanelInited = mixpanel.useInit();
mixpanel.useLogPageView(isMixpanelInited); mixpanel.useLogPageView(isMixpanelInited);
......
...@@ -17,6 +17,7 @@ import theme from 'theme'; ...@@ -17,6 +17,7 @@ import theme from 'theme';
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary'; import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
import GoogleAnalytics from 'ui/shared/GoogleAnalytics'; import GoogleAnalytics from 'ui/shared/GoogleAnalytics';
import Layout from 'ui/shared/layout/Layout'; import Layout from 'ui/shared/layout/Layout';
import Web3ModalProvider from 'ui/shared/Web3ModalProvider';
import 'lib/setLocale'; import 'lib/setLocale';
...@@ -52,17 +53,19 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { ...@@ -52,17 +53,19 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
{ ...ERROR_SCREEN_STYLES } { ...ERROR_SCREEN_STYLES }
onError={ handleError } onError={ handleError }
> >
<AppContextProvider pageProps={ pageProps }> <Web3ModalProvider>
<QueryClientProvider client={ queryClient }> <AppContextProvider pageProps={ pageProps }>
<ScrollDirectionProvider> <QueryClientProvider client={ queryClient }>
<SocketProvider url={ `${ config.api.socket }${ config.api.basePath }/socket/v2` }> <ScrollDirectionProvider>
{ getLayout(<Component { ...pageProps }/>) } <SocketProvider url={ `${ config.api.socket }${ config.api.basePath }/socket/v2` }>
</SocketProvider> { getLayout(<Component { ...pageProps }/>) }
</ScrollDirectionProvider> </SocketProvider>
<ReactQueryDevtools buttonPosition="bottom-left" position="left"/> </ScrollDirectionProvider>
<GoogleAnalytics/> <ReactQueryDevtools buttonPosition="bottom-left" position="left"/>
</QueryClientProvider> <GoogleAnalytics/>
</AppContextProvider> </QueryClientProvider>
</AppContextProvider>
</Web3ModalProvider>
</AppErrorBoundary> </AppErrorBoundary>
</ChakraProvider> </ChakraProvider>
); );
......
...@@ -6,6 +6,8 @@ import type { NextPageWithLayout } from 'nextjs/types'; ...@@ -6,6 +6,8 @@ import type { NextPageWithLayout } from 'nextjs/types';
import type { Props } from 'nextjs/getServerSideProps'; import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs'; import PageNextJs from 'nextjs/PageNextJs';
import LayoutApp from 'ui/shared/layout/LayoutApp';
const MarketplaceApp = dynamic(() => import('ui/pages/MarketplaceApp'), { ssr: false }); const MarketplaceApp = dynamic(() => import('ui/pages/MarketplaceApp'), { ssr: false });
const Page: NextPageWithLayout<Props> = (props: Props) => { const Page: NextPageWithLayout<Props> = (props: Props) => {
...@@ -16,6 +18,14 @@ const Page: NextPageWithLayout<Props> = (props: Props) => { ...@@ -16,6 +18,14 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
); );
}; };
Page.getLayout = function getLayout(page: React.ReactElement) {
return (
<LayoutApp>
{ page }
</LayoutApp>
);
};
export default Page; export default Page;
export { marketplace as getServerSideProps } from 'nextjs/getServerSideProps'; export { marketplace as getServerSideProps } from 'nextjs/getServerSideProps';
...@@ -12,7 +12,7 @@ const Page: NextPage = () => { ...@@ -12,7 +12,7 @@ const Page: NextPage = () => {
return ( return (
<PageNextJs pathname="/apps"> <PageNextJs pathname="/apps">
<> <>
<PageTitle title="Marketplace"/> <PageTitle title="DAppscout"/>
<Marketplace/> <Marketplace/>
</> </>
</PageNextJs> </PageNextJs>
......
...@@ -117,3 +117,12 @@ export type TransactionType = 'rootstock_remasc' | ...@@ -117,3 +117,12 @@ export type TransactionType = 'rootstock_remasc' |
'coin_transfer' 'coin_transfer'
export type TxsResponse = TransactionsResponseValidated | TransactionsResponsePending | BlockTransactionsResponse; export type TxsResponse = TransactionsResponseValidated | TransactionsResponsePending | BlockTransactionsResponse;
export interface TransactionsSorting {
sort: 'value' | 'fee';
order: 'asc' | 'desc';
}
export type TransactionsSortingField = TransactionsSorting['sort'];
export type TransactionsSortingValue = `${ TransactionsSortingField }-${ TransactionsSorting['order'] }`;
export interface VerifiedContractsSorting {
sort: 'balance' | 'txs_count';
order: 'asc' | 'desc';
}
export type VerifiedContractsSortingField = VerifiedContractsSorting['sort'];
export type VerifiedContractsSortingValue = `${ VerifiedContractsSortingField }-${ VerifiedContractsSorting['order'] }`;
export type Sort = 'val-desc' | 'val-asc' | 'fee-desc' | 'fee-asc' | '';
...@@ -5,7 +5,7 @@ import React from 'react'; ...@@ -5,7 +5,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { AddressFromToFilter, AddressTransactionsResponse } from 'types/api/address'; import type { AddressFromToFilter, AddressTransactionsResponse } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address'; import { AddressFromToFilterValues } from 'types/api/address';
import type { Transaction } from 'types/api/transaction'; import type { Transaction, TransactionsSortingField, TransactionsSortingValue, TransactionsSorting } from 'types/api/transaction';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
...@@ -18,7 +18,10 @@ import { generateListStub } from 'stubs/utils'; ...@@ -18,7 +18,10 @@ import { generateListStub } from 'stubs/utils';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import Pagination from 'ui/shared/pagination/Pagination'; import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import TxsContent from 'ui/txs/TxsContent'; import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue';
import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery';
import TxsWithAPISorting from 'ui/txs/TxsWithAPISorting';
import { SORT_OPTIONS } from 'ui/txs/useTxsSort';
import AddressCsvExportLink from './AddressCsvExportLink'; import AddressCsvExportLink from './AddressCsvExportLink';
import AddressTxsFilter from './AddressTxsFilter'; import AddressTxsFilter from './AddressTxsFilter';
...@@ -53,6 +56,7 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => { ...@@ -53,6 +56,7 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => {
const [ socketAlert, setSocketAlert ] = React.useState(''); const [ socketAlert, setSocketAlert ] = React.useState('');
const [ newItemsCount, setNewItemsCount ] = React.useState(0); const [ newItemsCount, setNewItemsCount ] = React.useState(0);
const [ sort, setSort ] = React.useState<TransactionsSortingValue | undefined>(getSortValueFromQuery<TransactionsSortingValue>(router.query, SORT_OPTIONS));
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const currentAddress = getQueryParamString(router.query.hash); const currentAddress = getQueryParamString(router.query.hash);
...@@ -63,6 +67,7 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => { ...@@ -63,6 +67,7 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => {
resourceName: 'address_txs', resourceName: 'address_txs',
pathParams: { hash: currentAddress }, pathParams: { hash: currentAddress },
filters: { filter: filterValue }, filters: { filter: filterValue },
sorting: getSortParamsFromValue<TransactionsSortingValue, TransactionsSortingField, TransactionsSorting['order']>(sort),
scrollRef, scrollRef,
options: { options: {
placeholderData: generateListStub<'address_txs'>(TX, 50, { next_page_params: { placeholderData: generateListStub<'address_txs'>(TX, 50, { next_page_params: {
...@@ -177,7 +182,7 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => { ...@@ -177,7 +182,7 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => {
<Pagination { ...addressTxsQuery.pagination } ml={ 8 }/> <Pagination { ...addressTxsQuery.pagination } ml={ 8 }/>
</ActionBar> </ActionBar>
) } ) }
<TxsContent <TxsWithAPISorting
filter={ filter } filter={ filter }
filterValue={ filterValue } filterValue={ filterValue }
query={ addressTxsQuery } query={ addressTxsQuery }
...@@ -187,6 +192,8 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => { ...@@ -187,6 +192,8 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => {
socketInfoAlert={ socketAlert } socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount } socketInfoNum={ newItemsCount }
top={ 80 } top={ 80 }
sorting={ sort }
setSort={ setSort }
/> />
</> </>
); );
......
...@@ -45,6 +45,30 @@ interface Props { ...@@ -45,6 +45,30 @@ interface Props {
hash: string; hash: string;
} }
type ItemProps = {
item: DistributionItem;
vulnerabilities: SolidityscanReport['scan_report']['scan_summary']['issue_severity_distribution'];
vulnerabilitiesCount: number;
}
const SolidityScanReportItem = ({ item, vulnerabilities, vulnerabilitiesCount }: ItemProps) => {
const bgBar = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const yetAnotherGrayColor = useColorModeValue('gray.400', 'gray.500');
return (
<>
<Box w={ 3 } h={ 3 } bg={ item.color } borderRadius="6px" mr={ 2 }></Box>
<Flex justifyContent="space-between" mr={ 3 }>
<Text>{ item.name }</Text>
<Text color={ vulnerabilities[item.id] > 0 ? 'text' : yetAnotherGrayColor }>{ vulnerabilities[item.id] }</Text>
</Flex>
<Box bg={ bgBar } h="10px" borderRadius="8px">
<Box bg={ item.color } w={ vulnerabilities[item.id] / vulnerabilitiesCount } h="10px" borderRadius="8px"/>
</Box>
</>
);
};
const SolidityscanReport = ({ className, hash }: Props) => { const SolidityscanReport = ({ className, hash }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure(); const { isOpen, onToggle, onClose } = useDisclosure();
...@@ -58,7 +82,6 @@ const SolidityscanReport = ({ className, hash }: Props) => { ...@@ -58,7 +82,6 @@ const SolidityscanReport = ({ className, hash }: Props) => {
const score = Number(data?.scan_report.scan_summary.score_v2); const score = Number(data?.scan_report.scan_summary.score_v2);
const bgBar = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const chartGrayColor = useColorModeValue('gray.100', 'gray.700'); const chartGrayColor = useColorModeValue('gray.100', 'gray.700');
const yetAnotherGrayColor = useColorModeValue('gray.400', 'gray.500'); const yetAnotherGrayColor = useColorModeValue('gray.400', 'gray.500');
const popoverBgColor = useColorModeValue('white', 'gray.900'); const popoverBgColor = useColorModeValue('white', 'gray.900');
...@@ -136,16 +159,7 @@ const SolidityscanReport = ({ className, hash }: Props) => { ...@@ -136,16 +159,7 @@ const SolidityscanReport = ({ className, hash }: Props) => {
<Text py="7px" variant="secondary" fontSize="xs" fontWeight={ 500 }>Vulnerabilities distribution</Text> <Text py="7px" variant="secondary" fontSize="xs" fontWeight={ 500 }>Vulnerabilities distribution</Text>
<Grid templateColumns="20px 1fr 100px" alignItems="center" rowGap={ 2 }> <Grid templateColumns="20px 1fr 100px" alignItems="center" rowGap={ 2 }>
{ DISTRIBUTION_ITEMS.map(item => ( { DISTRIBUTION_ITEMS.map(item => (
<> <SolidityScanReportItem item={ item } key={ item.id } vulnerabilities={ vulnerabilities } vulnerabilitiesCount={ vulnerabilitiesCount }/>
<Box w={ 3 } h={ 3 } bg={ item.color } borderRadius="6px" mr={ 2 }></Box>
<Flex justifyContent="space-between" mr={ 3 }>
<Text>{ item.name }</Text>
<Text color={ vulnerabilities[item.id] > 0 ? 'text' : yetAnotherGrayColor }>{ vulnerabilities[item.id] }</Text>
</Flex>
<Box bg={ bgBar } h="10px" borderRadius="8px">
<Box bg={ item.color } w={ vulnerabilities[item.id] / vulnerabilitiesCount } h="10px" borderRadius="8px"/>
</Box>
</>
)) } )) }
</Grid> </Grid>
</Box> </Box>
......
...@@ -17,11 +17,11 @@ const ContractConnectWallet = () => { ...@@ -17,11 +17,11 @@ const ContractConnectWallet = () => {
setIsModalOpening(true); setIsModalOpening(true);
await open(); await open();
setIsModalOpening(false); setIsModalOpening(false);
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Status: 'Started' }); mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: 'Smart contracts', Status: 'Started' });
}, [ open ]); }, [ open ]);
const handleAccountConnected = React.useCallback(({ isReconnected }: { isReconnected: boolean }) => { const handleAccountConnected = React.useCallback(({ isReconnected }: { isReconnected: boolean }) => {
!isReconnected && mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Status: 'Connected' }); !isReconnected && mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: 'Smart contracts', Status: 'Connected' });
}, []); }, []);
const handleDisconnect = React.useCallback(() => { const handleDisconnect = React.useCallback(() => {
......
...@@ -15,6 +15,7 @@ interface Props extends MarketplaceAppPreview { ...@@ -15,6 +15,7 @@ interface Props extends MarketplaceAppPreview {
isFavorite: boolean; isFavorite: boolean;
onFavoriteClick: (id: string, isFavorite: boolean) => void; onFavoriteClick: (id: string, isFavorite: boolean) => void;
isLoading: boolean; isLoading: boolean;
showDisclaimer: (id: string) => void;
} }
const MarketplaceAppCard = ({ const MarketplaceAppCard = ({
...@@ -30,9 +31,18 @@ const MarketplaceAppCard = ({ ...@@ -30,9 +31,18 @@ const MarketplaceAppCard = ({
isFavorite, isFavorite,
onFavoriteClick, onFavoriteClick,
isLoading, isLoading,
showDisclaimer,
}: Props) => { }: Props) => {
const categoriesLabel = categories.join(', '); const categoriesLabel = categories.join(', ');
const handleClick = useCallback((event: MouseEvent) => {
const isShown = window.localStorage.getItem('marketplace-disclaimer-shown');
if (!isShown) {
event.preventDefault();
showDisclaimer(id);
}
}, [ showDisclaimer, id ]);
const handleInfoClick = useCallback((event: MouseEvent) => { const handleInfoClick = useCallback((event: MouseEvent) => {
event.preventDefault(); event.preventDefault();
onInfoClick(id); onInfoClick(id);
...@@ -100,6 +110,7 @@ const MarketplaceAppCard = ({ ...@@ -100,6 +110,7 @@ const MarketplaceAppCard = ({
url={ url } url={ url }
external={ external } external={ external }
title={ title } title={ title }
onClick={ handleClick }
/> />
</Skeleton> </Skeleton>
......
import { LinkOverlay } from '@chakra-ui/react'; import { LinkOverlay } from '@chakra-ui/react';
import NextLink from 'next/link'; import NextLink from 'next/link';
import React from 'react'; import React from 'react';
import type { MouseEvent } from 'react';
type Props = { type Props = {
id: string; id: string;
url: string; url: string;
external?: boolean; external?: boolean;
title: string; title: string;
onClick?: (event: MouseEvent) => void;
} }
const MarketplaceAppCardLink = ({ url, external, id, title }: Props) => { const MarketplaceAppCardLink = ({ url, external, id, title, onClick }: Props) => {
return external ? ( return external ? (
<LinkOverlay href={ url } isExternal={ true }> <LinkOverlay href={ url } isExternal={ true }>
{ title } { title }
</LinkOverlay> </LinkOverlay>
) : ( ) : (
<NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref legacyBehavior> <NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref legacyBehavior>
<LinkOverlay> <LinkOverlay onClick={ onClick }>
{ title } { title }
</LinkOverlay> </LinkOverlay>
</NextLink> </NextLink>
......
import { Heading, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Text, Button, useColorModeValue } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
type Props = {
isOpen: boolean;
onClose: () => void;
appId: string;
}
const MarketplaceDisclaimerModal = ({ isOpen, onClose, appId }: Props) => {
const isMobile = useIsMobile();
const handleContinueClick = React.useCallback(() => {
window.localStorage.setItem('marketplace-disclaimer-shown', 'true');
}, [ ]);
return (
<Modal
isOpen={ isOpen }
onClose={ onClose }
size={ isMobile ? 'full' : 'md' }
isCentered
>
<ModalOverlay/>
<ModalContent>
<ModalHeader>
<Heading
as="h2"
fontSize="2xl"
fontWeight="medium"
lineHeight={ 1 }
color={ useColorModeValue('blackAlpha.800', 'whiteAlpha.800') }
>
Disclaimer
</Heading>
</ModalHeader>
<ModalBody>
<Text color={ useColorModeValue('gray.800', 'whiteAlpha.800') }>
You are now accessing a third-party app. Blockscout does not own, control, maintain, or audit 3rd party apps,{ ' ' }
and is not liable for any losses associated with these interactions. Please do so at your own risk.
<br/><br/>
By clicking continue, you agree that you understand the risks and have read the Disclaimer.
</Text>
</ModalBody>
<ModalFooter
display="flex"
flexDirection="row"
alignItems="center"
>
<NextLink href={{ pathname: '/apps/[id]', query: { id: appId } }} passHref legacyBehavior>
<Button
variant="solid"
colorScheme="blue"
mr={ 6 }
py="10px"
onClick={ handleContinueClick }
>
Continue to app
</Button>
</NextLink>
<Button
variant="outline"
colorScheme="blue"
onClick={ onClose }
>
Cancel
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
};
export default MarketplaceDisclaimerModal;
...@@ -14,9 +14,10 @@ type Props = { ...@@ -14,9 +14,10 @@ type Props = {
favoriteApps: Array<string>; favoriteApps: Array<string>;
onFavoriteClick: (id: string, isFavorite: boolean) => void; onFavoriteClick: (id: string, isFavorite: boolean) => void;
isLoading: boolean; isLoading: boolean;
showDisclaimer: (id: string) => void;
} }
const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick, isLoading }: Props) => { const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick, isLoading, showDisclaimer }: Props) => {
return apps.length > 0 ? ( return apps.length > 0 ? (
<Grid <Grid
templateColumns={{ templateColumns={{
...@@ -41,6 +42,7 @@ const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick, isLo ...@@ -41,6 +42,7 @@ const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick, isLo
isFavorite={ favoriteApps.includes(app.id) } isFavorite={ favoriteApps.includes(app.id) }
onFavoriteClick={ onFavoriteClick } onFavoriteClick={ onFavoriteClick }
isLoading={ isLoading } isLoading={ isLoading }
showDisclaimer={ showDisclaimer }
/> />
)) } )) }
</Grid> </Grid>
......
...@@ -29,6 +29,8 @@ export default function useMarketplace() { ...@@ -29,6 +29,8 @@ export default function useMarketplace() {
const [ selectedCategoryId, setSelectedCategoryId ] = React.useState<string>(MarketplaceCategory.ALL); const [ selectedCategoryId, setSelectedCategoryId ] = React.useState<string>(MarketplaceCategory.ALL);
const [ filterQuery, setFilterQuery ] = React.useState(defaultFilterQuery); const [ filterQuery, setFilterQuery ] = React.useState(defaultFilterQuery);
const [ favoriteApps, setFavoriteApps ] = React.useState<Array<string>>([]); const [ favoriteApps, setFavoriteApps ] = React.useState<Array<string>>([]);
const [ isAppInfoModalOpen, setIsAppInfoModalOpen ] = React.useState<boolean>(false);
const [ isDisclaimerModalOpen, setIsDisclaimerModalOpen ] = React.useState<boolean>(false);
const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean) => { const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean) => {
const favoriteApps = getFavoriteApps(); const favoriteApps = getFavoriteApps();
...@@ -46,10 +48,20 @@ export default function useMarketplace() { ...@@ -46,10 +48,20 @@ export default function useMarketplace() {
const showAppInfo = React.useCallback((id: string) => { const showAppInfo = React.useCallback((id: string) => {
setSelectedAppId(id); setSelectedAppId(id);
setIsAppInfoModalOpen(true);
}, []);
const showDisclaimer = React.useCallback((id: string) => {
setSelectedAppId(id);
setIsDisclaimerModalOpen(true);
}, []); }, []);
const debouncedFilterQuery = useDebounce(filterQuery, 500); const debouncedFilterQuery = useDebounce(filterQuery, 500);
const clearSelectedAppId = React.useCallback(() => setSelectedAppId(null), []); const clearSelectedAppId = React.useCallback(() => {
setSelectedAppId(null);
setIsAppInfoModalOpen(false);
setIsDisclaimerModalOpen(false);
}, []);
const handleCategoryChange = React.useCallback((newCategory: string) => { const handleCategoryChange = React.useCallback((newCategory: string) => {
setSelectedCategoryId(newCategory); setSelectedCategoryId(newCategory);
...@@ -104,6 +116,9 @@ export default function useMarketplace() { ...@@ -104,6 +116,9 @@ export default function useMarketplace() {
clearSelectedAppId, clearSelectedAppId,
favoriteApps, favoriteApps,
onFavoriteClick: handleFavoriteClick, onFavoriteClick: handleFavoriteClick,
isAppInfoModalOpen,
isDisclaimerModalOpen,
showDisclaimer,
}), [ }), [
selectedCategoryId, selectedCategoryId,
categories, categories,
...@@ -118,5 +133,8 @@ export default function useMarketplace() { ...@@ -118,5 +133,8 @@ export default function useMarketplace() {
isPlaceholderData, isPlaceholderData,
showAppInfo, showAppInfo,
debouncedFilterQuery, debouncedFilterQuery,
isAppInfoModalOpen,
isDisclaimerModalOpen,
showDisclaimer,
]); ]);
} }
import type { TypedData } from 'abitype';
import { useCallback } from 'react';
import type { Account, SignTypedDataParameters } from 'viem';
import { useAccount, useSendTransaction, useSwitchNetwork, useNetwork, useSignMessage, useSignTypedData } from 'wagmi';
import config from 'configs/app';
type SendTransactionArgs = {
chainId?: number;
mode?: 'prepared';
to: string;
};
export type SignTypedDataArgs<
TTypedData extends
| TypedData
| {
[key: string]: unknown;
} = TypedData,
TPrimaryType extends string = string,
> = SignTypedDataParameters<TTypedData, TPrimaryType, Account>;
export default function useMarketplaceWallet() {
const { address } = useAccount();
const { chain } = useNetwork();
const { sendTransactionAsync } = useSendTransaction();
const { signMessageAsync } = useSignMessage();
const { signTypedDataAsync } = useSignTypedData();
const { switchNetworkAsync } = useSwitchNetwork({ chainId: Number(config.chain.id) });
const switchNetwork = useCallback(async() => {
if (Number(config.chain.id) !== chain?.id) {
await switchNetworkAsync?.();
}
}, [ chain, switchNetworkAsync ]);
const sendTransaction = useCallback(async(transaction: SendTransactionArgs) => {
await switchNetwork();
const tx = await sendTransactionAsync(transaction);
return tx.hash;
}, [ sendTransactionAsync, switchNetwork ]);
const signMessage = useCallback(async(message: string) => {
await switchNetwork();
const signature = await signMessageAsync({ message });
return signature;
}, [ signMessageAsync, switchNetwork ]);
const signTypedData = useCallback(async(typedData: SignTypedDataArgs) => {
await switchNetwork();
if (typedData.domain) {
typedData.domain.chainId = Number(typedData.domain.chainId);
}
const signature = await signTypedDataAsync(typedData);
return signature;
}, [ signTypedDataAsync, switchNetwork ]);
return {
address,
sendTransaction,
signMessage,
signTypedData,
};
}
...@@ -189,7 +189,7 @@ const AddressPageContent = () => { ...@@ -189,7 +189,7 @@ const AddressPageContent = () => {
<AddressQrCode address={{ hash }} isLoading={ isLoading }/> <AddressQrCode address={{ hash }} isLoading={ isLoading }/>
<AccountActionsMenu isLoading={ isLoading }/> <AccountActionsMenu isLoading={ isLoading }/>
<HStack ml="auto" gap={ 2 }/> <HStack ml="auto" gap={ 2 }/>
{ addressQuery.data?.is_contract && config.UI.views.address.solidityscanEnabled && <SolidityscanReport hash={ hash }/> } { addressQuery.data?.is_contract && addressQuery.data?.is_verified && config.UI.views.address.solidityscanEnabled && <SolidityscanReport hash={ hash }/> }
<NetworkExplorers type="address" pathParam={ hash }/> <NetworkExplorers type="address" pathParam={ hash }/>
</Flex> </Flex>
); );
......
...@@ -24,7 +24,7 @@ import Pagination from 'ui/shared/pagination/Pagination'; ...@@ -24,7 +24,7 @@ import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import TxsContent from 'ui/txs/TxsContent'; import TxsWithFrontendSorting from 'ui/txs/TxsWithFrontendSorting';
const TAB_LIST_PROPS = { const TAB_LIST_PROPS = {
marginBottom: 0, marginBottom: 0,
...@@ -82,7 +82,7 @@ const BlockPageContent = () => { ...@@ -82,7 +82,7 @@ const BlockPageContent = () => {
const tabs: Array<RoutedTab> = React.useMemo(() => ([ const tabs: Array<RoutedTab> = React.useMemo(() => ([
{ id: 'index', title: 'Details', component: <BlockDetails query={ blockQuery }/> }, { id: 'index', title: 'Details', component: <BlockDetails query={ blockQuery }/> },
{ id: 'txs', title: 'Transactions', component: <TxsContent query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/> }, { id: 'txs', title: 'Transactions', component: <TxsWithFrontendSorting query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/> },
config.features.beaconChain.isEnabled && Boolean(blockQuery.data?.withdrawals_count) ? config.features.beaconChain.isEnabled && Boolean(blockQuery.data?.withdrawals_count) ?
{ id: 'withdrawals', title: 'Withdrawals', component: <BlockWithdrawals blockWithdrawalsQuery={ blockWithdrawalsQuery }/> } : { id: 'withdrawals', title: 'Withdrawals', component: <BlockWithdrawals blockWithdrawalsQuery={ blockWithdrawalsQuery }/> } :
null, null,
......
...@@ -10,6 +10,7 @@ import Transactions from 'ui/home/Transactions'; ...@@ -10,6 +10,7 @@ import Transactions from 'ui/home/Transactions';
import AdBanner from 'ui/shared/ad/AdBanner'; import AdBanner from 'ui/shared/ad/AdBanner';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop'; import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import SearchBar from 'ui/snippets/searchBar/SearchBar'; import SearchBar from 'ui/snippets/searchBar/SearchBar';
import WalletMenuDesktop from 'ui/snippets/walletMenu/WalletMenuDesktop';
const Home = () => { const Home = () => {
return ( return (
...@@ -22,7 +23,7 @@ const Home = () => { ...@@ -22,7 +23,7 @@ const Home = () => {
minW={{ base: 'unset', lg: '900px' }} minW={{ base: 'unset', lg: '900px' }}
data-label="hero plate" data-label="hero plate"
> >
<Flex mb={{ base: 6, lg: 8 }} justifyContent="space-between"> <Flex mb={{ base: 6, lg: 8 }} justifyContent="space-between" alignItems="center">
<Heading <Heading
as="h1" as="h1"
size={{ base: 'md', lg: 'xl' }} size={{ base: 'md', lg: 'xl' }}
...@@ -32,8 +33,9 @@ const Home = () => { ...@@ -32,8 +33,9 @@ const Home = () => {
> >
{ config.chain.name } explorer { config.chain.name } explorer
</Heading> </Heading>
<Box display={{ base: 'none', lg: 'block' }}> <Box display={{ base: 'none', lg: 'flex' }}>
{ config.features.account.isEnabled && <ProfileMenuDesktop/> } { config.features.account.isEnabled && <ProfileMenuDesktop isHomePage/> }
{ config.features.blockchainInteraction.isEnabled && <WalletMenuDesktop isHomePage/> }
</Box> </Box>
</Flex> </Flex>
<SearchBar isHomepage/> <SearchBar isHomepage/>
......
...@@ -7,7 +7,7 @@ import { generateListStub } from 'stubs/utils'; ...@@ -7,7 +7,7 @@ import { generateListStub } from 'stubs/utils';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import TxsContent from 'ui/txs/TxsContent'; import TxsWithFrontendSorting from 'ui/txs/TxsWithFrontendSorting';
const KettleTxs = () => { const KettleTxs = () => {
const router = useRouter(); const router = useRouter();
...@@ -31,7 +31,7 @@ const KettleTxs = () => { ...@@ -31,7 +31,7 @@ const KettleTxs = () => {
<> <>
<PageTitle title="Computor transactions" withTextAd/> <PageTitle title="Computor transactions" withTextAd/>
<AddressEntity address={{ hash }} mb={ 6 }/> <AddressEntity address={{ hash }} mb={ 6 }/>
<TxsContent <TxsWithFrontendSorting
query={ query } query={ query }
showSocketInfo={ false } showSocketInfo={ false }
/> />
......
...@@ -5,6 +5,7 @@ import config from 'configs/app'; ...@@ -5,6 +5,7 @@ import config from 'configs/app';
import PlusIcon from 'icons/plus.svg'; import PlusIcon from 'icons/plus.svg';
import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal'; import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal';
import MarketplaceCategoriesMenu from 'ui/marketplace/MarketplaceCategoriesMenu'; import MarketplaceCategoriesMenu from 'ui/marketplace/MarketplaceCategoriesMenu';
import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal';
import MarketplaceList from 'ui/marketplace/MarketplaceList'; import MarketplaceList from 'ui/marketplace/MarketplaceList';
import FilterInput from 'ui/shared/filters/FilterInput'; import FilterInput from 'ui/shared/filters/FilterInput';
...@@ -27,6 +28,9 @@ const Marketplace = () => { ...@@ -27,6 +28,9 @@ const Marketplace = () => {
clearSelectedAppId, clearSelectedAppId,
favoriteApps, favoriteApps,
onFavoriteClick, onFavoriteClick,
isAppInfoModalOpen,
isDisclaimerModalOpen,
showDisclaimer,
} = useMarketplace(); } = useMarketplace();
if (isError) { if (isError) {
...@@ -68,9 +72,10 @@ const Marketplace = () => { ...@@ -68,9 +72,10 @@ const Marketplace = () => {
favoriteApps={ favoriteApps } favoriteApps={ favoriteApps }
onFavoriteClick={ onFavoriteClick } onFavoriteClick={ onFavoriteClick }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
showDisclaimer={ showDisclaimer }
/> />
{ selectedApp && ( { (selectedApp && isAppInfoModalOpen) && (
<MarketplaceAppModal <MarketplaceAppModal
onClose={ clearSelectedAppId } onClose={ clearSelectedAppId }
isFavorite={ favoriteApps.includes(selectedApp.id) } isFavorite={ favoriteApps.includes(selectedApp.id) }
...@@ -79,6 +84,14 @@ const Marketplace = () => { ...@@ -79,6 +84,14 @@ const Marketplace = () => {
/> />
) } ) }
{ (selectedApp && isDisclaimerModalOpen) && (
<MarketplaceDisclaimerModal
isOpen={ isDisclaimerModalOpen }
onClose={ clearSelectedAppId }
appId={ selectedApp.id }
/>
) }
<Skeleton <Skeleton
isLoaded={ !isPlaceholderData } isLoaded={ !isPlaceholderData }
marginTop={{ base: 8, sm: 16 }} marginTop={{ base: 8, sm: 16 }}
......
import { Box, Center, useColorMode } from '@chakra-ui/react'; import { Box, Center, useColorMode } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { DappscoutIframeProvider, useDappscoutIframe } from 'dappscout-iframe';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import type { MarketplaceAppOverview } from 'types/client/marketplace'; import type { MarketplaceAppOverview } from 'types/client/marketplace';
...@@ -9,12 +10,12 @@ import { route } from 'nextjs-routes'; ...@@ -9,12 +10,12 @@ import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import { useAppContext } from 'lib/contexts/app';
import useApiFetch from 'lib/hooks/useFetch'; import useApiFetch from 'lib/hooks/useFetch';
import * as metadata from 'lib/metadata'; import * as metadata from 'lib/metadata';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import PageTitle from 'ui/shared/Page/PageTitle';
import useMarketplaceWallet from '../marketplace/useMarketplaceWallet';
const feature = config.features.marketplace; const feature = config.features.marketplace;
const configUrl = feature.isEnabled ? feature.configUrl : ''; const configUrl = feature.isEnabled ? feature.configUrl : '';
...@@ -26,35 +27,23 @@ const IFRAME_SANDBOX_ATTRIBUTE = 'allow-forms allow-orientation-lock ' + ...@@ -26,35 +27,23 @@ const IFRAME_SANDBOX_ATTRIBUTE = 'allow-forms allow-orientation-lock ' +
const IFRAME_ALLOW_ATTRIBUTE = 'clipboard-read; clipboard-write;'; const IFRAME_ALLOW_ATTRIBUTE = 'clipboard-read; clipboard-write;';
const MarketplaceApp = () => { type Props = {
const ref = useRef<HTMLIFrameElement>(null); address: string | undefined;
data: MarketplaceAppOverview | undefined;
const apiFetch = useApiFetch(); isPending: boolean;
const appProps = useAppContext(); };
const router = useRouter();
const id = getQueryParamString(router.query.id);
const { isPending, isError, error, data } = useQuery<unknown, ResourceError<unknown>, MarketplaceAppOverview>({
queryKey: [ 'marketplace-apps', id ],
queryFn: async() => {
const result = await apiFetch<Array<MarketplaceAppOverview>, unknown>(configUrl, undefined, { resource: 'marketplace-apps' });
if (!Array.isArray(result)) {
throw result;
}
const item = result.find((app: MarketplaceAppOverview) => app.id === id);
if (!item) {
throw { status: 404 };
}
return item; const MarketplaceAppContent = ({ address, data, isPending }: Props) => {
}, const { iframeRef, isReady } = useDappscoutIframe();
enabled: feature.isEnabled,
});
const [ iframeKey, setIframeKey ] = useState(0);
const [ isFrameLoading, setIsFrameLoading ] = useState(isPending); const [ isFrameLoading, setIsFrameLoading ] = useState(isPending);
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
useEffect(() => {
setIframeKey((key) => key + 1);
}, [ address ]);
const handleIframeLoad = useCallback(() => { const handleIframeLoad = useCallback(() => {
setIsFrameLoading(false); setIsFrameLoading(false);
}, []); }, []);
...@@ -72,9 +61,61 @@ const MarketplaceApp = () => { ...@@ -72,9 +61,61 @@ const MarketplaceApp = () => {
blockscoutNetworkRpc: config.chain.rpcUrl, blockscoutNetworkRpc: config.chain.rpcUrl,
}; };
ref?.current?.contentWindow?.postMessage(message, data.url); iframeRef?.current?.contentWindow?.postMessage(message, data.url);
} }
}, [ isFrameLoading, data, colorMode, ref ]); }, [ isFrameLoading, data, colorMode, iframeRef ]);
return (
<Center
h="100vh"
mx={{ base: -4, lg: -6 }}
>
{ (isFrameLoading) && (
<ContentLoader/>
) }
{ (data && isReady) && (
<Box
key={ iframeKey }
allow={ IFRAME_ALLOW_ATTRIBUTE }
ref={ iframeRef }
sandbox={ IFRAME_SANDBOX_ATTRIBUTE }
as="iframe"
h="100%"
w="100%"
display={ isFrameLoading ? 'none' : 'block' }
src={ data.url }
title={ data.title }
onLoad={ handleIframeLoad }
/>
) }
</Center>
);
};
const MarketplaceApp = () => {
const { address, sendTransaction, signMessage, signTypedData } = useMarketplaceWallet();
const apiFetch = useApiFetch();
const router = useRouter();
const id = getQueryParamString(router.query.id);
const { isPending, isError, error, data } = useQuery<unknown, ResourceError<unknown>, MarketplaceAppOverview>({
queryKey: [ 'marketplace-apps', id ],
queryFn: async() => {
const result = await apiFetch<Array<MarketplaceAppOverview>, unknown>(configUrl, undefined, { resource: 'marketplace-apps' });
if (!Array.isArray(result)) {
throw result;
}
const item = result.find((app: MarketplaceAppOverview) => app.id === id);
if (!item) {
throw { status: 404 };
}
return item;
},
enabled: feature.isEnabled,
});
useEffect(() => { useEffect(() => {
if (data) { if (data) {
...@@ -89,46 +130,17 @@ const MarketplaceApp = () => { ...@@ -89,46 +130,17 @@ const MarketplaceApp = () => {
throw new Error('Unable to load app', { cause: error }); throw new Error('Unable to load app', { cause: error });
} }
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer.includes('/apps');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to marketplace',
url: appProps.referrer,
};
}, [ appProps.referrer ]);
return ( return (
<> <DappscoutIframeProvider
{ !isPending && <PageTitle title={ data.title } backLink={ backLink }/> } address={ address }
<Center appUrl={ data?.url }
h="100vh" rpcUrl={ config.chain.rpcUrl }
mx={{ base: -4, lg: -12 }} sendTransaction={ sendTransaction }
> signMessage={ signMessage }
{ (isFrameLoading) && ( signTypedData={ signTypedData }
<ContentLoader/> >
) } <MarketplaceAppContent address={ address } data={ data } isPending={ isPending }/>
</DappscoutIframeProvider>
{ data && (
<Box
allow={ IFRAME_ALLOW_ATTRIBUTE }
ref={ ref }
sandbox={ IFRAME_SANDBOX_ATTRIBUTE }
as="iframe"
h="100%"
w="100%"
display={ isFrameLoading ? 'none' : 'block' }
src={ data.url }
title={ data.title }
onLoad={ handleIframeLoad }
/>
) }
</Center>
</>
); );
}; };
......
...@@ -3,7 +3,7 @@ import { useRouter } from 'next/router'; ...@@ -3,7 +3,7 @@ import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { TokenType } from 'types/api/token'; import type { TokenType } from 'types/api/token';
import type { TokensSortingValue } from 'types/api/tokens'; import type { TokensSortingValue, TokensSortingField, TokensSorting } from 'types/api/tokens';
import type { RoutedTab } from 'ui/shared/Tabs/types'; import type { RoutedTab } from 'ui/shared/Tabs/types';
import config from 'configs/app'; import config from 'configs/app';
...@@ -16,11 +16,13 @@ import PopoverFilter from 'ui/shared/filters/PopoverFilter'; ...@@ -16,11 +16,13 @@ import PopoverFilter from 'ui/shared/filters/PopoverFilter';
import TokenTypeFilter from 'ui/shared/filters/TokenTypeFilter'; import TokenTypeFilter from 'ui/shared/filters/TokenTypeFilter';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue';
import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TokensList from 'ui/tokens/Tokens'; import TokensList from 'ui/tokens/Tokens';
import TokensActionBar from 'ui/tokens/TokensActionBar'; import TokensActionBar from 'ui/tokens/TokensActionBar';
import TokensBridgedChainsFilter from 'ui/tokens/TokensBridgedChainsFilter'; import TokensBridgedChainsFilter from 'ui/tokens/TokensBridgedChainsFilter';
import { getSortParamsFromValue, getSortValueFromQuery, getTokenFilterValue, getBridgedChainsFilterValue } from 'ui/tokens/utils'; import { SORT_OPTIONS, getTokenFilterValue, getBridgedChainsFilterValue } from 'ui/tokens/utils';
const TAB_LIST_PROPS = { const TAB_LIST_PROPS = {
marginBottom: 0, marginBottom: 0,
...@@ -44,7 +46,7 @@ const Tokens = () => { ...@@ -44,7 +46,7 @@ const Tokens = () => {
const q = getQueryParamString(router.query.q); const q = getQueryParamString(router.query.q);
const [ searchTerm, setSearchTerm ] = React.useState<string>(q ?? ''); const [ searchTerm, setSearchTerm ] = React.useState<string>(q ?? '');
const [ sort, setSort ] = React.useState<TokensSortingValue | undefined>(getSortValueFromQuery(router.query)); const [ sort, setSort ] = React.useState<TokensSortingValue | undefined>(getSortValueFromQuery<TokensSortingValue>(router.query, SORT_OPTIONS));
const [ tokenTypes, setTokenTypes ] = React.useState<Array<TokenType> | undefined>(getTokenFilterValue(router.query.type)); const [ tokenTypes, setTokenTypes ] = React.useState<Array<TokenType> | undefined>(getTokenFilterValue(router.query.type));
const [ bridgeChains, setBridgeChains ] = React.useState<Array<string> | undefined>(getBridgedChainsFilterValue(router.query.chain_ids)); const [ bridgeChains, setBridgeChains ] = React.useState<Array<string> | undefined>(getBridgedChainsFilterValue(router.query.chain_ids));
...@@ -53,7 +55,7 @@ const Tokens = () => { ...@@ -53,7 +55,7 @@ const Tokens = () => {
const tokensQuery = useQueryWithPages({ const tokensQuery = useQueryWithPages({
resourceName: tab === 'bridged' ? 'tokens_bridged' : 'tokens', resourceName: tab === 'bridged' ? 'tokens_bridged' : 'tokens',
filters: tab === 'bridged' ? { q: debouncedSearchTerm, chain_ids: bridgeChains } : { q: debouncedSearchTerm, type: tokenTypes }, filters: tab === 'bridged' ? { q: debouncedSearchTerm, chain_ids: bridgeChains } : { q: debouncedSearchTerm, type: tokenTypes },
sorting: getSortParamsFromValue(sort), sorting: getSortParamsFromValue<TokensSortingValue, TokensSortingField, TokensSorting['order']>(sort),
options: { options: {
placeholderData: generateListStub<'tokens'>( placeholderData: generateListStub<'tokens'>(
TOKEN_INFO_ERC_20, TOKEN_INFO_ERC_20,
......
...@@ -13,8 +13,8 @@ import PageTitle from 'ui/shared/Page/PageTitle'; ...@@ -13,8 +13,8 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination'; import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TxsContent from 'ui/txs/TxsContent';
import TxsWatchlist from 'ui/txs/TxsWatchlist'; import TxsWatchlist from 'ui/txs/TxsWatchlist';
import TxsWithFrontendSorting from 'ui/txs/TxsWithFrontendSorting';
const TAB_LIST_PROPS = { const TAB_LIST_PROPS = {
marginBottom: 0, marginBottom: 0,
...@@ -60,12 +60,13 @@ const Transactions = () => { ...@@ -60,12 +60,13 @@ const Transactions = () => {
{ {
id: 'validated', id: 'validated',
title: verifiedTitle, title: verifiedTitle,
component: <TxsContent query={ txsQuery } showSocketInfo={ txsQuery.pagination.page === 1 } socketInfoNum={ num } socketInfoAlert={ socketAlert }/> }, component:
<TxsWithFrontendSorting query={ txsQuery } showSocketInfo={ txsQuery.pagination.page === 1 } socketInfoNum={ num } socketInfoAlert={ socketAlert }/> },
{ {
id: 'pending', id: 'pending',
title: 'Pending', title: 'Pending',
component: ( component: (
<TxsContent <TxsWithFrontendSorting
query={ txsQuery } query={ txsQuery }
showBlockInfo={ false } showBlockInfo={ false }
showSocketInfo={ txsQuery.pagination.page === 1 } showSocketInfo={ txsQuery.pagination.page === 1 }
......
...@@ -3,6 +3,7 @@ import { useRouter } from 'next/router'; ...@@ -3,6 +3,7 @@ import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { VerifiedContractsFilters } from 'types/api/contracts'; import type { VerifiedContractsFilters } from 'types/api/contracts';
import type { VerifiedContractsSorting, VerifiedContractsSortingField, VerifiedContractsSortingValue } from 'types/api/verifiedContracts';
import useDebounce from 'lib/hooks/useDebounce'; import useDebounce from 'lib/hooks/useDebounce';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
...@@ -16,9 +17,10 @@ import FilterInput from 'ui/shared/filters/FilterInput'; ...@@ -16,9 +17,10 @@ import FilterInput from 'ui/shared/filters/FilterInput';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination'; import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue';
import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery';
import Sort from 'ui/shared/sort/Sort'; import Sort from 'ui/shared/sort/Sort';
import type { SortField, Sort as TSort } from 'ui/verifiedContracts/utils'; import { SORT_OPTIONS } from 'ui/verifiedContracts/utils';
import { SORT_OPTIONS, sortFn, getNextSortValue } from 'ui/verifiedContracts/utils';
import VerifiedContractsCounters from 'ui/verifiedContracts/VerifiedContractsCounters'; import VerifiedContractsCounters from 'ui/verifiedContracts/VerifiedContractsCounters';
import VerifiedContractsFilter from 'ui/verifiedContracts/VerifiedContractsFilter'; import VerifiedContractsFilter from 'ui/verifiedContracts/VerifiedContractsFilter';
import VerifiedContractsList from 'ui/verifiedContracts/VerifiedContractsList'; import VerifiedContractsList from 'ui/verifiedContracts/VerifiedContractsList';
...@@ -28,15 +30,17 @@ const VerifiedContracts = () => { ...@@ -28,15 +30,17 @@ const VerifiedContracts = () => {
const router = useRouter(); const router = useRouter();
const [ searchTerm, setSearchTerm ] = React.useState(getQueryParamString(router.query.q) || undefined); const [ searchTerm, setSearchTerm ] = React.useState(getQueryParamString(router.query.q) || undefined);
const [ type, setType ] = React.useState(getQueryParamString(router.query.filter) as VerifiedContractsFilters['filter'] || undefined); const [ type, setType ] = React.useState(getQueryParamString(router.query.filter) as VerifiedContractsFilters['filter'] || undefined);
const [ sort, setSort ] = React.useState<TSort>(); const [ sort, setSort ] =
React.useState<VerifiedContractsSortingValue | undefined>(getSortValueFromQuery<VerifiedContractsSortingValue>(router.query, SORT_OPTIONS));
const debouncedSearchTerm = useDebounce(searchTerm || '', 300); const debouncedSearchTerm = useDebounce(searchTerm || '', 300);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { isError, isPlaceholderData, data, pagination, onFilterChange } = useQueryWithPages({ const { isError, isPlaceholderData, data, pagination, onFilterChange, onSortingChange } = useQueryWithPages({
resourceName: 'verified_contracts', resourceName: 'verified_contracts',
filters: { q: debouncedSearchTerm, filter: type }, filters: { q: debouncedSearchTerm, filter: type },
sorting: getSortParamsFromValue<VerifiedContractsSortingValue, VerifiedContractsSortingField, VerifiedContractsSorting['order']>(sort),
options: { options: {
placeholderData: generateListStub<'verified_contracts'>( placeholderData: generateListStub<'verified_contracts'>(
VERIFIED_CONTRACT_INFO, VERIFIED_CONTRACT_INFO,
...@@ -67,11 +71,10 @@ const VerifiedContracts = () => { ...@@ -67,11 +71,10 @@ const VerifiedContracts = () => {
setType(filter); setType(filter);
}, [ debouncedSearchTerm, onFilterChange ]); }, [ debouncedSearchTerm, onFilterChange ]);
const handleSortToggle = React.useCallback((field: SortField) => { const handleSortChange = React.useCallback((value?: VerifiedContractsSortingValue) => {
return () => { setSort(value);
setSort(getNextSortValue(field)); onSortingChange(getSortParamsFromValue(value));
}; }, [ onSortingChange ]);
}, []);
const typeFilter = <VerifiedContractsFilter onChange={ handleTypeChange } defaultValue={ type } isActive={ Boolean(type) }/>; const typeFilter = <VerifiedContractsFilter onChange={ handleTypeChange } defaultValue={ type } isActive={ Boolean(type) }/>;
...@@ -89,7 +92,7 @@ const VerifiedContracts = () => { ...@@ -89,7 +92,7 @@ const VerifiedContracts = () => {
<Sort <Sort
options={ SORT_OPTIONS } options={ SORT_OPTIONS }
sort={ sort } sort={ sort }
setSort={ setSort } setSort={ handleSortChange }
/> />
); );
...@@ -112,15 +115,13 @@ const VerifiedContracts = () => { ...@@ -112,15 +115,13 @@ const VerifiedContracts = () => {
</> </>
); );
const sortedData = data?.items.slice().sort(sortFn(sort)); const content = data?.items ? (
const content = sortedData ? (
<> <>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
<VerifiedContractsList data={ sortedData } isLoading={ isPlaceholderData }/> <VerifiedContractsList data={ data.items } isLoading={ isPlaceholderData }/>
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<VerifiedContractsTable data={ sortedData } sort={ sort } onSortToggle={ handleSortToggle } isLoading={ isPlaceholderData }/> <VerifiedContractsTable data={ data.items } sort={ sort } setSorting={ handleSortChange } isLoading={ isPlaceholderData }/>
</Hide> </Hide>
</> </>
) : null; ) : null;
......
...@@ -14,7 +14,7 @@ import PageTitle from 'ui/shared/Page/PageTitle'; ...@@ -14,7 +14,7 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import TxsContent from 'ui/txs/TxsContent'; import TxsWithFrontendSorting from 'ui/txs/TxsWithFrontendSorting';
import ZkEvmL2TxnBatchDetails from 'ui/zkEvmL2TxnBatches/ZkEvmL2TxnBatchDetails'; import ZkEvmL2TxnBatchDetails from 'ui/zkEvmL2TxnBatches/ZkEvmL2TxnBatchDetails';
const ZkEvmL2TxnBatch = () => { const ZkEvmL2TxnBatch = () => {
...@@ -51,7 +51,7 @@ const ZkEvmL2TxnBatch = () => { ...@@ -51,7 +51,7 @@ const ZkEvmL2TxnBatch = () => {
const tabs: Array<RoutedTab> = React.useMemo(() => ([ const tabs: Array<RoutedTab> = React.useMemo(() => ([
{ id: 'index', title: 'Details', component: <ZkEvmL2TxnBatchDetails query={ batchQuery }/> }, { id: 'index', title: 'Details', component: <ZkEvmL2TxnBatchDetails query={ batchQuery }/> },
{ id: 'txs', title: 'Transactions', component: <TxsContent query={ batchTxsQuery } showSocketInfo={ false }/> }, { id: 'txs', title: 'Transactions', component: <TxsWithFrontendSorting query={ batchTxsQuery } showSocketInfo={ false }/> },
].filter(Boolean)), [ batchQuery, batchTxsQuery ]); ].filter(Boolean)), [ batchQuery, batchTxsQuery ]);
const backLink = React.useMemo(() => { const backLink = React.useMemo(() => {
......
import { SkeletonCircle, Image } from '@chakra-ui/react'; import { SkeletonCircle, Image, Icon } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import profileIcon from 'icons/profile.svg';
import { useAppContext } from 'lib/contexts/app'; import { useAppContext } from 'lib/contexts/app';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo'; import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import IdenticonGithub from 'ui/shared/IdenticonGithub';
interface Props { interface Props {
size: number; size: number;
...@@ -34,7 +34,7 @@ const UserAvatar = ({ size }: Props) => { ...@@ -34,7 +34,7 @@ const UserAvatar = ({ size }: Props) => {
boxSize={ `${ size }px` } boxSize={ `${ size }px` }
borderRadius="full" borderRadius="full"
overflow="hidden" overflow="hidden"
fallback={ isImageLoadError || !data?.avatar ? <IdenticonGithub size={ size } seed={ data?.email || 'randomness' } flexShrink={ 0 }/> : undefined } fallback={ isImageLoadError || !data?.avatar ? <Icon as={ profileIcon } boxSize={ 5 }/> : undefined }
onError={ handleImageLoadError } onError={ handleImageLoadError }
/> />
); );
......
...@@ -75,7 +75,7 @@ const Web3ModalProvider = ({ children, fallback }: Props) => { ...@@ -75,7 +75,7 @@ const Web3ModalProvider = ({ children, fallback }: Props) => {
const web3ModalTheme = useColorModeValue('light', 'dark'); const web3ModalTheme = useColorModeValue('light', 'dark');
if (!wagmiConfig || !ethereumClient || !feature.isEnabled) { if (!wagmiConfig || !ethereumClient || !feature.isEnabled) {
return typeof fallback === 'function' ? fallback() : (fallback || null); return typeof fallback === 'function' ? fallback() : (fallback || <>{ children }</>); // eslint-disable-line react/jsx-no-useless-fragment
} }
return ( return (
......
import React from 'react';
import type { Props } from './types';
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
import HeaderAlert from 'ui/snippets/header/HeaderAlert';
import HeaderDesktop from 'ui/snippets/header/HeaderDesktop';
import HeaderMobile from 'ui/snippets/header/HeaderMobile';
import * as Layout from './components';
const LayoutDefault = ({ children }: Props) => {
return (
<Layout.Container>
<HeaderMobile/>
<Layout.MainArea>
<Layout.MainColumn
paddingTop={{ base: '138px', lg: 6 }}
paddingX={{ base: 4, lg: 6 }}
>
<HeaderAlert/>
<HeaderDesktop isMarketplaceAppPage/>
<AppErrorBoundary>
<Layout.Content pt={{ base: 0, lg: 6 }}>
{ children }
</Layout.Content>
</AppErrorBoundary>
</Layout.MainColumn>
</Layout.MainArea>
<Layout.Footer/>
</Layout.Container>
);
};
export default LayoutDefault;
import { Box } from '@chakra-ui/react'; import { Box, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
interface Props { interface Props {
className?: string;
children: React.ReactNode; children: React.ReactNode;
} }
const Content = ({ children }: Props) => { const Content = ({ children, className }: Props) => {
return ( return (
<Box pt={{ base: 0, lg: '52px' }} as="main"> <Box pt={{ base: 0, lg: '52px' }} as="main" className={ className }>
{ children } { children }
</Box> </Box>
); );
}; };
export default React.memo(Content); export default React.memo(chakra(Content));
...@@ -41,7 +41,7 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasPage ...@@ -41,7 +41,7 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasPage
aria-label="Prev page" aria-label="Prev page"
w="36px" w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 }/> } icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 }/> }
isDisabled={ !canGoBackwards || page === 1 || isLoading } isDisabled={ !canGoBackwards || isLoading }
/> />
</Skeleton> </Skeleton>
<Skeleton isLoaded={ !showSkeleton } display="inline-block" borderRadius="base"> <Skeleton isLoaded={ !showSkeleton } display="inline-block" borderRadius="base">
......
...@@ -68,7 +68,7 @@ it('returns correct data if there is only one page', async() => { ...@@ -68,7 +68,7 @@ it('returns correct data if there is only one page', async() => {
expect(result.current.data).toEqual(responses.page_empty); expect(result.current.data).toEqual(responses.page_empty);
expect(result.current.pagination).toMatchObject({ expect(result.current.pagination).toMatchObject({
page: 1, page: 1,
canGoBackwards: true, canGoBackwards: false,
hasNextPage: false, hasNextPage: false,
isLoading: false, isLoading: false,
isVisible: false, isVisible: false,
...@@ -91,7 +91,7 @@ describe('if there are multiple pages', () => { ...@@ -91,7 +91,7 @@ describe('if there are multiple pages', () => {
expect(result.current.data).toEqual(responses.page_1); expect(result.current.data).toEqual(responses.page_1);
expect(result.current.pagination).toMatchObject({ expect(result.current.pagination).toMatchObject({
page: 1, page: 1,
canGoBackwards: true, canGoBackwards: false,
hasNextPage: true, hasNextPage: true,
isLoading: false, isLoading: false,
isVisible: true, isVisible: true,
...@@ -258,7 +258,7 @@ describe('if there are multiple pages', () => { ...@@ -258,7 +258,7 @@ describe('if there are multiple pages', () => {
expect(result.current.data).toEqual(responses.page_1); expect(result.current.data).toEqual(responses.page_1);
expect(result.current.pagination).toMatchObject({ expect(result.current.pagination).toMatchObject({
page: 1, page: 1,
canGoBackwards: true, canGoBackwards: false,
hasNextPage: true, hasNextPage: true,
isLoading: false, isLoading: false,
isVisible: true, isVisible: true,
...@@ -310,7 +310,7 @@ describe('if there are multiple pages', () => { ...@@ -310,7 +310,7 @@ describe('if there are multiple pages', () => {
expect(result.current.data).toEqual(responses.page_1); expect(result.current.data).toEqual(responses.page_1);
expect(result.current.pagination).toMatchObject({ expect(result.current.pagination).toMatchObject({
page: 1, page: 1,
canGoBackwards: true, canGoBackwards: false,
hasNextPage: true, hasNextPage: true,
isLoading: false, isLoading: false,
isVisible: true, isVisible: true,
...@@ -404,7 +404,7 @@ describe('if there is page query param in URL', () => { ...@@ -404,7 +404,7 @@ describe('if there is page query param in URL', () => {
expect(result.current.data).toEqual(responses.page_3); expect(result.current.data).toEqual(responses.page_3);
expect(result.current.pagination).toMatchObject({ expect(result.current.pagination).toMatchObject({
page: 3, page: 3,
canGoBackwards: false, canGoBackwards: true,
hasNextPage: false, hasNextPage: false,
isLoading: false, isLoading: false,
isVisible: true, isVisible: true,
...@@ -458,7 +458,7 @@ describe('queries with filters', () => { ...@@ -458,7 +458,7 @@ describe('queries with filters', () => {
expect(result.current.data).toEqual(responses.page_filtered); expect(result.current.data).toEqual(responses.page_filtered);
expect(result.current.pagination).toMatchObject({ expect(result.current.pagination).toMatchObject({
page: 1, page: 1,
canGoBackwards: true, canGoBackwards: false,
hasNextPage: true, hasNextPage: true,
isLoading: false, isLoading: false,
isVisible: true, isVisible: true,
...@@ -547,7 +547,7 @@ describe('queries with sorting', () => { ...@@ -547,7 +547,7 @@ describe('queries with sorting', () => {
expect(result.current.data).toEqual(responses.page_sorted); expect(result.current.data).toEqual(responses.page_sorted);
expect(result.current.pagination).toMatchObject({ expect(result.current.pagination).toMatchObject({
page: 1, page: 1,
canGoBackwards: true, canGoBackwards: false,
hasNextPage: false, hasNextPage: false,
isLoading: false, isLoading: false,
isVisible: false, isVisible: false,
......
...@@ -61,7 +61,6 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -61,7 +61,6 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
const [ hasPages, setHasPages ] = React.useState(page > 1); const [ hasPages, setHasPages ] = React.useState(page > 1);
const isMounted = React.useRef(false); const isMounted = React.useRef(false);
const canGoBackwards = React.useRef(!router.query.page);
const queryParams = { ...pageParams[page], ...filters, ...sorting }; const queryParams = { ...pageParams[page], ...filters, ...sorting };
const scrollToTop = useCallback(() => { const scrollToTop = useCallback(() => {
...@@ -107,7 +106,6 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -107,7 +106,6 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
let nextPageQuery: typeof router.query = { ...router.query }; let nextPageQuery: typeof router.query = { ...router.query };
if (page === 2) { if (page === 2) {
nextPageQuery = omit(router.query, [ 'next_page_params', 'page' ]); nextPageQuery = omit(router.query, [ 'next_page_params', 'page' ]);
canGoBackwards.current = true;
} else { } else {
nextPageQuery.next_page_params = encodeURIComponent(JSON.stringify(pageParams[page - 1])); nextPageQuery.next_page_params = encodeURIComponent(JSON.stringify(pageParams[page - 1]));
nextPageQuery.page = String(page - 1); nextPageQuery.page = String(page - 1);
...@@ -130,7 +128,6 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -130,7 +128,6 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
queryClient.removeQueries({ queryKey: [ resourceName ] }); queryClient.removeQueries({ queryKey: [ resourceName ] });
setPage(1); setPage(1);
setPageParams({}); setPageParams({});
canGoBackwards.current = true;
window.setTimeout(() => { window.setTimeout(() => {
// FIXME after router is updated we still have inactive queries for previously visited page (e.g third), where we came from // FIXME after router is updated we still have inactive queries for previously visited page (e.g third), where we came from
// so have to remove it but with some delay :) // so have to remove it but with some delay :)
...@@ -193,7 +190,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -193,7 +190,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
resetPage, resetPage,
hasPages, hasPages,
hasNextPage, hasNextPage,
canGoBackwards: canGoBackwards.current, canGoBackwards: Boolean(pageParams[page - 1]),
isLoading: queryResult.isPlaceholderData, isLoading: queryResult.isPlaceholderData,
isVisible: hasPages || hasNextPage, isVisible: hasPages || hasNextPage,
}; };
......
export default function getSortParamsFromValue<SortValue extends string, SortField extends string, SortOrder extends string>(val?: SortValue) {
if (!val) {
return undefined;
}
const sortingChunks = val.split('-') as [ SortField, SortOrder ];
return { sort: sortingChunks[0], order: sortingChunks[1] };
}
import type { Query } from 'nextjs-routes';
import type { Option } from 'ui/shared/sort/Sort';
export default function getSortValueFromQuery<SortValue extends string>(query: Query, sortOptions: Array<Option<SortValue>>) {
if (!query.sort || !query.order) {
return undefined;
}
const str = query.sort + '-' + query.order;
if (sortOptions.map(option => option.id).includes(str as SortValue)) {
return str as SortValue;
}
}
...@@ -10,7 +10,11 @@ import NetworkMenuButton from 'ui/snippets/networkMenu/NetworkMenuButton'; ...@@ -10,7 +10,11 @@ import NetworkMenuButton from 'ui/snippets/networkMenu/NetworkMenuButton';
import NetworkMenuContentMobile from 'ui/snippets/networkMenu/NetworkMenuContentMobile'; import NetworkMenuContentMobile from 'ui/snippets/networkMenu/NetworkMenuContentMobile';
import useNetworkMenu from 'ui/snippets/networkMenu/useNetworkMenu'; import useNetworkMenu from 'ui/snippets/networkMenu/useNetworkMenu';
const Burger = () => { interface Props {
isMarketplaceAppPage?: boolean;
}
const Burger = ({ isMarketplaceAppPage }: Props) => {
const iconColor = useColorModeValue('gray.600', 'white'); const iconColor = useColorModeValue('gray.600', 'white');
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const networkMenu = useNetworkMenu(); const networkMenu = useNetworkMenu();
...@@ -26,7 +30,7 @@ const Burger = () => { ...@@ -26,7 +30,7 @@ const Burger = () => {
return ( return (
<> <>
<Box padding={ 2 } onClick={ onOpen }> <Box padding={ 2 } onClick={ onOpen } cursor="pointer">
<Icon <Icon
as={ burgerIcon } as={ burgerIcon }
boxSize={ 6 } boxSize={ 6 }
...@@ -57,7 +61,7 @@ const Burger = () => { ...@@ -57,7 +61,7 @@ const Burger = () => {
</Flex> </Flex>
{ networkMenu.isOpen ? { networkMenu.isOpen ?
<NetworkMenuContentMobile tabs={ networkMenu.availableTabs } items={ networkMenu.data }/> : <NetworkMenuContentMobile tabs={ networkMenu.availableTabs } items={ networkMenu.data }/> :
<NavigationMobile onNavLinkClick={ onClose }/> <NavigationMobile onNavLinkClick={ onClose } isMarketplaceAppPage={ isMarketplaceAppPage }/>
} }
</DrawerBody> </DrawerBody>
</DrawerContent> </DrawerContent>
......
...@@ -2,14 +2,19 @@ import { HStack, Box } from '@chakra-ui/react'; ...@@ -2,14 +2,19 @@ import { HStack, Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop'; import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import SearchBar from 'ui/snippets/searchBar/SearchBar'; import SearchBar from 'ui/snippets/searchBar/SearchBar';
import WalletMenuDesktop from 'ui/snippets/walletMenu/WalletMenuDesktop';
import Burger from './Burger';
type Props = { type Props = {
renderSearchBar?: () => React.ReactNode; renderSearchBar?: () => React.ReactNode;
isMarketplaceAppPage?: boolean;
} }
const HeaderDesktop = ({ renderSearchBar }: Props) => { const HeaderDesktop = ({ renderSearchBar, isMarketplaceAppPage }: Props) => {
const searchBar = renderSearchBar ? renderSearchBar() : <SearchBar/>; const searchBar = renderSearchBar ? renderSearchBar() : <SearchBar/>;
...@@ -22,10 +27,19 @@ const HeaderDesktop = ({ renderSearchBar }: Props) => { ...@@ -22,10 +27,19 @@ const HeaderDesktop = ({ renderSearchBar }: Props) => {
justifyContent="center" justifyContent="center"
gap={ 12 } gap={ 12 }
> >
{ isMarketplaceAppPage && (
<Box display="flex" alignItems="center" gap={ 3 }>
<Burger isMarketplaceAppPage/>
<NetworkLogo isCollapsed/>
</Box>
) }
<Box width="100%"> <Box width="100%">
{ searchBar } { searchBar }
</Box> </Box>
{ config.features.account.isEnabled && <ProfileMenuDesktop/> } <Box display="flex">
{ config.features.account.isEnabled && <ProfileMenuDesktop/> }
{ config.features.blockchainInteraction.isEnabled && <WalletMenuDesktop/> }
</Box>
</HStack> </HStack>
); );
}; };
......
...@@ -7,6 +7,7 @@ import { useScrollDirection } from 'lib/contexts/scrollDirection'; ...@@ -7,6 +7,7 @@ import { useScrollDirection } from 'lib/contexts/scrollDirection';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo'; import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import ProfileMenuMobile from 'ui/snippets/profileMenu/ProfileMenuMobile'; import ProfileMenuMobile from 'ui/snippets/profileMenu/ProfileMenuMobile';
import SearchBar from 'ui/snippets/searchBar/SearchBar'; import SearchBar from 'ui/snippets/searchBar/SearchBar';
import WalletMenuMobile from 'ui/snippets/walletMenu/WalletMenuMobile';
import Burger from './Burger'; import Burger from './Burger';
...@@ -47,7 +48,10 @@ const HeaderMobile = ({ isHomePage, renderSearchBar }: Props) => { ...@@ -47,7 +48,10 @@ const HeaderMobile = ({ isHomePage, renderSearchBar }: Props) => {
> >
<Burger/> <Burger/>
<NetworkLogo/> <NetworkLogo/>
{ config.features.account.isEnabled ? <ProfileMenuMobile/> : <Box boxSize={ 10 }/> } <Flex columnGap={ 2 }>
{ config.features.account.isEnabled ? <ProfileMenuMobile/> : <Box boxSize={ 10 }/> }
{ config.features.blockchainInteraction.isEnabled && <WalletMenuMobile/> }
</Flex>
</Flex> </Flex>
{ !isHomePage && searchBar } { !isHomePage && searchBar }
</Box> </Box>
......
...@@ -17,10 +17,11 @@ import useNavLinkStyleProps from './useNavLinkStyleProps'; ...@@ -17,10 +17,11 @@ import useNavLinkStyleProps from './useNavLinkStyleProps';
type Props = { type Props = {
item: NavGroupItem; item: NavGroupItem;
onClick: () => void; onClick: () => void;
isExpanded?: boolean;
} }
const NavLinkGroup = ({ item, onClick }: Props) => { const NavLinkGroup = ({ item, onClick, isExpanded }: Props) => {
const styleProps = useNavLinkStyleProps({ isActive: item.isActive }); const styleProps = useNavLinkStyleProps({ isActive: item.isActive, isExpanded });
return ( return (
<Box as="li" listStyleType="none" w="100%" onClick={ onClick }> <Box as="li" listStyleType="none" w="100%" onClick={ onClick }>
......
...@@ -11,9 +11,10 @@ import NavLinkGroupMobile from './NavLinkGroupMobile'; ...@@ -11,9 +11,10 @@ import NavLinkGroupMobile from './NavLinkGroupMobile';
interface Props { interface Props {
onNavLinkClick?: () => void; onNavLinkClick?: () => void;
isMarketplaceAppPage?: boolean;
} }
const NavigationMobile = ({ onNavLinkClick }: Props) => { const NavigationMobile = ({ onNavLinkClick, isMarketplaceAppPage }: Props) => {
const { mainNavItems, accountNavItems } = useNavItems(); const { mainNavItems, accountNavItems } = useNavItems();
const [ openedGroupIndex, setOpenedGroupIndex ] = React.useState(-1); const [ openedGroupIndex, setOpenedGroupIndex ] = React.useState(-1);
...@@ -38,6 +39,8 @@ const NavigationMobile = ({ onNavLinkClick }: Props) => { ...@@ -38,6 +39,8 @@ const NavigationMobile = ({ onNavLinkClick }: Props) => {
const openedItem = mainNavItems[openedGroupIndex]; const openedItem = mainNavItems[openedGroupIndex];
const isCollapsed = isMarketplaceAppPage ? false : undefined;
return ( return (
<Flex position="relative" flexDirection="column" flexGrow={ 1 }> <Flex position="relative" flexDirection="column" flexGrow={ 1 }>
<Box <Box
...@@ -61,9 +64,9 @@ const NavigationMobile = ({ onNavLinkClick }: Props) => { ...@@ -61,9 +64,9 @@ const NavigationMobile = ({ onNavLinkClick }: Props) => {
> >
{ mainNavItems.map((item, index) => { { mainNavItems.map((item, index) => {
if (isGroupItem(item)) { if (isGroupItem(item)) {
return <NavLinkGroupMobile key={ item.text } item={ item } onClick={ onGroupItemOpen(index) }/>; return <NavLinkGroupMobile key={ item.text } item={ item } onClick={ onGroupItemOpen(index) } isExpanded={ isMarketplaceAppPage }/>;
} else { } else {
return <NavLink key={ item.text } item={ item } onClick={ onNavLinkClick }/>; return <NavLink key={ item.text } item={ item } onClick={ onNavLinkClick } isCollapsed={ isCollapsed }/>;
} }
}) } }) }
</VStack> </VStack>
...@@ -77,7 +80,7 @@ const NavigationMobile = ({ onNavLinkClick }: Props) => { ...@@ -77,7 +80,7 @@ const NavigationMobile = ({ onNavLinkClick }: Props) => {
borderColor="divider" borderColor="divider"
> >
<VStack as="ul" spacing="1" alignItems="flex-start"> <VStack as="ul" spacing="1" alignItems="flex-start">
{ accountNavItems.map((item) => <NavLink key={ item.text } item={ item } onClick={ onNavLinkClick }/>) } { accountNavItems.map((item) => <NavLink key={ item.text } item={ item } onClick={ onNavLinkClick } isCollapsed={ isCollapsed }/>) }
</VStack> </VStack>
</Box> </Box>
) } ) }
...@@ -113,10 +116,10 @@ const NavigationMobile = ({ onNavLinkClick }: Props) => { ...@@ -113,10 +116,10 @@ const NavigationMobile = ({ onNavLinkClick }: Props) => {
borderColor: 'divider', borderColor: 'divider',
}} }}
> >
{ item.map(subItem => <NavLink key={ subItem.text } item={ subItem } onClick={ onNavLinkClick }/>) } { item.map(subItem => <NavLink key={ subItem.text } item={ subItem } onClick={ onNavLinkClick } isCollapsed={ isCollapsed }/>) }
</Box> </Box>
) : ) :
<NavLink key={ item.text } item={ item } mb={ 1 } onClick={ onNavLinkClick }/>, <NavLink key={ item.text } item={ item } mb={ 1 } onClick={ onNavLinkClick } isCollapsed={ isCollapsed }/>,
) } ) }
</Box> </Box>
</Box> </Box>
......
...@@ -23,7 +23,7 @@ test('no auth', async({ mount, page }) => { ...@@ -23,7 +23,7 @@ test('no auth', async({ mount, page }) => {
{ hooksConfig }, { hooksConfig },
); );
await component.locator('.identicon').click(); await component.locator('a').click();
expect(page.url()).toBe(`${ app.url }/auth/auth0?path=%2F`); expect(page.url()).toBe(`${ app.url }/auth/auth0?path=%2F`);
}); });
......
import type { ButtonProps } from '@chakra-ui/react'; import type { IconButtonProps } from '@chakra-ui/react';
import { Popover, PopoverContent, PopoverBody, PopoverTrigger, Button } from '@chakra-ui/react'; import { Popover, PopoverContent, PopoverBody, PopoverTrigger, IconButton, Tooltip, Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo'; import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
...@@ -8,9 +8,16 @@ import * as mixpanel from 'lib/mixpanel/index'; ...@@ -8,9 +8,16 @@ import * as mixpanel from 'lib/mixpanel/index';
import UserAvatar from 'ui/shared/UserAvatar'; import UserAvatar from 'ui/shared/UserAvatar';
import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent'; import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
const ProfileMenuDesktop = () => { import useMenuButtonColors from '../useMenuButtonColors';
type Props = {
isHomePage?: boolean;
};
const ProfileMenuDesktop = ({ isHomePage }: Props) => {
const { data, error, isPending } = useFetchProfileInfo(); const { data, error, isPending } = useFetchProfileInfo();
const loginUrl = useLoginUrl(); const loginUrl = useLoginUrl();
const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors();
const [ hasMenu, setHasMenu ] = React.useState(false); const [ hasMenu, setHasMenu ] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
...@@ -27,7 +34,7 @@ const ProfileMenuDesktop = () => { ...@@ -27,7 +34,7 @@ const ProfileMenuDesktop = () => {
); );
}, []); }, []);
const buttonProps: Partial<ButtonProps> = (() => { const iconButtonProps: Partial<IconButtonProps> = (() => {
if (hasMenu || !loginUrl) { if (hasMenu || !loginUrl) {
return {}; return {};
} }
...@@ -39,19 +46,53 @@ const ProfileMenuDesktop = () => { ...@@ -39,19 +46,53 @@ const ProfileMenuDesktop = () => {
}; };
})(); })();
const variant = React.useMemo(() => {
if (hasMenu) {
return 'subtle';
}
return isHomePage ? 'solid' : 'outline';
}, [ hasMenu, isHomePage ]);
let iconButtonStyles: Partial<IconButtonProps> = {};
if (hasMenu) {
iconButtonStyles = {
bg: isHomePage ? 'blue.50' : themedBackground,
};
} else if (isHomePage) {
iconButtonStyles = {
color: 'white',
};
} else {
iconButtonStyles = {
borderColor: themedBorderColor,
color: themedColor,
};
}
return ( return (
<Popover openDelay={ 300 } placement="bottom-end" gutter={ 10 } isLazy> <Popover openDelay={ 300 } placement="bottom-end" gutter={ 10 } isLazy>
<PopoverTrigger> <Tooltip
<Button label={ <span>Sign in to My Account to add tags,<br/>create watchlists, access API keys and more</span> }
variant="unstyled" textAlign="center"
display="block" padding={ 2 }
boxSize="50px" isDisabled={ hasMenu }
flexShrink={ 0 } openDelay={ 300 }
{ ...buttonProps } >
> <Box>
<UserAvatar size={ 50 }/> <PopoverTrigger>
</Button> <IconButton
</PopoverTrigger> aria-label="profile menu"
icon={ <UserAvatar size={ 20 }/> }
variant={ variant }
colorScheme="blue"
boxSize="40px"
flexShrink={ 0 }
{ ...iconButtonProps }
{ ...iconButtonStyles }
/>
</PopoverTrigger>
</Box>
</Tooltip>
{ hasMenu && ( { hasMenu && (
<PopoverContent w="212px"> <PopoverContent w="212px">
<PopoverBody padding="24px 16px 16px 16px"> <PopoverBody padding="24px 16px 16px 16px">
......
...@@ -23,7 +23,7 @@ test('no auth', async({ mount, page }) => { ...@@ -23,7 +23,7 @@ test('no auth', async({ mount, page }) => {
{ hooksConfig }, { hooksConfig },
); );
await component.locator('.identicon').click(); await component.locator('a').click();
expect(page.url()).toBe(`${ app.url }/auth/auth0?path=%2F`); expect(page.url()).toBe(`${ app.url }/auth/auth0?path=%2F`);
}); });
......
import { Box, Drawer, DrawerOverlay, DrawerContent, DrawerBody, useDisclosure, Button } from '@chakra-ui/react'; import { Drawer, DrawerOverlay, DrawerContent, DrawerBody, useDisclosure, IconButton } from '@chakra-ui/react';
import type { ButtonProps } from '@chakra-ui/react'; import type { IconButtonProps } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo'; import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
...@@ -8,11 +8,13 @@ import * as mixpanel from 'lib/mixpanel/index'; ...@@ -8,11 +8,13 @@ import * as mixpanel from 'lib/mixpanel/index';
import UserAvatar from 'ui/shared/UserAvatar'; import UserAvatar from 'ui/shared/UserAvatar';
import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent'; import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
import useMenuButtonColors from '../useMenuButtonColors';
const ProfileMenuMobile = () => { const ProfileMenuMobile = () => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const { data, error, isPending } = useFetchProfileInfo(); const { data, error, isPending } = useFetchProfileInfo();
const loginUrl = useLoginUrl(); const loginUrl = useLoginUrl();
const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors();
const [ hasMenu, setHasMenu ] = React.useState(false); const [ hasMenu, setHasMenu ] = React.useState(false);
const handleSignInClick = React.useCallback(() => { const handleSignInClick = React.useCallback(() => {
...@@ -29,7 +31,7 @@ const ProfileMenuMobile = () => { ...@@ -29,7 +31,7 @@ const ProfileMenuMobile = () => {
} }
}, [ data, error?.status, isPending ]); }, [ data, error?.status, isPending ]);
const buttonProps: Partial<ButtonProps> = (() => { const iconButtonProps: Partial<IconButtonProps> = (() => {
if (hasMenu || !loginUrl) { if (hasMenu || !loginUrl) {
return {}; return {};
} }
...@@ -43,17 +45,19 @@ const ProfileMenuMobile = () => { ...@@ -43,17 +45,19 @@ const ProfileMenuMobile = () => {
return ( return (
<> <>
<Box padding={ 2 } onClick={ hasMenu ? onOpen : undefined }> <IconButton
<Button aria-label="profile menu"
variant="unstyled" icon={ <UserAvatar size={ 20 }/> }
display="block" variant={ data?.avatar ? 'subtle' : 'outline' }
boxSize="24px" colorScheme="gray"
flexShrink={ 0 } boxSize="40px"
{ ...buttonProps } flexShrink={ 0 }
> bg={ data?.avatar ? themedBackground : undefined }
<UserAvatar size={ 24 }/> color={ themedColor }
</Button> borderColor={ !data?.avatar ? themedBorderColor : undefined }
</Box> onClick={ hasMenu ? onOpen : undefined }
{ ...iconButtonProps }
/>
{ hasMenu && ( { hasMenu && (
<Drawer <Drawer
isOpen={ isOpen } isOpen={ isOpen }
......
import { LightMode } from '@chakra-ui/react';
import { test as base, expect } from '@playwright/experimental-ct-react'; import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
...@@ -93,9 +92,7 @@ test('search by name homepage +@dark-mode', async({ mount, page }) => { ...@@ -93,9 +92,7 @@ test('search by name homepage +@dark-mode', async({ mount, page }) => {
await mount( await mount(
<TestApp> <TestApp>
<LightMode> <SearchBar isHomepage/>
<SearchBar isHomepage/>
</LightMode>
</TestApp>, </TestApp>,
); );
await page.getByPlaceholder(/search/i).type('o'); await page.getByPlaceholder(/search/i).type('o');
......
...@@ -132,12 +132,6 @@ const SearchBar = ({ isHomepage }: Props) => { ...@@ -132,12 +132,6 @@ const SearchBar = ({ isHomepage }: Props) => {
<PopoverBody <PopoverBody
p={ 0 } p={ 0 }
color="chakra-body-text" color="chakra-body-text"
sx={
isHomepage ? {
mark: { bgColor: 'green.100' },
'*::-webkit-scrollbar-thumb': { backgroundColor: 'blackAlpha.300' },
} : {}
}
> >
<Box <Box
maxH="50vh" maxH="50vh"
......
...@@ -30,8 +30,8 @@ const TopBarStats = () => { ...@@ -30,8 +30,8 @@ const TopBarStats = () => {
<span>${ Number(data.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }) }</span> <span>${ Number(data.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }) }</span>
</Skeleton> </Skeleton>
) } ) }
{ data?.coin_price && data.gas_prices && <TextSeparator color="divider"/> } { data?.coin_price && config.UI.homepage.showGasTracker && <TextSeparator color="divider"/> }
{ data?.gas_prices && ( { data?.gas_prices && config.UI.homepage.showGasTracker && (
<Skeleton isLoaded={ !isPlaceholderData }> <Skeleton isLoaded={ !isPlaceholderData }>
<span>Gas: { data.gas_prices.average } Gwei</span> <span>Gas: { data.gas_prices.average } Gwei</span>
</Skeleton> </Skeleton>
......
import { useColorModeValue } from '@chakra-ui/react';
export default function useMenuColors() {
const themedBackground = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const themedBorderColor = useColorModeValue('gray.300', 'gray.700');
const themedColor = useColorModeValue('blackAlpha.800', 'gray.400');
return { themedBackground, themedBorderColor, themedColor };
}
import { Box, Button, Text } from '@chakra-ui/react';
import React from 'react';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
type Props = {
address?: string;
disconnect?: () => void;
};
const WalletMenuContent = ({ address, disconnect }: Props) => (
<Box>
<Text
fontSize="sm"
fontWeight={ 600 }
mb={ 1 }
{ ...getDefaultTransitionProps() }
>
My wallet
</Text>
<Text
fontSize="sm"
mb={ 5 }
fontWeight={ 400 }
color="text_secondary"
{ ...getDefaultTransitionProps() }
>
Your wallet is used to interact with apps and contracts in the explorer.
</Text>
<AddressEntity
address={{ hash: address }}
noTooltip
truncation="dynamic"
fontSize="sm"
fontWeight={ 700 }
color="text"
mb={ 6 }
/>
<Button size="sm" width="full" variant="outline" onClick={ disconnect }>
Disconnect
</Button>
</Box>
);
export default WalletMenuContent;
import type { ButtonProps } from '@chakra-ui/react';
import { Popover, PopoverContent, PopoverBody, PopoverTrigger, Button, Box, useBoolean } from '@chakra-ui/react';
import React from 'react';
import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon';
import HashStringShorten from 'ui/shared/HashStringShorten';
import useWallet from 'ui/snippets/walletMenu/useWallet';
import WalletMenuContent from 'ui/snippets/walletMenu/WalletMenuContent';
import useMenuButtonColors from '../useMenuButtonColors';
import WalletTooltip from './WalletTooltip';
type Props = {
isHomePage?: boolean;
};
const WalletMenuDesktop = ({ isHomePage }: Props) => {
const { isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen } = useWallet();
const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors();
const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean(false);
const variant = React.useMemo(() => {
if (isWalletConnected) {
return 'subtle';
}
return isHomePage ? 'solid' : 'outline';
}, [ isWalletConnected, isHomePage ]);
let buttonStyles: Partial<ButtonProps> = {};
if (isWalletConnected) {
buttonStyles = {
bg: isHomePage ? 'blue.50' : themedBackground,
color: isHomePage ? 'blackAlpha.800' : themedColor,
_hover: {
color: isHomePage ? 'blackAlpha.800' : themedColor,
},
};
} else if (isHomePage) {
buttonStyles = {
color: 'white',
};
} else {
buttonStyles = {
borderColor: themedBorderColor,
color: themedColor,
};
}
return (
<Popover
openDelay={ 300 }
placement="bottom-end"
gutter={ 10 }
isLazy
isOpen={ isPopoverOpen }
onClose={ setIsPopoverOpen.off }
>
<WalletTooltip isDisabled={ isWalletConnected }>
<Box ml={ 2 }>
<PopoverTrigger>
<Button
variant={ variant }
colorScheme="blue"
flexShrink={ 0 }
isLoading={ isModalOpening || isModalOpen }
loadingText="Connect wallet"
onClick={ isWalletConnected ? setIsPopoverOpen.on : connect }
fontSize="sm"
{ ...buttonStyles }
>
{ isWalletConnected ? (
<>
<Box mr={ 2 }>
<AddressIdenticon size={ 20 } hash={ address }/>
</Box>
<HashStringShorten hash={ address } isTooltipDisabled/>
</>
) : 'Connect wallet' }
</Button>
</PopoverTrigger>
</Box>
</WalletTooltip>
{ isWalletConnected && (
<PopoverContent w="235px">
<PopoverBody padding="24px 16px 16px 16px">
<WalletMenuContent address={ address } disconnect={ disconnect }/>
</PopoverBody>
</PopoverContent>
) }
</Popover>
);
};
export default WalletMenuDesktop;
import { Drawer, DrawerOverlay, DrawerContent, DrawerBody, useDisclosure, IconButton, Icon } from '@chakra-ui/react';
import React from 'react';
import walletIcon from 'icons/wallet.svg';
import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon';
import useWallet from 'ui/snippets/walletMenu/useWallet';
import WalletMenuContent from 'ui/snippets/walletMenu/WalletMenuContent';
import useMenuButtonColors from '../useMenuButtonColors';
import WalletTooltip from './WalletTooltip';
const WalletMenuMobile = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen } = useWallet();
const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors();
return (
<>
<WalletTooltip isDisabled={ isWalletConnected } isMobile>
<IconButton
aria-label="wallet menu"
icon={ isWalletConnected ?
<AddressIdenticon size={ 20 } hash={ address }/> :
<Icon as={ walletIcon } boxSize={ 6 }/>
}
variant={ isWalletConnected ? 'subtle' : 'outline' }
colorScheme="gray"
boxSize="40px"
flexShrink={ 0 }
bg={ isWalletConnected ? themedBackground : undefined }
color={ themedColor }
borderColor={ !isWalletConnected ? themedBorderColor : undefined }
onClick={ isWalletConnected ? onOpen : connect }
isLoading={ isModalOpening || isModalOpen }
/>
</WalletTooltip>
{ isWalletConnected && (
<Drawer
isOpen={ isOpen }
placement="right"
onClose={ onClose }
autoFocus={ false }
>
<DrawerOverlay/>
<DrawerContent maxWidth="260px">
<DrawerBody p={ 6 }>
<WalletMenuContent address={ address } disconnect={ disconnect }/>
</DrawerBody>
</DrawerContent>
</Drawer>
) }
</>
);
};
export default WalletMenuMobile;
import { Tooltip, useBoolean } from '@chakra-ui/react';
import React from 'react';
type Props = {
children: React.ReactNode;
isDisabled?: boolean;
isMobile?: boolean;
};
const WalletTooltip = ({ children, isDisabled, isMobile }: Props) => {
const [ isTooltipShown, setIsTooltipShown ] = useBoolean(false);
React.useEffect(() => {
const key = `wallet-connect-tooltip-shown-${ isMobile ? 'mobile' : 'desktop' }`;
const wasShown = window.localStorage.getItem(key);
if (!wasShown) {
setIsTooltipShown.on();
window.localStorage.setItem(key, 'true');
}
}, [ setIsTooltipShown, isMobile ]);
return (
<Tooltip
label={ <span>Your wallet is used to interact with<br/>apps and contracts in the explorer</span> }
textAlign="center"
padding={ 2 }
isDisabled={ isDisabled }
openDelay={ 300 }
isOpen={ isTooltipShown || (isMobile ? false : undefined) }
onClose={ setIsTooltipShown.off }
display={ isMobile ? { base: 'flex', lg: 'none' } : { base: 'none', lg: 'flex' } }
>
{ children }
</Tooltip>
);
};
export default WalletTooltip;
import { useWeb3Modal } from '@web3modal/react';
import React from 'react';
import { useAccount, useDisconnect } from 'wagmi';
import * as mixpanel from 'lib/mixpanel/index';
export default function useWallet() {
const { open, isOpen } = useWeb3Modal();
const { disconnect } = useDisconnect();
const [ isModalOpening, setIsModalOpening ] = React.useState(false);
const [ isClientLoaded, setIsClientLoaded ] = React.useState(false);
React.useEffect(() => {
setIsClientLoaded(true);
}, []);
const handleConnect = React.useCallback(async() => {
setIsModalOpening(true);
await open();
setIsModalOpening(false);
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: 'Header', Status: 'Started' });
}, [ open ]);
const handleAccountConnected = React.useCallback(({ isReconnected }: { isReconnected: boolean }) => {
!isReconnected && mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: 'Header', Status: 'Connected' });
}, []);
const handleDisconnect = React.useCallback(() => {
disconnect();
}, [ disconnect ]);
const { address, isDisconnected } = useAccount({ onConnect: handleAccountConnected });
const isWalletConnected = isClientLoaded && !isDisconnected && address !== undefined;
return {
isWalletConnected,
address: address || '',
connect: handleConnect,
disconnect: handleDisconnect,
isModalOpening,
isModalOpen: isOpen,
};
}
...@@ -7,6 +7,8 @@ import { STATS_COUNTER } from 'stubs/stats'; ...@@ -7,6 +7,8 @@ import { STATS_COUNTER } from 'stubs/stats';
import DataFetchAlert from '../shared/DataFetchAlert'; import DataFetchAlert from '../shared/DataFetchAlert';
import NumberWidget from './NumberWidget'; import NumberWidget from './NumberWidget';
const UNITS_WITHOUT_SPACE = [ 's' ];
const NumberWidgetsList = () => { const NumberWidgetsList = () => {
const { data, isPlaceholderData, isError } = useApiQuery('stats_counters', { const { data, isPlaceholderData, isError } = useApiQuery('stats_counters', {
queryOptions: { queryOptions: {
...@@ -26,11 +28,18 @@ const NumberWidgetsList = () => { ...@@ -26,11 +28,18 @@ const NumberWidgetsList = () => {
{ {
data?.counters?.map(({ id, title, value, units, description }, index) => { data?.counters?.map(({ id, title, value, units, description }, index) => {
let unitsStr = '';
if (UNITS_WITHOUT_SPACE.includes(units)) {
unitsStr = units;
} else if (units) {
unitsStr = ' ' + units;
}
return ( return (
<NumberWidget <NumberWidget
key={ id + (isPlaceholderData ? index : '') } key={ id + (isPlaceholderData ? index : '') }
label={ title } label={ title }
value={ `${ Number(value).toLocaleString(undefined, { maximumFractionDigits: 3, notation: 'compact' }) } ${ units ? units : '' }` } value={ `${ Number(value).toLocaleString(undefined, { maximumFractionDigits: 3, notation: 'compact' }) }${ unitsStr }` }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
description={ description } description={ description }
/> />
......
import type { TokenType } from 'types/api/token'; import type { TokenType } from 'types/api/token';
import type { TokensSortingField, TokensSortingValue, TokensSorting } from 'types/api/tokens'; import type { TokensSortingValue } from 'types/api/tokens';
import type { Query } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
...@@ -29,22 +27,3 @@ const bridgedTokensChainIds = (() => { ...@@ -29,22 +27,3 @@ const bridgedTokensChainIds = (() => {
return feature.chains.map(chain => chain.id); return feature.chains.map(chain => chain.id);
})(); })();
export const getBridgedChainsFilterValue = (getFilterValuesFromQuery<string>).bind(null, bridgedTokensChainIds); export const getBridgedChainsFilterValue = (getFilterValuesFromQuery<string>).bind(null, bridgedTokensChainIds);
export const getSortValueFromQuery = (query: Query): TokensSortingValue | undefined => {
if (!query.sort || !query.order) {
return undefined;
}
const str = query.sort + '-' + query.order;
if (SORT_OPTIONS.map(option => option.id).includes(str)) {
return str as TokensSortingValue;
}
};
export const getSortParamsFromValue = (val?: TokensSortingValue): TokensSorting | undefined => {
if (!val) {
return undefined;
}
const sortingChunks = val.split('-') as [ TokensSortingField, TokensSorting['order'] ];
return { sort: sortingChunks[0], order: sortingChunks[1] };
};
...@@ -2,17 +2,23 @@ import { Box, Show, Hide } from '@chakra-ui/react'; ...@@ -2,17 +2,23 @@ import { Box, Show, Hide } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressFromToFilter } from 'types/api/address'; import type { AddressFromToFilter } from 'types/api/address';
import type { Transaction, TransactionsSortingField, TransactionsSortingValue } from 'types/api/transaction';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import AddressCsvExportLink from 'ui/address/AddressCsvExportLink'; import AddressCsvExportLink from 'ui/address/AddressCsvExportLink';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages'; import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import getNextSortValue from 'ui/shared/sort/getNextSortValue';
import TxsHeaderMobile from './TxsHeaderMobile'; import TxsHeaderMobile from './TxsHeaderMobile';
import TxsListItem from './TxsListItem'; import TxsListItem from './TxsListItem';
import TxsTable from './TxsTable'; import TxsTable from './TxsTable';
import useTxsSort from './useTxsSort';
const SORT_SEQUENCE: Record<TransactionsSortingField, Array<TransactionsSortingValue | undefined>> = {
value: [ 'value-desc', 'value-asc', undefined ],
fee: [ 'fee-desc', 'fee-asc', undefined ],
};
type Props = { type Props = {
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
...@@ -26,12 +32,17 @@ type Props = { ...@@ -26,12 +32,17 @@ type Props = {
filterValue?: AddressFromToFilter; filterValue?: AddressFromToFilter;
enableTimeIncrement?: boolean; enableTimeIncrement?: boolean;
top?: number; top?: number;
items?: Array<Transaction>;
isPlaceholderData: boolean;
isError: boolean;
setSorting: (value: TransactionsSortingValue | undefined) => void;
sort: TransactionsSortingValue | undefined;
} }
const TxsContent = ({ const TxsContent = ({
query,
filter, filter,
filterValue, filterValue,
query,
showBlockInfo = true, showBlockInfo = true,
showSocketInfo = true, showSocketInfo = true,
socketInfoAlert, socketInfoAlert,
...@@ -39,11 +50,20 @@ const TxsContent = ({ ...@@ -39,11 +50,20 @@ const TxsContent = ({
currentAddress, currentAddress,
enableTimeIncrement, enableTimeIncrement,
top, top,
items,
isPlaceholderData,
isError,
setSorting,
sort,
}: Props) => { }: Props) => {
const { data, isPlaceholderData, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const content = data?.items ? ( const onSortToggle = React.useCallback((field: TransactionsSortingField) => () => {
const value = getNextSortValue<TransactionsSortingField, TransactionsSortingValue>(SORT_SEQUENCE, field)(sort);
setSorting(value);
}, [ sort, setSorting ]);
const content = items ? (
<> <>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
<Box> <Box>
...@@ -55,7 +75,7 @@ const TxsContent = ({ ...@@ -55,7 +75,7 @@ const TxsContent = ({
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
/> />
) } ) }
{ data.items.map((tx, index) => ( { items.map((tx, index) => (
<TxsListItem <TxsListItem
key={ tx.hash + (isPlaceholderData ? index : '') } key={ tx.hash + (isPlaceholderData ? index : '') }
tx={ tx } tx={ tx }
...@@ -69,9 +89,9 @@ const TxsContent = ({ ...@@ -69,9 +89,9 @@ const TxsContent = ({
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<TxsTable <TxsTable
txs={ data.items } txs={ items }
sort={ setSortByField } sort={ onSortToggle }
sorting={ sorting } sorting={ sort }
showBlockInfo={ showBlockInfo } showBlockInfo={ showBlockInfo }
showSocketInfo={ showSocketInfo } showSocketInfo={ showSocketInfo }
socketInfoAlert={ socketInfoAlert } socketInfoAlert={ socketInfoAlert }
...@@ -88,8 +108,8 @@ const TxsContent = ({ ...@@ -88,8 +108,8 @@ const TxsContent = ({
const actionBar = isMobile ? ( const actionBar = isMobile ? (
<TxsHeaderMobile <TxsHeaderMobile
mt={ -6 } mt={ -6 }
sorting={ sorting } sorting={ sort }
setSorting={ setSortByValue } setSorting={ setSorting }
paginationProps={ query.pagination } paginationProps={ query.pagination }
showPagination={ query.pagination.isVisible } showPagination={ query.pagination.isVisible }
filterComponent={ filter } filterComponent={ filter }
...@@ -107,7 +127,7 @@ const TxsContent = ({ ...@@ -107,7 +127,7 @@ const TxsContent = ({
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
items={ data?.items } items={ items }
emptyText="There are no transactions." emptyText="There are no transactions."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
......
import { HStack, chakra } from '@chakra-ui/react'; import { HStack, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { Sort as TSort } from 'types/client/txs-sort'; import type { TransactionsSortingValue } from 'types/api/transaction';
import type { PaginationParams } from 'ui/shared/pagination/types'; import type { PaginationParams } from 'ui/shared/pagination/types';
// import FilterInput from 'ui/shared/filters/FilterInput'; // import FilterInput from 'ui/shared/filters/FilterInput';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import Pagination from 'ui/shared/pagination/Pagination'; import Pagination from 'ui/shared/pagination/Pagination';
import type { Option } from 'ui/shared/sort/Sort';
import Sort from 'ui/shared/sort/Sort'; import Sort from 'ui/shared/sort/Sort';
// import TxsFilters from './TxsFilters'; import { SORT_OPTIONS } from './useTxsSort';
const SORT_OPTIONS: Array<Option<TSort>> = [ // import TxsFilters from './TxsFilters';
{ title: 'Default', id: undefined },
{ title: 'Value ascending', id: 'val-asc' },
{ title: 'Value descending', id: 'val-desc' },
{ title: 'Fee ascending', id: 'fee-asc' },
{ title: 'Fee descending', id: 'fee-desc' },
];
type Props = { type Props = {
sorting: TSort; sorting: TransactionsSortingValue | undefined;
setSorting: (val: TSort | undefined) => void; setSorting: (val: TransactionsSortingValue | undefined) => void;
paginationProps: PaginationParams; paginationProps: PaginationParams;
className?: string; className?: string;
showPagination?: boolean; showPagination?: boolean;
......
...@@ -2,8 +2,7 @@ import { Link, Table, Tbody, Tr, Th, Icon, Show, Hide } from '@chakra-ui/react'; ...@@ -2,8 +2,7 @@ import { Link, Table, Tbody, Tr, Th, Icon, Show, Hide } from '@chakra-ui/react';
import { AnimatePresence } from 'framer-motion'; import { AnimatePresence } from 'framer-motion';
import React from 'react'; import React from 'react';
import type { Transaction } from 'types/api/transaction'; import type { Transaction, TransactionsSortingField, TransactionsSortingValue } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort';
import config from 'configs/app'; import config from 'configs/app';
import rightArrowIcon from 'icons/arrows/east.svg'; import rightArrowIcon from 'icons/arrows/east.svg';
...@@ -14,8 +13,8 @@ import TxsTableItem from './TxsTableItem'; ...@@ -14,8 +13,8 @@ import TxsTableItem from './TxsTableItem';
type Props = { type Props = {
txs: Array<Transaction>; txs: Array<Transaction>;
sort: (field: 'val' | 'fee') => () => void; sort: (field: TransactionsSortingField) => () => void;
sorting?: Sort; sorting?: TransactionsSortingValue;
top: number; top: number;
showBlockInfo: boolean; showBlockInfo: boolean;
showSocketInfo: boolean; showSocketInfo: boolean;
...@@ -58,9 +57,9 @@ const TxsTable = ({ ...@@ -58,9 +57,9 @@ const TxsTable = ({
</Th> </Th>
{ !config.UI.views.tx.hiddenFields?.value && ( { !config.UI.views.tx.hiddenFields?.value && (
<Th width="20%" isNumeric> <Th width="20%" isNumeric>
<Link onClick={ sort('val') } display="flex" justifyContent="end"> <Link onClick={ sort('value') } display="flex" justifyContent="end">
{ sorting === 'val-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> } { sorting === 'value-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
{ sorting === 'val-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> } { sorting === 'value-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> }
{ `Value ${ config.chain.currency.symbol }` } { `Value ${ config.chain.currency.symbol }` }
</Link> </Link>
</Th> </Th>
......
...@@ -2,7 +2,7 @@ import React from 'react'; ...@@ -2,7 +2,7 @@ import React from 'react';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken'; import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages'; import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
import TxsContent from 'ui/txs/TxsContent'; import TxsWithFrontendSorting from 'ui/txs/TxsWithFrontendSorting';
type Props = { type Props = {
query: QueryWithPagesResult<'txs_watchlist'>; query: QueryWithPagesResult<'txs_watchlist'>;
...@@ -10,7 +10,7 @@ type Props = { ...@@ -10,7 +10,7 @@ type Props = {
const TxsWatchlist = ({ query }: Props) => { const TxsWatchlist = ({ query }: Props) => {
useRedirectForInvalidAuthToken(); useRedirectForInvalidAuthToken();
return <TxsContent query={ query } showSocketInfo={ false }/>; return <TxsWithFrontendSorting query={ query } showSocketInfo={ false }/>;
}; };
export default TxsWatchlist; export default TxsWatchlist;
import React from 'react';
import type { AddressFromToFilter } from 'types/api/address';
import type { TransactionsSortingValue } from 'types/api/transaction';
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue';
import TxsContent from './TxsContent';
type Props = {
// eslint-disable-next-line max-len
query: QueryWithPagesResult<'address_txs'>;
showBlockInfo?: boolean;
showSocketInfo?: boolean;
socketInfoAlert?: string;
socketInfoNum?: number;
currentAddress?: string;
filter?: React.ReactNode;
filterValue?: AddressFromToFilter;
enableTimeIncrement?: boolean;
top?: number;
sorting: TransactionsSortingValue | undefined;
setSort: (value?: TransactionsSortingValue) => void;
}
const TxsWithAPISorting = ({
filter,
filterValue,
query,
showBlockInfo = true,
showSocketInfo = true,
socketInfoAlert,
socketInfoNum,
currentAddress,
enableTimeIncrement,
top,
sorting,
setSort,
}: Props) => {
const handleSortChange = React.useCallback((value?: TransactionsSortingValue) => {
setSort(value);
query.onSortingChange(getSortParamsFromValue(value));
}, [ setSort, query ]);
return (
<TxsContent
filter={ filter }
filterValue={ filterValue }
showBlockInfo={ showBlockInfo }
showSocketInfo={ showSocketInfo }
socketInfoAlert={ socketInfoAlert }
socketInfoNum={ socketInfoNum }
currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
top={ top }
items={ query.data?.items }
isPlaceholderData={ query.isPlaceholderData }
isError={ query.isError }
setSorting={ handleSortChange }
sort={ sorting }
query={ query }
/>
);
};
export default TxsWithAPISorting;
import React from 'react';
import type { AddressFromToFilter } from 'types/api/address';
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
import TxsContent from './TxsContent';
import useTxsSort from './useTxsSort';
type Props = {
// eslint-disable-next-line max-len
query: QueryWithPagesResult<'txs_validated' | 'txs_pending'> | QueryWithPagesResult<'txs_watchlist'> | QueryWithPagesResult<'block_txs'> | QueryWithPagesResult<'zkevm_l2_txn_batch_txs'>;
showBlockInfo?: boolean;
showSocketInfo?: boolean;
socketInfoAlert?: string;
socketInfoNum?: number;
currentAddress?: string;
filter?: React.ReactNode;
filterValue?: AddressFromToFilter;
enableTimeIncrement?: boolean;
top?: number;
}
const TxsWithFrontendSorting = ({
filter,
filterValue,
query,
showBlockInfo = true,
showSocketInfo = true,
socketInfoAlert,
socketInfoNum,
currentAddress,
enableTimeIncrement,
top,
}: Props) => {
const { data, isPlaceholderData, isError, setSortByValue, sorting } = useTxsSort(query);
return (
<TxsContent
filter={ filter }
filterValue={ filterValue }
showBlockInfo={ showBlockInfo }
showSocketInfo={ showSocketInfo }
socketInfoAlert={ socketInfoAlert }
socketInfoNum={ socketInfoNum }
currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
top={ top }
items={ data?.items }
isPlaceholderData={ isPlaceholderData }
isError={ isError }
setSorting={ setSortByValue }
sort={ sorting }
query={ query }
/>
);
};
export default TxsWithFrontendSorting;
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { TxsResponse } from 'types/api/transaction'; import type { Transaction, TransactionsSortingValue, TxsResponse } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import compareBns from 'lib/bigint/compareBns';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import sortTxs from 'lib/tx/sortTxs'; import type { Option } from 'ui/shared/sort/Sort';
export const SORT_OPTIONS: Array<Option<TransactionsSortingValue>> = [
{ title: 'Default', id: undefined },
{ title: 'Value ascending', id: 'value-asc' },
{ title: 'Value descending', id: 'value-desc' },
{ title: 'Fee ascending', id: 'fee-asc' },
{ title: 'Fee descending', id: 'fee-desc' },
];
type SortingValue = TransactionsSortingValue | undefined;
type HookResult = UseQueryResult<TxsResponse, ResourceError<unknown>> & { type HookResult = UseQueryResult<TxsResponse, ResourceError<unknown>> & {
sorting: Sort; sorting: SortingValue;
setSortByField: (field: 'val' | 'fee') => () => void; setSortByValue: (value: SortingValue) => void;
setSortByValue: (value: Sort | undefined) => void;
} }
const sortTxs = (sorting: SortingValue) => (tx1: Transaction, tx2: Transaction) => {
switch (sorting) {
case 'value-desc':
return compareBns(tx1.value, tx2.value);
case 'value-asc':
return compareBns(tx2.value, tx1.value);
case 'fee-desc':
return compareBns(tx1.fee.value || 0, tx2.fee.value || 0);
case 'fee-asc':
return compareBns(tx2.fee.value || 0, tx1.fee.value || 0);
default:
return 0;
}
};
export default function useTxsSort( export default function useTxsSort(
queryResult: UseQueryResult<TxsResponse, ResourceError<unknown>>, queryResult: UseQueryResult<TxsResponse, ResourceError<unknown>>,
): HookResult { ): HookResult {
const [ sorting, setSorting ] = React.useState<Sort>(cookies.get(cookies.NAMES.TXS_SORT) as Sort); const [ sorting, setSorting ] = React.useState<SortingValue>(cookies.get(cookies.NAMES.TXS_SORT) as SortingValue);
const setSortByField = React.useCallback((field: 'val' | 'fee') => () => {
if (queryResult.isPlaceholderData) {
return;
}
setSorting((prevVal) => {
let newVal: Sort = '';
if (field === 'val') {
if (prevVal === 'val-asc') {
newVal = '';
} else if (prevVal === 'val-desc') {
newVal = 'val-asc';
} else {
newVal = 'val-desc';
}
}
if (field === 'fee') {
if (prevVal === 'fee-asc') {
newVal = '';
} else if (prevVal === 'fee-desc') {
newVal = 'fee-asc';
} else {
newVal = 'fee-desc';
}
}
cookies.set(cookies.NAMES.TXS_SORT, newVal);
return newVal;
});
}, [ queryResult.isPlaceholderData ]);
const setSortByValue = React.useCallback((value: Sort | undefined) => { const setSortByValue = React.useCallback((value: SortingValue) => {
setSorting((prevVal: Sort) => { setSorting((prevVal: SortingValue) => {
let newVal: Sort = ''; let newVal: SortingValue = undefined;
if (value !== prevVal) { if (value !== prevVal) {
newVal = value as Sort; newVal = value as SortingValue;
} }
cookies.set(cookies.NAMES.TXS_SORT, newVal); cookies.set(cookies.NAMES.TXS_SORT, newVal ? newVal : '');
return newVal; return newVal;
}); });
}, []); }, []);
return React.useMemo(() => { return React.useMemo(() => {
if (queryResult.isError || queryResult.isPending) { if (queryResult.isError || queryResult.isPending) {
return { ...queryResult, setSortByField, setSortByValue, sorting }; return { ...queryResult, setSortByValue, sorting };
} }
return { return {
...queryResult, ...queryResult,
data: { ...queryResult.data, items: queryResult.data.items.slice().sort(sortTxs(sorting)) }, data: { ...queryResult.data, items: queryResult.data.items.slice().sort(sortTxs(sorting)) },
setSortByField,
setSortByValue, setSortByValue,
sorting, sorting,
}; };
}, [ queryResult, setSortByField, setSortByValue, sorting ]); }, [ queryResult, setSortByValue, sorting ]);
} }
...@@ -2,23 +2,30 @@ import { Table, Tbody, Tr, Th, Link, Icon } from '@chakra-ui/react'; ...@@ -2,23 +2,30 @@ import { Table, Tbody, Tr, Th, Link, Icon } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { VerifiedContract } from 'types/api/contracts'; import type { VerifiedContract } from 'types/api/contracts';
import type { VerifiedContractsSorting, VerifiedContractsSortingField, VerifiedContractsSortingValue } from 'types/api/verifiedContracts';
import config from 'configs/app'; import config from 'configs/app';
import arrowIcon from 'icons/arrows/east.svg'; import arrowIcon from 'icons/arrows/east.svg';
import getNextSortValue from 'ui/shared/sort/getNextSortValue';
import { default as Thead } from 'ui/shared/TheadSticky'; import { default as Thead } from 'ui/shared/TheadSticky';
import { SORT_SEQUENCE } from 'ui/verifiedContracts/utils';
import type { Sort, SortField } from './utils';
import VerifiedContractsTableItem from './VerifiedContractsTableItem'; import VerifiedContractsTableItem from './VerifiedContractsTableItem';
interface Props { interface Props {
data: Array<VerifiedContract>; data: Array<VerifiedContract>;
sort: Sort | undefined; sort: VerifiedContractsSortingValue | undefined;
onSortToggle: (field: SortField) => () => void; setSorting: (val: VerifiedContractsSortingValue | undefined) => void;
isLoading?: boolean; isLoading?: boolean;
} }
const VerifiedContractsTable = ({ data, sort, onSortToggle, isLoading }: Props) => { const VerifiedContractsTable = ({ data, sort, setSorting, isLoading }: Props) => {
const sortIconTransform = sort?.includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)'; const sortIconTransform = sort?.includes('asc' as VerifiedContractsSorting['order']) ? 'rotate(-90deg)' : 'rotate(90deg)';
const onSortToggle = React.useCallback((field: VerifiedContractsSortingField) => () => {
const value = getNextSortValue<VerifiedContractsSortingField, VerifiedContractsSortingValue>(SORT_SEQUENCE, field)(sort);
setSorting(value);
}, [ sort, setSorting ]);
return ( return (
<Table variant="simple" size="sm"> <Table variant="simple" size="sm">
...@@ -32,8 +39,8 @@ const VerifiedContractsTable = ({ data, sort, onSortToggle, isLoading }: Props) ...@@ -32,8 +39,8 @@ const VerifiedContractsTable = ({ data, sort, onSortToggle, isLoading }: Props)
</Link> </Link>
</Th> </Th>
<Th width="130px" isNumeric> <Th width="130px" isNumeric>
<Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ isLoading ? undefined : onSortToggle('txs') } columnGap={ 1 }> <Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ isLoading ? undefined : onSortToggle('txs_count') } columnGap={ 1 }>
{ sort?.includes('txs') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> } { sort?.includes('txs_count') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
Txs Txs
</Link> </Link>
</Th> </Th>
......
import type { VerifiedContract } from 'types/api/contracts'; import type { VerifiedContractsSortingValue, VerifiedContractsSortingField } from 'types/api/verifiedContracts';
import compareBns from 'lib/bigint/compareBns';
import { default as getNextSortValueShared } from 'ui/shared/sort/getNextSortValue';
import type { Option } from 'ui/shared/sort/Sort'; import type { Option } from 'ui/shared/sort/Sort';
export type SortField = 'balance' | 'txs'; export const SORT_OPTIONS: Array<Option<VerifiedContractsSortingValue>> = [
export type Sort = `${ SortField }-asc` | `${ SortField }-desc`;
export const SORT_OPTIONS: Array<Option<Sort>> = [
{ title: 'Default', id: undefined }, { title: 'Default', id: undefined },
{ title: 'Balance descending', id: 'balance-desc' }, { title: 'Balance descending', id: 'balance-desc' },
{ title: 'Balance ascending', id: 'balance-asc' }, { title: 'Balance ascending', id: 'balance-asc' },
{ title: 'Txs count descending', id: 'txs-desc' }, { title: 'Txs count descending', id: 'txs_count-desc' },
{ title: 'Txs count ascending', id: 'txs-asc' }, { title: 'Txs count ascending', id: 'txs_count-asc' },
]; ];
const SORT_SEQUENCE: Record<SortField, Array<Sort | undefined>> = { export const SORT_SEQUENCE: Record<VerifiedContractsSortingField, Array<VerifiedContractsSortingValue | undefined>> = {
balance: [ 'balance-desc', 'balance-asc', undefined ], balance: [ 'balance-desc', 'balance-asc', undefined ],
txs: [ 'txs-desc', 'txs-asc', undefined ], txs_count: [ 'txs_count-desc', 'txs_count-asc', undefined ],
};
export const getNextSortValue = (getNextSortValueShared<SortField, Sort>).bind(undefined, SORT_SEQUENCE);
export const sortFn = (sort: Sort | undefined) => (a: VerifiedContract, b: VerifiedContract) => {
switch (sort) {
case 'balance-asc':
case 'balance-desc': {
const result = compareBns(b.coin_balance, a.coin_balance) * (sort.includes('desc') ? 1 : -1);
return a.coin_balance === b.coin_balance ? 0 : result;
}
case 'txs-asc':
case 'txs-desc': {
const result = ((a.tx_count || 0) > (b.tx_count || 0) ? -1 : 1) * (sort.includes('desc') ? 1 : -1);
return a.tx_count === b.tx_count ? 0 : result;
}
default:
return 0;
}
}; };
...@@ -76,7 +76,7 @@ const WatchListItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) = ...@@ -76,7 +76,7 @@ const WatchListItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) =
}, },
onSuccess: () => { onSuccess: () => {
setSwitchDisabled(false); setSwitchDisabled(false);
showNotificationToast(!notificationEnabled); showNotificationToast(notificationEnabled);
}, },
}); });
......
This diff is collapsed.
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