Commit 4acf7a06 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into chakra-v3

parents 9b66ff8a fdfaa676
......@@ -9,7 +9,7 @@ on:
jobs:
cleanup_release:
uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup_helmfile.yaml@master
uses: blockscout/actions/.github/workflows/cleanup_helmfile.yaml@main
with:
appName: review-l2-$GITHUB_REF_NAME_SLUG
globalEnv: review
......@@ -18,7 +18,7 @@ jobs:
vaultRole: ci-dev
secrets: inherit
cleanup_l2_release:
uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup_helmfile.yaml@master
uses: blockscout/actions/.github/workflows/cleanup_helmfile.yaml@main
with:
appName: review-$GITHUB_REF_NAME_SLUG
globalEnv: review
......
......@@ -62,7 +62,7 @@ jobs:
deploy_review_l2:
name: Deploy frontend (L2)
needs: [ make_slug, publish_image ]
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy_helmfile.yaml@master
uses: blockscout/actions/.github/workflows/deploy_helmfile.yaml@main
with:
appName: review-l2-${{ needs.make_slug.outputs.REF_SLUG }}
globalEnv: review
......
......@@ -64,7 +64,7 @@ jobs:
deploy_review:
name: Deploy frontend
needs: [ make_slug, publish_image ]
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy_helmfile.yaml@master
uses: blockscout/actions/.github/workflows/deploy_helmfile.yaml@main
with:
appName: review-${{ needs.make_slug.outputs.REF_SLUG }}
globalEnv: review
......
......@@ -16,6 +16,7 @@
/public/assets/configs
/public/icons/sprite.svg
/public/icons/sprite.*.svg
/public/icons/registry.json
/public/icons/README.md
/public/static/og_image.png
/public/sitemap.xml
......
......@@ -49,7 +49,7 @@ RUN yarn --frozen-lockfile --network-timeout 100000
# ****** STAGE 2: Build *******
# *****************************
FROM node:22.11.0-alpine AS builder
RUN apk add --no-cache --upgrade libc6-compat bash
RUN apk add --no-cache --upgrade libc6-compat bash jq
# pass build args to env variables
ARG GIT_COMMIT_SHA
......
......@@ -8,8 +8,8 @@ helmDefaults:
recreatePods: false
repositories:
- name: blockscout-ci-cd
url: https://blockscout.github.io/blockscout-ci-cd
# - name: blockscout-ci-cd
# url: https://blockscout.github.io/blockscout-ci-cd
- name: blockscout
url: https://blockscout.github.io/helm-charts
- name: bedag
......@@ -65,4 +65,4 @@ releases:
values:
- values/review/values.yaml.gotmpl
- global:
env: "review"
\ No newline at end of file
env: "review"
#!/bin/bash
yarn icons build -i ./icons -o ./public/icons --optimize
icons_dir="./icons"
target_dir="./public/icons"
yarn icons build -i $icons_dir -o $target_dir --optimize
create_registry_file() {
# Create a temporary file to store the registry
local registry_file="$target_dir/registry.json"
# Start the JSON array
echo "[]" > "$registry_file"
# Detect OS and set appropriate stat command
get_file_size() {
local file="$1"
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
stat -f%z "$file"
else
# Linux and others
stat -c%s "$file"
fi
}
# Function to process each file
process_file() {
local file="$1"
local relative_path="${file#$icons_dir/}"
local file_size=$(get_file_size "$file")
# Create a temporary file with the new entry
jq --arg name "$relative_path" --arg size "$file_size" \
'. + [{"name": $name, "file_size": ($size|tonumber)}]' \
"$registry_file" > "${registry_file}.tmp"
# Move the temporary file back
mv "${registry_file}.tmp" "$registry_file"
}
# Find all SVG files and process them
find "$icons_dir" -type f -name "*.svg" | while read -r file; do
process_file "$file"
done
}
# Skip hash creation and renaming for playwright environment
if [ "$NEXT_PUBLIC_APP_ENV" != "pw" ]; then
# Generate hash from the sprite file
HASH=$(md5sum ./public/icons/sprite.svg | cut -d' ' -f1 | head -c 8)
HASH=$(md5sum $target_dir/sprite.svg | cut -d' ' -f1 | head -c 8)
# Remove old sprite files
rm -f ./public/icons/sprite.*.svg
rm -f $target_dir/sprite.*.svg
# Rename the new sprite file
mv ./public/icons/sprite.svg "./public/icons/sprite.${HASH}.svg"
mv $target_dir/sprite.svg "$target_dir/sprite.${HASH}.svg"
export NEXT_PUBLIC_ICON_SPRITE_HASH=${HASH}
# Skip registry creation in development environment
# just to make the dev build faster
# remove this condition if you want to create the registry file in development environment
if [ "$NEXT_PUBLIC_APP_ENV" != "development" ]; then
create_registry_file
fi
echo "SVG sprite created: sprite.${HASH}.svg"
else
echo "SVG sprite created: sprite.svg (hash skipped for playwright environment)"
......
......@@ -35,9 +35,6 @@ export_envs_from_preset() {
# If there is a preset, load the environment variables from the its file
export_envs_from_preset
# Generate OG image
node --no-warnings ./og_image_generator.js
# Download external assets
./download_assets.sh ./public/assets/configs
......@@ -61,6 +58,9 @@ else
fi
echo
# Generate OG image
node --no-warnings ./og_image_generator.js
# Create envs.js file with run-time environment variables for the client app
./make_envs_script.sh
......
......@@ -28,6 +28,7 @@ if (process.env.NEXT_PUBLIC_OG_IMAGE_URL) {
background: bannerConfig.background?.[0],
title_color: bannerConfig.text_color?.[0],
invert_logo: !process.env.NEXT_PUBLIC_NETWORK_LOGO_DARK,
app_url: process.env.NEXT_PUBLIC_APP_HOST,
};
console.log('⏳ Making request to OG image generator service...');
......
......@@ -73,7 +73,6 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/api/csrf': 'Regular page',
'/api/healthz': 'Regular page',
'/api/config': 'Regular page',
'/api/sprite': 'Regular page',
};
export default function getPageOgType(pathname: Route['pathname']) {
......
......@@ -76,7 +76,6 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/api/csrf': DEFAULT_TEMPLATE,
'/api/healthz': DEFAULT_TEMPLATE,
'/api/config': DEFAULT_TEMPLATE,
'/api/sprite': DEFAULT_TEMPLATE,
};
const TEMPLATE_MAP_ENHANCED: Partial<Record<Route['pathname'], string>> = {
......
......@@ -73,7 +73,6 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/api/csrf': '%network_name% node API CSRF token',
'/api/healthz': '%network_name% node API health check',
'/api/config': '%network_name% node API app config',
'/api/sprite': '%network_name% node API SVG sprite content',
};
const TEMPLATE_MAP_ENHANCED: Partial<Record<Route['pathname'], string>> = {
......
......@@ -71,7 +71,6 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/api/csrf': 'Node API: CSRF token',
'/api/healthz': 'Node API: Health check',
'/api/config': 'Node API: App config',
'/api/sprite': 'Node API: SVG sprite content',
};
export default function getPageType(pathname: Route['pathname']) {
......
......@@ -49,13 +49,13 @@ interface SocketMessageParamsGeneric<Event extends string | undefined, Payload e
export namespace SocketMessage {
export type NewBlock = SocketMessageParamsGeneric<'new_block', NewBlockSocketResponse>;
export type BlocksIndexStatus = SocketMessageParamsGeneric<'block_index_status', { finished: boolean; ratio: string }>;
export type InternalTxsIndexStatus = SocketMessageParamsGeneric<'internal_txs_index_status', { finished: boolean; ratio: string }>;
export type BlocksIndexStatus = SocketMessageParamsGeneric<'index_status', { finished: boolean; ratio: string }>;
export type InternalTxsIndexStatus = SocketMessageParamsGeneric<'index_status', { finished: boolean; ratio: string }>;
export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>;
export type TxRawTrace = SocketMessageParamsGeneric<'raw_trace', RawTracesResponse>;
export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>;
export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>;
export type NewOptimisticDeposits = SocketMessageParamsGeneric<'deposits', { deposits: number }>;
export type NewOptimisticDeposits = SocketMessageParamsGeneric<'new_optimism_deposits', { deposits: number }>;
export type NewArbitrumDeposits = SocketMessageParamsGeneric<'new_messages_to_rollup_amount', { new_messages_to_rollup_amount: number }>;
export type AddressBalance = SocketMessageParamsGeneric<'balance', { balance: string; block_number: number; exchange_rate: string }>;
export type AddressCurrentCoinBalance =
......
......@@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'react';
import { useSocket } from './context';
const CHANNEL_REGISTRY: Record<string, Channel> = {};
const CHANNEL_REGISTRY: Record<string, { channel: Channel; subscribers: number }> = {};
interface Params {
topic: string | undefined;
......@@ -53,11 +53,12 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on
let ch: Channel;
if (CHANNEL_REGISTRY[topic]) {
ch = CHANNEL_REGISTRY[topic];
ch = CHANNEL_REGISTRY[topic].channel;
CHANNEL_REGISTRY[topic].subscribers++;
onJoinRef.current?.(ch, '');
} else {
ch = socket.channel(topic);
CHANNEL_REGISTRY[topic] = ch;
CHANNEL_REGISTRY[topic] = { channel: ch, subscribers: 1 };
ch.join()
.receive('ok', (message) => onJoinRef.current?.(ch, message))
.receive('error', () => {
......@@ -68,8 +69,14 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on
setChannel(ch);
return () => {
ch.leave();
delete CHANNEL_REGISTRY[topic];
if (CHANNEL_REGISTRY[topic]) {
CHANNEL_REGISTRY[topic].subscribers > 0 && CHANNEL_REGISTRY[topic].subscribers--;
if (CHANNEL_REGISTRY[topic].subscribers === 0) {
ch.leave();
delete CHANNEL_REGISTRY[topic];
}
}
setChannel(undefined);
};
}, [ socket, topic, params, isDisabled, onSocketError ]);
......
......@@ -46,7 +46,7 @@ Promise<GetServerSidePropsResult<Props<Pathname>>> => {
const isTrackingDisabled = process.env.DISABLE_TRACKING === 'true';
if (!isTrackingDisabled && !config.app.isDev) {
if (!isTrackingDisabled) {
// log pageview
const hostname = req.headers.host;
const timestamp = new Date().toISOString();
......
......@@ -26,7 +26,6 @@ declare module "nextjs-routes" {
| StaticRoute<"/api/metrics">
| StaticRoute<"/api/monitoring/invalid-api-schema">
| StaticRoute<"/api/proxy">
| StaticRoute<"/api/sprite">
| StaticRoute<"/api-docs">
| DynamicRoute<"/apps/[id]", { "id": string }>
| StaticRoute<"/apps">
......
......@@ -4,19 +4,12 @@ import type { NextPageWithLayout } from 'nextjs/types';
import PageNextJs from 'nextjs/PageNextJs';
import { useRollbar } from 'lib/rollbar';
import AppError from 'ui/shared/AppError/AppError';
import LayoutError from 'ui/shared/layout/LayoutError';
const error = new Error('Not found', { cause: { status: 404 } });
const Page: NextPageWithLayout = () => {
const rollbar = useRollbar();
React.useEffect(() => {
rollbar?.error('Page not found');
}, [ rollbar ]);
return (
<PageNextJs pathname="/404">
<AppError error={ error }/>
......
import fs from 'fs';
import type { NextApiRequest, NextApiResponse } from 'next';
import path from 'path';
import config from 'configs/app';
const ROOT_DIR = './icons';
const NAME_PREFIX = ROOT_DIR.replace('./', '') + '/';
interface IconInfo {
name: string;
fileSize: number;
}
const getIconName = (filePath: string) => filePath.replace(NAME_PREFIX, '').replace('.svg', '');
function collectIconNames(dir: string) {
const files = fs.readdirSync(dir, { withFileTypes: true });
let icons: Array<IconInfo> = [];
files.forEach((file) => {
const filePath = path.join(dir, file.name);
const stats = fs.statSync(filePath);
file.name.endsWith('.svg') && icons.push({
name: getIconName(filePath),
fileSize: stats.size,
});
if (file.isDirectory()) {
icons = [ ...icons, ...collectIconNames(filePath) ];
}
});
return icons;
}
export default async function spriteHandler(req: NextApiRequest, res: NextApiResponse) {
if (!config.app.isDev) {
return res.status(404).json({ error: 'Not found' });
}
const icons = collectIconNames(ROOT_DIR);
res.status(200).json({
icons,
});
}
......@@ -24,12 +24,11 @@ import AccountHistoryFilter from './AddressAccountHistoryFilter';
const getFilterValue = (getFilterValueFromQuery<NovesHistoryFilterValue>).bind(null, NovesHistoryFilterValues);
type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
isQueryEnabled?: boolean;
};
const AddressAccountHistory = ({ scrollRef, shouldRender = true, isQueryEnabled = true }: Props) => {
const AddressAccountHistory = ({ shouldRender = true, isQueryEnabled = true }: Props) => {
const router = useRouter();
const isMounted = useIsMounted();
......@@ -40,7 +39,6 @@ const AddressAccountHistory = ({ scrollRef, shouldRender = true, isQueryEnabled
const { data, isError, pagination, isPlaceholderData } = useQueryWithPages({
resourceName: 'noves_address_history',
pathParams: { address: currentAddress },
scrollRef,
options: {
enabled: isQueryEnabled,
placeholderData: generateListStub<'noves_address_history'>(NOVES_TRANSLATE, 10, { hasNextPage: false, pageNumber: 1, pageSize: 10 }),
......
......@@ -27,12 +27,11 @@ import AddressBlocksValidatedTableItem from './blocksValidated/AddressBlocksVali
const OVERLOAD_COUNT = 75;
interface Props {
scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
isQueryEnabled?: boolean;
}
const AddressBlocksValidated = ({ scrollRef, shouldRender = true, isQueryEnabled = true }: Props) => {
const AddressBlocksValidated = ({ shouldRender = true, isQueryEnabled = true }: Props) => {
const [ socketAlert, setSocketAlert ] = React.useState('');
const [ newItemsCount, setNewItemsCount ] = React.useState(0);
......@@ -44,7 +43,6 @@ const AddressBlocksValidated = ({ scrollRef, shouldRender = true, isQueryEnabled
const query = useQueryWithPages({
resourceName: 'address_blocks_validated',
pathParams: { hash: addressHash },
scrollRef,
options: {
enabled: isQueryEnabled,
placeholderData: generateListStub<'address_blocks_validated'>(
......
......@@ -31,10 +31,9 @@ import type { AddressQuery } from './utils/useAddressQuery';
interface Props {
addressQuery: AddressQuery;
scrollRef?: React.RefObject<HTMLDivElement>;
}
const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
const AddressDetails = ({ addressQuery }: Props) => {
const router = useRouter();
const addressHash = getQueryParamString(router.query.hash);
......@@ -44,13 +43,6 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
addressQuery,
});
const handleCounterItemClick = React.useCallback(() => {
window.setTimeout(() => {
// cannot do scroll instantly, have to wait a little
scrollRef?.current?.scrollIntoView({ behavior: 'smooth' });
}, 500);
}, [ scrollRef ]);
const error404Data = React.useMemo(() => ({
hash: addressHash || '',
is_contract: false,
......@@ -180,7 +172,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
Tokens
</DetailedInfo.ItemLabel>
<DetailedInfo.ItemValue py={ addressQuery.data ? 0 : undefined }>
{ addressQuery.data ? <TokenSelect onClick={ handleCounterItemClick }/> : <Box>0</Box> }
{ addressQuery.data ? <TokenSelect/> : <Box>0</Box> }
</DetailedInfo.ItemValue>
</>
) }
......@@ -211,7 +203,6 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
prop="transactions_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
isDegradedData={ addressQuery.isDegradedData }
/>
......@@ -233,7 +224,6 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
prop="token_transfers_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
isDegradedData={ addressQuery.isDegradedData }
/>
......@@ -257,7 +247,6 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
prop="gas_usage_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
isDegradedData={ addressQuery.isDegradedData }
/>
......@@ -287,7 +276,6 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
prop="validations_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
isDegradedData={ addressQuery.isDegradedData }
/>
......
......@@ -16,12 +16,11 @@ import AddressCsvExportLink from './AddressCsvExportLink';
import AddressEpochRewardsListItem from './epochRewards/AddressEpochRewardsListItem';
type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
isQueryEnabled?: boolean;
};
const AddressEpochRewards = ({ scrollRef, shouldRender = true, isQueryEnabled = true }: Props) => {
const AddressEpochRewards = ({ shouldRender = true, isQueryEnabled = true }: Props) => {
const router = useRouter();
const isMounted = useIsMounted();
......@@ -32,7 +31,6 @@ const AddressEpochRewards = ({ scrollRef, shouldRender = true, isQueryEnabled =
pathParams: {
hash,
},
scrollRef,
options: {
enabled: isQueryEnabled && Boolean(hash),
placeholderData: generateListStub<'address_epoch_rewards'>(EPOCH_REWARD_ITEM, 50, { next_page_params: {
......
......@@ -24,11 +24,10 @@ import AddressTxsFilter from './AddressTxsFilter';
const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
isQueryEnabled?: boolean;
};
const AddressInternalTxs = ({ scrollRef, shouldRender = true, isQueryEnabled = true }: Props) => {
const AddressInternalTxs = ({ shouldRender = true, isQueryEnabled = true }: Props) => {
const router = useRouter();
const isMounted = useIsMounted();
......@@ -40,7 +39,6 @@ const AddressInternalTxs = ({ scrollRef, shouldRender = true, isQueryEnabled = t
resourceName: 'address_internal_txs',
pathParams: { hash },
filters: { filter: filterValue },
scrollRef,
options: {
enabled: isQueryEnabled,
placeholderData: generateListStub<'address_internal_txs'>(
......
......@@ -15,12 +15,11 @@ import AddressCsvExportLink from './AddressCsvExportLink';
import useAddressQuery from './utils/useAddressQuery';
type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
isQueryEnabled?: boolean;
};
const AddressLogs = ({ scrollRef, shouldRender = true, isQueryEnabled = true }: Props) => {
const AddressLogs = ({ shouldRender = true, isQueryEnabled = true }: Props) => {
const router = useRouter();
const isMounted = useIsMounted();
......@@ -28,7 +27,6 @@ const AddressLogs = ({ scrollRef, shouldRender = true, isQueryEnabled = true }:
const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({
resourceName: 'address_logs',
pathParams: { hash },
scrollRef,
options: {
enabled: isQueryEnabled,
placeholderData: generateListStub<'address_logs'>(LOG, 3, { next_page_params: {
......
......@@ -8,12 +8,11 @@ import AddressMudTable from './mud/AddressMudTable';
import AddressMudTables from './mud/AddressMudTables';
type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
isQueryEnabled?: boolean;
};
const AddressMud = ({ scrollRef, shouldRender = true, isQueryEnabled = true }: Props) => {
const AddressMud = ({ shouldRender = true, isQueryEnabled = true }: Props) => {
const isMounted = useIsMounted();
const router = useRouter();
const tableId = router.query.table_id?.toString();
......@@ -24,14 +23,14 @@ const AddressMud = ({ scrollRef, shouldRender = true, isQueryEnabled = true }: P
}
if (tableId && recordId) {
return <AddressMudRecord tableId={ tableId } recordId={ recordId } isQueryEnabled={ isQueryEnabled } scrollRef={ scrollRef }/>;
return <AddressMudRecord tableId={ tableId } recordId={ recordId } isQueryEnabled={ isQueryEnabled }/>;
}
if (tableId) {
return <AddressMudTable tableId={ tableId } scrollRef={ scrollRef } isQueryEnabled={ isQueryEnabled }/>;
return <AddressMudTable tableId={ tableId } isQueryEnabled={ isQueryEnabled }/>;
}
return <AddressMudTables scrollRef={ scrollRef } isQueryEnabled={ isQueryEnabled }/>;
return <AddressMudTables isQueryEnabled={ isQueryEnabled }/>;
};
export default AddressMud;
......@@ -63,14 +63,13 @@ const matchFilters = (filters: Filters, tokenTransfer: TokenTransfer, address?:
};
type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
isQueryEnabled?: boolean;
// for tests only
overloadCount?: number;
};
const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT, shouldRender = true, isQueryEnabled = true }: Props) => {
const AddressTokenTransfers = ({ overloadCount = OVERLOAD_COUNT, shouldRender = true, isQueryEnabled = true }: Props) => {
const router = useRouter();
const queryClient = useQueryClient();
const isMobile = useIsMobile();
......@@ -94,7 +93,6 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT, shou
resourceName: 'address_token_transfers',
pathParams: { hash: currentAddress },
filters: tokenFilter ? { token: tokenFilter } : filters,
scrollRef,
options: {
enabled: isQueryEnabled,
placeholderData: getTokenTransfersStub(undefined, {
......
......@@ -47,14 +47,13 @@ const matchFilter = (filterValue: AddressFromToFilter, transaction: Transaction,
};
type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
isQueryEnabled?: boolean;
// for tests only
overloadCount?: number;
};
const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT, shouldRender = true, isQueryEnabled = true }: Props) => {
const AddressTxs = ({ overloadCount = OVERLOAD_COUNT, shouldRender = true, isQueryEnabled = true }: Props) => {
const router = useRouter();
const queryClient = useQueryClient();
const isMounted = useIsMounted();
......@@ -74,7 +73,6 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT, shouldRender =
pathParams: { hash: currentAddress },
filters: { filter: filterValue },
sorting: getSortParamsFromValue<TransactionsSortingValue, TransactionsSortingField, TransactionsSorting['order']>(sort),
scrollRef,
options: {
enabled: isQueryEnabled,
placeholderData: generateListStub<'address_txs'>(TX, 50, { next_page_params: {
......@@ -186,7 +184,7 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT, shouldRender =
return (
<>
{ !isMobile && (
{ !isMobile && addressTxsQuery.pagination.isVisible && (
<ActionBar>
{ filter }
{ currentAddress && csvExportLink }
......
......@@ -14,11 +14,10 @@ import BeaconChainWithdrawalsListItem from 'ui/withdrawals/beaconChain/BeaconCha
import BeaconChainWithdrawalsTable from 'ui/withdrawals/beaconChain/BeaconChainWithdrawalsTable';
type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
isQueryEnabled?: boolean;
};
const AddressWithdrawals = ({ scrollRef, shouldRender = true, isQueryEnabled = true }: Props) => {
const AddressWithdrawals = ({ shouldRender = true, isQueryEnabled = true }: Props) => {
const router = useRouter();
const isMounted = useIsMounted();
......@@ -27,7 +26,6 @@ const AddressWithdrawals = ({ scrollRef, shouldRender = true, isQueryEnabled = t
const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({
resourceName: 'address_withdrawals',
pathParams: { hash },
scrollRef,
options: {
enabled: isQueryEnabled,
placeholderData: generateListStub<'address_withdrawals'>(WITHDRAWAL, 50, { next_page_params: {
......
......@@ -40,7 +40,8 @@ test.describe('full view', () => {
},
};
const component = await render(<ContractDetails/>, { hooksConfig }, { withSocket: true });
await createSocket();
const socket = await createSocket();
await socketServer.joinChannel(socket, `addresses:${ addressMock.contract.hash.toLowerCase() }`);
await expect(component).toHaveScreenshot();
});
......@@ -51,7 +52,8 @@ test.describe('full view', () => {
},
};
const component = await render(<ContractDetails/>, { hooksConfig }, { withSocket: true });
await createSocket();
const socket = await createSocket();
await socketServer.joinChannel(socket, `addresses:${ addressMock.contract.hash.toLowerCase() }`);
await expect(component).toHaveScreenshot();
});
......@@ -62,7 +64,8 @@ test.describe('full view', () => {
},
};
const component = await render(<ContractDetails/>, { hooksConfig }, { withSocket: true });
await createSocket();
const socket = await createSocket();
await socketServer.joinChannel(socket, `addresses:${ addressMock.contract.hash.toLowerCase() }`);
await expect(component).toHaveScreenshot();
});
......@@ -73,7 +76,8 @@ test.describe('full view', () => {
},
};
const component = await render(<ContractDetails/>, { hooksConfig }, { withSocket: true });
await createSocket();
const socket = await createSocket();
await socketServer.joinChannel(socket, `addresses:${ addressMock.contract.hash.toLowerCase() }`);
await expect(component).toHaveScreenshot();
});
});
......@@ -85,7 +89,8 @@ test.describe('mobile view', () => {
await mockApiResponse('contract', contractMock.withChangedByteCode, { pathParams: { hash: addressMock.contract.hash } });
await mockApiResponse('contract', contractMock.withChangedByteCode, { pathParams: { hash: addressMock.contract.implementations?.[0].address as string } });
const component = await render(<ContractDetails/>, { hooksConfig }, { withSocket: true });
await createSocket();
const socket = await createSocket();
await socketServer.joinChannel(socket, `addresses:${ addressMock.contract.hash.toLowerCase() }`);
await expect(component).toHaveScreenshot();
});
});
......@@ -95,7 +100,7 @@ test('verified via lookup in eth_bytecode_db', async({ render, mockApiResponse,
await render(<ContractDetails/>, { hooksConfig }, { withSocket: true });
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());
const channel = await socketServer.joinChannel(socket, `addresses:${ addressMock.contract.hash.toLowerCase() }`);
await page.waitForResponse(contractApiUrl);
socketServer.sendMessage(socket, channel, 'smart_contract_was_verified', {});
const request = await page.waitForRequest(addressApiUrl);
......@@ -103,9 +108,11 @@ test('verified via lookup in eth_bytecode_db', async({ render, mockApiResponse,
expect(request).toBeTruthy();
});
test('verified with multiple sources', async({ render, page, mockApiResponse }) => {
test('verified with multiple sources', async({ render, page, mockApiResponse, createSocket }) => {
await mockApiResponse('contract', contractMock.withMultiplePaths, { pathParams: { hash: addressMock.contract.hash } });
await render(<ContractDetails/>, { hooksConfig }, { withSocket: true });
const socket = await createSocket();
await socketServer.joinChannel(socket, `addresses:${ addressMock.contract.hash.toLowerCase() }`);
const section = page.locator('section', { hasText: 'Contract source code' });
await expect(section).toHaveScreenshot();
......@@ -117,7 +124,7 @@ test('verified with multiple sources', async({ render, page, mockApiResponse })
await expect(section).toHaveScreenshot();
});
test('self destructed', async({ render, mockApiResponse, page }) => {
test('self destructed', async({ render, mockApiResponse, page, createSocket }) => {
const hooksConfig = {
router: {
query: { hash: addressMock.contract.hash, tab: 'contract_bytecode' },
......@@ -125,15 +132,19 @@ test('self destructed', async({ render, mockApiResponse, page }) => {
};
await mockApiResponse('contract', contractMock.selfDestructed, { pathParams: { hash: addressMock.contract.hash } });
await render(<ContractDetails/>, { hooksConfig }, { withSocket: true });
const socket = await createSocket();
await socketServer.joinChannel(socket, `addresses:${ addressMock.contract.hash.toLowerCase() }`);
const section = page.locator('section', { hasText: 'Contract creation code' });
await expect(section).toHaveScreenshot();
});
test('non verified', async({ render, mockApiResponse }) => {
test('non verified', async({ render, mockApiResponse, createSocket }) => {
await mockApiResponse('address', { ...addressMock.contract, name: null }, { pathParams: { hash: addressMock.contract.hash } });
await mockApiResponse('contract', contractMock.nonVerified, { pathParams: { hash: addressMock.contract.hash } });
const component = await render(<ContractDetails/>, { hooksConfig }, { withSocket: true });
const socket = await createSocket();
await socketServer.joinChannel(socket, `addresses:${ addressMock.contract.hash.toLowerCase() }`);
await expect(component).toHaveScreenshot();
});
......@@ -57,7 +57,7 @@ const ContractDetails = ({ addressHash, channel, mainContractQuery }: Props) =>
const contractQuery = useApiQuery('contract', {
pathParams: { hash: selectedItem?.address },
queryOptions: {
enabled: Boolean(selectedItem?.address),
enabled: Boolean(selectedItem?.address && !mainContractQuery.isPlaceholderData),
refetchOnMount: false,
placeholderData: addressInfo?.is_verified ? stubs.CONTRACT_CODE_VERIFIED : stubs.CONTRACT_CODE_UNVERIFIED,
},
......
......@@ -14,7 +14,6 @@ interface Props {
prop: keyof AddressCounters;
query: UseQueryResult<AddressCounters, ResourceError<unknown>>;
address: string;
onClick: () => void;
isAddressQueryLoading: boolean;
isDegradedData: boolean;
}
......@@ -25,7 +24,12 @@ const PROP_TO_TAB = {
validations_count: 'blocks_validated',
};
const AddressCounterItem = ({ prop, query, address, onClick, isAddressQueryLoading, isDegradedData }: Props) => {
const AddressCounterItem = ({ prop, query, address, isAddressQueryLoading, isDegradedData }: Props) => {
const handleClick = React.useCallback(() => {
window.scrollTo({ top: 0, behavior: 'smooth' });
}, []);
if (query.isPlaceholderData || isAddressQueryLoading) {
return <Skeleton loading h={ 5 } w="80px" borderRadius="full"/>;
}
......@@ -53,8 +57,8 @@ const AddressCounterItem = ({ prop, query, address, onClick, isAddressQueryLoadi
return (
<Link
href={ route({ pathname: '/address/[hash]', query: { hash: address, tab: PROP_TO_TAB[prop] } }) }
onClick={ onClick }
scroll={ false }
onClick={ handleClick }
>
{ Number(data).toLocaleString() }
</Link>
......
......@@ -12,7 +12,6 @@ import IconSvg from 'ui/shared/IconSvg';
import useAddressQuery from '../utils/useAddressQuery';
type TableViewProps = {
scrollRef?: React.RefObject<HTMLDivElement>;
className?: string;
hash: string;
tableId: string;
......@@ -27,21 +26,17 @@ type RecordViewProps = Omit<TableViewProps, 'recordId' | 'recordName'> & {
};
type BreadcrumbItemProps = {
scrollRef?: React.RefObject<HTMLDivElement>;
text: string;
href: string;
isLast?: boolean;
};
const BreadcrumbItem = ({ text, href, isLast, scrollRef }: BreadcrumbItemProps) => {
const BreadcrumbItem = ({ text, href, isLast }: BreadcrumbItemProps) => {
const currentUrl = isBrowser() ? window.location.href : '';
const onLinkClick = React.useCallback(() => {
window.setTimeout(() => {
// cannot do scroll instantly, have to wait a little
scrollRef?.current?.scrollIntoView({ behavior: 'smooth' });
}, 500);
}, [ scrollRef ]);
window.scrollTo({ top: 0, behavior: 'smooth' });
}, []);
if (isLast) {
return (
......@@ -95,20 +90,17 @@ const AddressMudBreadcrumbs = (props: TableViewProps | RecordViewProps) => {
<BreadcrumbItem
text="MUD World"
href={ route({ pathname: '/address/[hash]', query: queryParams }) }
scrollRef={ props.scrollRef }
/>
<BreadcrumbItem
text={ props.tableName }
href={ route({ pathname: '/address/[hash]', query: { ...queryParams, table_id: props.tableId } }) }
isLast={ !('recordId' in props) }
scrollRef={ props.scrollRef }
/>
{ ('recordId' in props && typeof props.recordId === 'string') && ('recordName' in props && typeof props.recordName === 'string') && (
<BreadcrumbItem
text={ props.recordName }
href={ route({ pathname: '/address/[hash]', query: { ...queryParams, table_id: props.tableId, record_id: props.recordId } }) }
isLast
scrollRef={ props.scrollRef }
/>
) }
......
......@@ -15,13 +15,12 @@ import AddressMudRecordValues from './AddressMudRecordValues';
import { getValueString } from './utils';
type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
isQueryEnabled?: boolean;
tableId: string;
recordId: string;
};
const AddressMudRecord = ({ tableId, recordId, isQueryEnabled = true, scrollRef }: Props) => {
const AddressMudRecord = ({ tableId, recordId, isQueryEnabled = true }: Props) => {
const router = useRouter();
const hash = getQueryParamString(router.query.hash);
......@@ -51,7 +50,6 @@ const AddressMudRecord = ({ tableId, recordId, isQueryEnabled = true, scrollRef
recordId={ recordId }
recordName={ data.record.id }
mb={ 6 }
scrollRef={ scrollRef }
/>
) }
<Box hideBelow="lg">
......
......@@ -24,14 +24,13 @@ const BREADCRUMBS_HEIGHT = 60;
const FILTERS_HEIGHT = 44;
type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
isQueryEnabled?: boolean;
tableId: string;
};
type FilterKeys = keyof AddressMudRecordsFilter;
const AddressMudTable = ({ scrollRef, tableId, isQueryEnabled = true }: Props) => {
const AddressMudTable = ({ tableId, isQueryEnabled = true }: Props) => {
const router = useRouter();
const [ sorting, setSorting ] =
React.useState<AddressMudRecordsSorting | undefined>(getSortParamsFromQuery<AddressMudRecordsSorting>(router.query, SORT_SEQUENCE));
......@@ -46,7 +45,6 @@ const AddressMudTable = ({ scrollRef, tableId, isQueryEnabled = true }: Props) =
pathParams: { hash, table_id: tableId },
filters,
sorting,
scrollRef,
options: {
// no placeholder data because the structure of a table is unpredictable
enabled: isQueryEnabled,
......@@ -108,7 +106,6 @@ const AddressMudTable = ({ scrollRef, tableId, isQueryEnabled = true }: Props) =
hash={ hash }
tableId={ tableId }
tableName={ data?.table.table_full_name }
scrollRef={ scrollRef }
mb={ hasActiveFilters ? 4 : 0 }
/>
) : null;
......@@ -132,7 +129,6 @@ const AddressMudTable = ({ scrollRef, tableId, isQueryEnabled = true }: Props) =
setFilters={ setFilters }
filters={ filters }
toggleTableHasHorizontalScroll={ handleTableHasHorizontalScroll }
scrollRef={ scrollRef }
hash={ hash }
/>
) : null;
......
......@@ -18,11 +18,10 @@ import AddressMudTablesListItem from './AddressMudTablesListItem';
import AddressMudTablesTable from './AddressMudTablesTable';
type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
isQueryEnabled?: boolean;
};
const AddressMudTables = ({ scrollRef, isQueryEnabled = true }: Props) => {
const AddressMudTables = ({ isQueryEnabled = true }: Props) => {
const router = useRouter();
const hash = getQueryParamString(router.query.hash);
......@@ -34,7 +33,6 @@ const AddressMudTables = ({ scrollRef, isQueryEnabled = true }: Props) => {
resourceName: 'address_mud_tables',
pathParams: { hash },
filters: { q: debouncedSearchTerm },
scrollRef,
options: {
enabled: isQueryEnabled,
placeholderData: generateListStub<'address_mud_tables'>(ADDRESS_MUD_TABLE_ITEM, 3, { next_page_params: {
......@@ -72,7 +70,6 @@ const AddressMudTables = ({ scrollRef, isQueryEnabled = true }: Props) => {
items={ data.items }
isLoading={ isPlaceholderData }
top={ ACTION_BAR_HEIGHT_DESKTOP }
scrollRef={ scrollRef }
hash={ hash }
/>
</Box>
......
......@@ -10,12 +10,11 @@ type Props = {
items: AddressMudTables['items'];
isLoading: boolean;
top: number;
scrollRef?: React.RefObject<HTMLDivElement>;
hash: string;
};
//sorry for the naming
const AddressMudTablesTable = ({ items, isLoading, top, scrollRef, hash }: Props) => {
const AddressMudTablesTable = ({ items, isLoading, top, hash }: Props) => {
return (
<TableRoot style={{ tableLayout: 'auto' }}>
<TableHeaderSticky top={ top }>
......@@ -32,7 +31,6 @@ const AddressMudTablesTable = ({ items, isLoading, top, scrollRef, hash }: Props
key={ item.table.table_id + (isLoading ? String(index) : '') }
item={ item }
isLoading={ isLoading }
scrollRef={ scrollRef }
hash={ hash }
/>
)) }
......
......@@ -11,15 +11,13 @@ import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { TableBody, TableCell, TableRoot, TableRow } from 'toolkit/chakra/table';
import IconSvg from 'ui/shared/IconSvg';
type Props = {
item: AddressMudTableItem;
isLoading: boolean;
scrollRef?: React.RefObject<HTMLDivElement>;
hash: string;
};
const AddressMudTablesTableItem = ({ item, isLoading, scrollRef, hash }: Props) => {
const AddressMudTablesTableItem = ({ item, isLoading, hash }: Props) => {
const [ isOpened, setIsOpened ] = React.useState(false);
const router = useRouter();
......@@ -44,8 +42,9 @@ const AddressMudTablesTableItem = ({ item, isLoading, scrollRef, hash }: Props)
{ shallow: true },
);
}
scrollRef?.current?.scrollIntoView();
}, [ router, scrollRef, hash ]);
window.scrollTo({ top: 0, behavior: 'smooth' });
}, [ router, hash ]);
return (
<>
......
......@@ -22,11 +22,7 @@ import useFetchTokens from '../utils/useFetchTokens';
import TokenSelectDesktop from './TokenSelectDesktop';
import TokenSelectMobile from './TokenSelectMobile';
interface Props {
onClick?: () => void;
}
const TokenSelect = ({ onClick }: Props) => {
const TokenSelect = () => {
const router = useRouter();
const isMobile = useIsMobile();
const queryClient = useQueryClient();
......@@ -42,8 +38,8 @@ const TokenSelect = ({ onClick }: Props) => {
const handleIconButtonClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Tokens show all (icon)' });
onClick?.();
}, [ onClick ]);
window.scrollTo({ top: 0, behavior: 'smooth' });
}, [ ]);
if (isPending) {
return (
......
......@@ -20,7 +20,7 @@ const TokenSelectItem = ({ data }: Props) => {
const secondRow = (() => {
switch (data.token.type) {
case 'ERC-20': {
const tokenDecimals = Number(data.token.decimals) || 18;
const tokenDecimals = Number(data.token.decimals ?? 18);
const text = `${ BigNumber(data.value).dividedBy(10 ** tokenDecimals).dp(8).toFormat() } ${ data.token.symbol || '' }`;
return (
......
......@@ -37,7 +37,7 @@ const LatestOptimisticDeposits = () => {
}, [ setNum ]);
const channel = useSocketChannel({
topic: 'optimism_deposits:new_deposits',
topic: 'optimism:new_deposits',
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: false,
......@@ -45,7 +45,7 @@ const LatestOptimisticDeposits = () => {
useSocketMessage({
channel,
event: 'deposits',
event: 'new_optimism_deposits',
handler: handleNewDepositMessage,
});
......
......@@ -67,7 +67,6 @@ const AddressPageContent = () => {
const router = useRouter();
const appProps = useAppContext();
const tabsScrollRef = React.useRef<HTMLDivElement>(null);
const hash = getQueryParamString(router.query.hash);
const checkDomainName = useCheckDomainNameParam(hash);
......@@ -153,23 +152,53 @@ const AddressPageContent = () => {
const tabs: Array<TabItemRegular> = React.useMemo(() => {
return [
{
id: 'index',
title: 'Details',
component: <AddressDetails addressQuery={ addressQuery }/>,
},
addressQuery.data?.is_contract ? {
id: 'contract',
title: () => {
const tabName = addressQuery.data.proxy_type === 'eip7702' ? 'Code' : 'Contract';
if (addressQuery.data.is_verified) {
return (
<>
<span>{ tabName }</span>
<IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 }/>
</>
);
}
return tabName;
},
component: (
<AddressContract
tabs={ contractTabs.tabs }
shouldRender={ !isTabsLoading }
isLoading={ contractTabs.isLoading }
/>
),
subTabs: CONTRACT_TAB_IDS,
} : undefined,
config.features.mudFramework.isEnabled && mudTablesCountQuery.data && mudTablesCountQuery.data > 0 && {
id: 'mud',
title: 'MUD',
count: mudTablesCountQuery.data,
component: <AddressMud scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
component: <AddressMud shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
},
{
id: 'txs',
title: 'Transactions',
count: addressTabsCountersQuery.data?.transactions_count,
component: <AddressTxs scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
component: <AddressTxs shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
},
txInterpretation.isEnabled && txInterpretation.provider === 'noves' ?
{
id: 'account_history',
title: 'Account history',
component: <AddressAccountHistory scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
component: <AddressAccountHistory shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
} :
undefined,
config.features.userOps.isEnabled && Boolean(userOpsAccountQuery.data?.total_ops) ?
......@@ -185,14 +214,14 @@ const AddressPageContent = () => {
id: 'withdrawals',
title: 'Withdrawals',
count: addressTabsCountersQuery.data?.withdrawals_count,
component: <AddressWithdrawals scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
component: <AddressWithdrawals shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
} :
undefined,
{
id: 'token_transfers',
title: 'Token transfers',
count: addressTabsCountersQuery.data?.token_transfers_count,
component: <AddressTokenTransfers scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
component: <AddressTokenTransfers shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
},
{
id: 'tokens',
......@@ -205,13 +234,13 @@ const AddressPageContent = () => {
id: 'internal_txns',
title: 'Internal txns',
count: addressTabsCountersQuery.data?.internal_transactions_count,
component: <AddressInternalTxs scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
component: <AddressInternalTxs shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
},
addressTabsCountersQuery.data?.celo_election_rewards_count ? {
id: 'epoch_rewards',
title: 'Epoch rewards',
count: addressTabsCountersQuery.data?.celo_election_rewards_count,
component: <AddressEpochRewards scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
component: <AddressEpochRewards shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
} : undefined,
{
id: 'coin_balance_history',
......@@ -223,7 +252,7 @@ const AddressPageContent = () => {
id: 'blocks_validated',
title: `Blocks ${ getNetworkValidationActionText() }`,
count: addressTabsCountersQuery.data?.validations_count,
component: <AddressBlocksValidated scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
component: <AddressBlocksValidated shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
} :
undefined,
addressTabsCountersQuery.data?.logs_count ?
......@@ -231,38 +260,12 @@ const AddressPageContent = () => {
id: 'logs',
title: 'Logs',
count: addressTabsCountersQuery.data?.logs_count,
component: <AddressLogs scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
component: <AddressLogs shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
} :
undefined,
addressQuery.data?.is_contract ? {
id: 'contract',
title: () => {
const tabName = addressQuery.data.proxy_type === 'eip7702' ? 'Code' : 'Contract';
if (addressQuery.data.is_verified) {
return (
<>
<span>{ tabName }</span>
<IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 }/>
</>
);
}
return tabName;
},
component: (
<AddressContract
tabs={ contractTabs.tabs }
shouldRender={ !isTabsLoading }
isLoading={ contractTabs.isLoading }
/>
),
subTabs: CONTRACT_TAB_IDS,
} : undefined,
].filter(Boolean);
}, [
addressQuery.data,
addressQuery,
contractTabs,
addressTabsCountersQuery.data,
userOpsAccountQuery.data,
......@@ -347,10 +350,6 @@ const AddressPageContent = () => {
/>
);
const content = (addressQuery.isError || addressQuery.isDegradedData) ?
null :
<RoutedTabs tabs={ tabs } listProps={{ mt: 6 }} isLoading={ isTabsLoading }/>;
const backLink = React.useMemo(() => {
if (appProps.referrer && appProps.referrer.includes('/accounts')) {
return {
......@@ -428,10 +427,7 @@ const AddressPageContent = () => {
{ !addressMetadataQuery.isPending &&
<AddressMetadataAlert tags={ addressMetadataQuery.data?.addresses?.[hash.toLowerCase()]?.tags } mt="-4px" mb={ 6 }/> }
{ config.features.metasuites.isEnabled && <Box display="none" id="meta-suites__address" data-ready={ !isLoading }/> }
<AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/>
{ /* should stay before tabs to scroll up with pagination */ }
<Box ref={ tabsScrollRef }></Box>
{ content }
<RoutedTabs tabs={ tabs } listProps={{ mt: 6 }} isLoading={ isTabsLoading }/>
</>
);
};
......
......@@ -3,9 +3,6 @@ import { Flex, Box } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { StaticRoute } from 'nextjs-routes';
import { route } from 'nextjs-routes';
import useFetch from 'lib/hooks/useFetch';
import { Tooltip } from 'toolkit/chakra/tooltip';
import useClipboard from 'toolkit/hooks/useClipboard';
......@@ -21,10 +18,10 @@ const formatFileSize = (fileSizeInBytes: number) => `${ (fileSizeInBytes / 1_024
interface IconInfo {
name: string;
fileSize: number;
file_size: number;
}
const Item = ({ name, fileSize, bgColor }: IconInfo & HTMLChakraProps<'div'>) => {
const Item = ({ name, file_size: fileSize, bgColor }: IconInfo & HTMLChakraProps<'div'>) => {
const { hasCopied, copy } = useClipboard(name, 1000);
const [ copied, setCopied ] = React.useState(false);
......@@ -47,7 +44,7 @@ const Item = ({ name, fileSize, bgColor }: IconInfo & HTMLChakraProps<'div'>) =>
onClick={ copy }
cursor="pointer"
>
<IconSvg name={ name as IconName } boxSize="100px" bgColor={ bgColor } borderRadius="base"/>
<IconSvg name={ name.replace('.svg', '') as IconName } boxSize="100px" bgColor={ bgColor } borderRadius="base"/>
<Tooltip content={ copied ? 'Copied' : 'Copy to clipboard' } open={ copied }>
<Box fontWeight={ 500 } mt={ 2 }>{ name }</Box>
</Tooltip>
......@@ -63,8 +60,7 @@ const Sprite = () => {
const { data, isFetching, isError } = useQuery({
queryKey: [ 'sprite' ],
queryFn: () => {
const url = route({ pathname: '/node-api/sprite' as StaticRoute<'/api/sprite'>['pathname'] });
return fetch<{ icons: Array<IconInfo> }, unknown>(url);
return fetch<Array<IconInfo>, unknown>('/icons/registry.json');
},
});
......@@ -73,11 +69,13 @@ const Sprite = () => {
return <ContentLoader/>;
}
if (isError || !data || !('icons' in data)) {
if (isError || !data || !Array.isArray(data)) {
return <DataFetchAlert/>;
}
const items = data.icons.filter((icon) => icon.name.includes(searchTerm));
const items = data
.filter((icon) => icon.name.includes(searchTerm))
.sort((a, b) => a.name.localeCompare(b.name));
if (items.length === 0) {
return <EmptySearchResult text="No icons found"/>;
......@@ -91,12 +89,12 @@ const Sprite = () => {
})();
const total = React.useMemo(() => {
if (!data || !('icons' in data)) {
if (!data || !Array.isArray(data)) {
return;
}
return data?.icons.reduce((result, item) => {
return data.reduce((result, item) => {
result.num++;
result.fileSize += item.fileSize;
result.fileSize += item.file_size;
return result;
}, { num: 0, fileSize: 0 });
}, [ data ]);
......
......@@ -11,7 +11,6 @@ import { IconButton } from 'toolkit/chakra/icon-button';
import { MenuContent, MenuRoot, MenuTrigger } from 'toolkit/chakra/menu';
import { Skeleton } from 'toolkit/chakra/skeleton';
import IconSvg from 'ui/shared/IconSvg';
import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
import MetadataUpdateMenuItem from './items/MetadataUpdateMenuItem';
import PrivateTagMenuItem from './items/PrivateTagMenuItem';
......@@ -32,14 +31,10 @@ const AccountActionsMenu = ({ isLoading, className, showUpdateMetadataItem }: Pr
const isTokenInstancePage = router.pathname === '/token/[hash]/instance/[id]';
const isTxPage = router.pathname === '/tx/[hash]';
const profileQuery = useProfileQuery();
const handleButtonClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Address actions (more button)' });
}, []);
const userWithoutEmail = profileQuery.data && !profileQuery.data.email;
const items = [
{
render: (props: ItemProps) => <MetadataUpdateMenuItem { ...props }/>,
......@@ -47,7 +42,7 @@ const AccountActionsMenu = ({ isLoading, className, showUpdateMetadataItem }: Pr
},
{
render: (props: ItemProps) => <TokenInfoMenuItem { ...props }/>,
enabled: config.features.account.isEnabled && isTokenPage && config.features.addressVerification.isEnabled && !userWithoutEmail,
enabled: config.features.account.isEnabled && isTokenPage && config.features.addressVerification.isEnabled,
},
{
render: (props: ItemProps) => <PrivateTagMenuItem { ...props } entityType={ isTxPage ? 'tx' : 'address' }/>,
......
......@@ -72,7 +72,7 @@ const TokenInfoMenuItem = ({ hash, type }: ItemProps) => {
const icon = <IconSvg name="edit" boxSize={ 6 } p={ 0.5 }/>;
return (
<AuthGuard onAuthSuccess={ onAuthSuccess }>
<AuthGuard onAuthSuccess={ onAuthSuccess } ensureEmail>
{ ({ onClick }) => (
<ButtonItem label={ label } icon={ icon } onClick={ onClick }/>
) }
......@@ -83,7 +83,7 @@ const TokenInfoMenuItem = ({ hash, type }: ItemProps) => {
const icon = <IconSvg name="edit" boxSize={ 6 } p={ 1 }/>;
return (
<AuthGuard onAuthSuccess={ onAuthSuccess }>
<AuthGuard onAuthSuccess={ onAuthSuccess } ensureEmail>
{ ({ onClick }) => (
<MenuItem onClick={ onClick } value="add-token-info">
{ icon }
......
......@@ -26,7 +26,7 @@ function getRequestParams(token: TokenInfo, tokenId?: string): WatchAssetParams
options: {
address: token.address,
symbol: token.symbol || '',
decimals: Number(token.decimals) || 18,
decimals: Number(token.decimals ?? '18'),
image: token.icon_url || '',
},
};
......
......@@ -3,7 +3,7 @@ import React from 'react';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import AuthModal from './AuthModal';
import useIsAuth from './useIsAuth';
import useProfileQuery from './useProfileQuery';
interface InjectedProps {
onClick: () => void;
......@@ -12,27 +12,51 @@ interface InjectedProps {
interface Props {
children: (props: InjectedProps) => React.ReactNode;
onAuthSuccess: () => void;
ensureEmail?: boolean;
}
const AuthGuard = ({ children, onAuthSuccess }: Props) => {
const AuthGuard = ({ children, onAuthSuccess, ensureEmail }: Props) => {
const authModal = useDisclosure();
const isAuth = useIsAuth();
const profileQuery = useProfileQuery();
const handleClick = React.useCallback(() => {
isAuth ? onAuthSuccess() : authModal.onOpen();
}, [ authModal, isAuth, onAuthSuccess ]);
if (profileQuery.data) {
if (ensureEmail && !profileQuery.data.email) {
authModal.onOpen();
} else {
onAuthSuccess();
}
} else {
authModal.onOpen();
}
}, [ authModal, ensureEmail, profileQuery.data, onAuthSuccess ]);
const handleModalClose = React.useCallback((isSuccess?: boolean) => {
if (isSuccess) {
if (ensureEmail && !profileQuery.data?.email) {
// If the user has logged in and has not added an email
// we need to close the modal and open it again
// so the initial screen will be correct
authModal.onClose();
window.setTimeout(() => {
authModal.onOpen();
}, 500);
return;
}
onAuthSuccess();
}
authModal.onClose();
}, [ authModal, onAuthSuccess ]);
}, [ authModal, ensureEmail, profileQuery.data, onAuthSuccess ]);
return (
<>
{ children({ onClick: handleClick }) }
{ authModal.open && <AuthModal onClose={ handleModalClose } initialScreen={{ type: 'select_method' }}/> }
{ authModal.open && (
<AuthModal
onClose={ handleModalClose }
initialScreen={ profileQuery.data && !profileQuery.data.email && ensureEmail ? { type: 'email' } : { type: 'select_method' } }
/>
) }
</>
);
};
......
......@@ -38,7 +38,7 @@ const IntTxsIndexingStatus = () => {
useSocketMessage({
channel: internalTxsIndexingChannel,
event: 'internal_txs_index_status',
event: 'index_status',
handler: handleInternalTxsIndexStatus,
});
......
......@@ -51,7 +51,7 @@ const IndexingBlocksAlert = () => {
useSocketMessage({
channel: blockIndexingChannel,
event: 'block_index_status',
event: 'index_status',
handler: handleBlocksIndexStatus,
});
......
......@@ -29,6 +29,7 @@ const SettingsColorTheme = ({ onSelect }: Props) => {
window.document.documentElement.style.setProperty(varName, hex);
cookies.set(cookies.NAMES.COLOR_MODE_HEX, hex);
cookies.set(cookies.NAMES.COLOR_MODE, nextTheme.colorMode);
window.localStorage.setItem(cookies.NAMES.COLOR_MODE, nextTheme.colorMode);
}, [ setColorMode ]);
......
......@@ -105,7 +105,7 @@ const TxsStats = () => {
isLoading={ isLoading }
/>
) }
{ txFeeSum24h && (
{ txFeeSum24h != null && (
<StatsWidget
label={ txsStatsQuery.data?.transactions_fee_24h?.title ?
getLabelFromTitle(txsStatsQuery.data?.transactions_fee_24h?.title) :
......
......@@ -3838,10 +3838,10 @@
dependencies:
webpack-bundle-analyzer "4.10.1"
"@next/env@15.0.3":
version "15.0.3"
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.0.3.tgz#a2e9bf274743c52b74d30f415f3eba750d51313a"
integrity sha512-t9Xy32pjNOvVn2AS+Utt6VmyrshbpfUMhIjFO60gI58deSo/KgLOp31XZ4O+kY/Is8WAGYwA5gR7kOb1eORDBA==
"@next/env@15.2.3":
version "15.2.3"
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.2.3.tgz#037ee37c4d61fcbdbb212694cc33d7dcf6c7975a"
integrity sha512-a26KnbW9DFEUsSxAxKBORR/uD9THoYoKbkpFywMN/AFvboTt94b8+g/07T8J6ACsdLag8/PDU60ov4rPxRAixw==
"@next/eslint-plugin-next@15.0.3":
version "15.0.3"
......@@ -3850,45 +3850,45 @@
dependencies:
fast-glob "3.3.1"
"@next/swc-darwin-arm64@15.0.3":
version "15.0.3"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.0.3.tgz#4c40c506cf3d4d87da0204f4cccf39e6bdc46a71"
integrity sha512-s3Q/NOorCsLYdCKvQlWU+a+GeAd3C8Rb3L1YnetsgwXzhc3UTWrtQpB/3eCjFOdGUj5QmXfRak12uocd1ZiiQw==
"@next/swc-darwin-x64@15.0.3":
version "15.0.3"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.0.3.tgz#8e06cacae3dae279744f9fbe88dea679ec2c1ca3"
integrity sha512-Zxl/TwyXVZPCFSf0u2BNj5sE0F2uR6iSKxWpq4Wlk/Sv9Ob6YCKByQTkV2y6BCic+fkabp9190hyrDdPA/dNrw==
"@next/swc-linux-arm64-gnu@15.0.3":
version "15.0.3"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.0.3.tgz#c144ad1f21091b9c6e1e330ecc2d56188763191d"
integrity sha512-T5+gg2EwpsY3OoaLxUIofmMb7ohAUlcNZW0fPQ6YAutaWJaxt1Z1h+8zdl4FRIOr5ABAAhXtBcpkZNwUcKI2fw==
"@next/swc-linux-arm64-musl@15.0.3":
version "15.0.3"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.0.3.tgz#3ccb71c6703bf421332f177d1bb0e10528bc73a2"
integrity sha512-WkAk6R60mwDjH4lG/JBpb2xHl2/0Vj0ZRu1TIzWuOYfQ9tt9NFsIinI1Epma77JVgy81F32X/AeD+B2cBu/YQA==
"@next/swc-linux-x64-gnu@15.0.3":
version "15.0.3"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.0.3.tgz#b90aa9b07001b4000427c35ab347a9273cbeebb3"
integrity sha512-gWL/Cta1aPVqIGgDb6nxkqy06DkwJ9gAnKORdHWX1QBbSZZB+biFYPFti8aKIQL7otCE1pjyPaXpFzGeG2OS2w==
"@next/swc-linux-x64-musl@15.0.3":
version "15.0.3"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.0.3.tgz#0ac9724fb44718fc97bfea971ac3fe17e486590e"
integrity sha512-QQEMwFd8r7C0GxQS62Zcdy6GKx999I/rTO2ubdXEe+MlZk9ZiinsrjwoiBL5/57tfyjikgh6GOU2WRQVUej3UA==
"@next/swc-win32-arm64-msvc@15.0.3":
version "15.0.3"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.0.3.tgz#932437d4cf27814e963ba8ae5f033b4421fab9ca"
integrity sha512-9TEp47AAd/ms9fPNgtgnT7F3M1Hf7koIYYWCMQ9neOwjbVWJsHZxrFbI3iEDJ8rf1TDGpmHbKxXf2IFpAvheIQ==
"@next/swc-win32-x64-msvc@15.0.3":
version "15.0.3"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.0.3.tgz#940a6f7b370cdde0cc67eabe945d9e6d97e0be9f"
integrity sha512-VNAz+HN4OGgvZs6MOoVfnn41kBzT+M+tB+OK4cww6DNyWS6wKaDpaAm/qLeOUbnMh0oVx1+mg0uoYARF69dJyA==
"@next/swc-darwin-arm64@15.2.3":
version "15.2.3"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.3.tgz#2688c185651ef7a16e5642c85048cc4e151159fa"
integrity sha512-uaBhA8aLbXLqwjnsHSkxs353WrRgQgiFjduDpc7YXEU0B54IKx3vU+cxQlYwPCyC8uYEEX7THhtQQsfHnvv8dw==
"@next/swc-darwin-x64@15.2.3":
version "15.2.3"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.3.tgz#3e802259b2c9a4e2ad55ff827f41f775b726fc7d"
integrity sha512-pVwKvJ4Zk7h+4hwhqOUuMx7Ib02u3gDX3HXPKIShBi9JlYllI0nU6TWLbPT94dt7FSi6mSBhfc2JrHViwqbOdw==
"@next/swc-linux-arm64-gnu@15.2.3":
version "15.2.3"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.3.tgz#315d7b54b89153f125bdc3e40bcb7ccf94ef124b"
integrity sha512-50ibWdn2RuFFkOEUmo9NCcQbbV9ViQOrUfG48zHBCONciHjaUKtHcYFiCwBVuzD08fzvzkWuuZkd4AqbvKO7UQ==
"@next/swc-linux-arm64-musl@15.2.3":
version "15.2.3"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.3.tgz#a1a458eb7cf19c59d2014ee388a7305e9a77973f"
integrity sha512-2gAPA7P652D3HzR4cLyAuVYwYqjG0mt/3pHSWTCyKZq/N/dJcUAEoNQMyUmwTZWCJRKofB+JPuDVP2aD8w2J6Q==
"@next/swc-linux-x64-gnu@15.2.3":
version "15.2.3"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.3.tgz#a3cf22eda7601536ccd68e8ba4c1bfb4a1a33460"
integrity sha512-ODSKvrdMgAJOVU4qElflYy1KSZRM3M45JVbeZu42TINCMG3anp7YCBn80RkISV6bhzKwcUqLBAmOiWkaGtBA9w==
"@next/swc-linux-x64-musl@15.2.3":
version "15.2.3"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.3.tgz#0e33c1224c76aa3078cc2249c80ef583f9d7a943"
integrity sha512-ZR9kLwCWrlYxwEoytqPi1jhPd1TlsSJWAc+H/CJHmHkf2nD92MQpSRIURR1iNgA/kuFSdxB8xIPt4p/T78kwsg==
"@next/swc-win32-arm64-msvc@15.2.3":
version "15.2.3"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.3.tgz#4e0583fb981b931915a9ad22e579f9c9d5b803dd"
integrity sha512-+G2FrDcfm2YDbhDiObDU/qPriWeiz/9cRR0yMWJeTLGGX6/x8oryO3tt7HhodA1vZ8r2ddJPCjtLcpaVl7TE2Q==
"@next/swc-win32-x64-msvc@15.2.3":
version "15.2.3"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.3.tgz#727b90c7dcc2279344115a94b99d93d452956f02"
integrity sha512-gHYS9tc+G2W0ZC8rBL+H6RdtXIyk40uLiaos0yj5US85FNhbFEndMA2nW3z47nzOWiSvXTZ5kBClc3rD0zJg0w==
"@noble/ciphers@1.2.1":
version "1.2.1"
......@@ -6097,14 +6097,7 @@
resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9"
integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==
"@swc/helpers@0.5.13":
version "0.5.13"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.13.tgz#33e63ff3cd0cade557672bd7888a39ce7d115a8c"
integrity sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==
dependencies:
tslib "^2.4.0"
"@swc/helpers@^0.5.0":
"@swc/helpers@0.5.15", "@swc/helpers@^0.5.0":
version "0.5.15"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7"
integrity sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==
......@@ -9145,12 +9138,7 @@ camelcase@^6.2.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
caniuse-lite@^1.0.30001400:
version "1.0.30001418"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz#5f459215192a024c99e3e3a53aac310fc7cf24e6"
integrity sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==
caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001663:
caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001663:
version "1.0.30001663"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001663.tgz#1529a723505e429fdfd49532e9fc42273ba7fed7"
integrity sha512-o9C3X27GLKbLeTYZ6HBOLU1tsAcBZsLis28wrVzddShCS16RujjHp9GDHKZqrB3meE0YjhawvMFsGb/igqiPzA==
......@@ -14465,27 +14453,27 @@ next-themes@0.4.4:
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.4.tgz#ce6f68a4af543821bbc4755b59c0d3ced55c2d13"
integrity sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==
next@15.0.3:
version "15.0.3"
resolved "https://registry.yarnpkg.com/next/-/next-15.0.3.tgz#804f5b772e7570ef1f088542a59860914d3288e9"
integrity sha512-ontCbCRKJUIoivAdGB34yCaOcPgYXr9AAkV/IwqFfWWTXEPUgLYkSkqBhIk9KK7gGmgjc64B+RdoeIDM13Irnw==
next@15.2.3:
version "15.2.3"
resolved "https://registry.yarnpkg.com/next/-/next-15.2.3.tgz#1ac803c08076d47eb5b431cb625135616c6bec7e"
integrity sha512-x6eDkZxk2rPpu46E1ZVUWIBhYCLszmUY6fvHBFcbzJ9dD+qRX6vcHusaqqDlnY+VngKzKbAiG2iRCkPbmi8f7w==
dependencies:
"@next/env" "15.0.3"
"@next/env" "15.2.3"
"@swc/counter" "0.1.3"
"@swc/helpers" "0.5.13"
"@swc/helpers" "0.5.15"
busboy "1.6.0"
caniuse-lite "^1.0.30001579"
postcss "8.4.31"
styled-jsx "5.1.6"
optionalDependencies:
"@next/swc-darwin-arm64" "15.0.3"
"@next/swc-darwin-x64" "15.0.3"
"@next/swc-linux-arm64-gnu" "15.0.3"
"@next/swc-linux-arm64-musl" "15.0.3"
"@next/swc-linux-x64-gnu" "15.0.3"
"@next/swc-linux-x64-musl" "15.0.3"
"@next/swc-win32-arm64-msvc" "15.0.3"
"@next/swc-win32-x64-msvc" "15.0.3"
"@next/swc-darwin-arm64" "15.2.3"
"@next/swc-darwin-x64" "15.2.3"
"@next/swc-linux-arm64-gnu" "15.2.3"
"@next/swc-linux-arm64-musl" "15.2.3"
"@next/swc-linux-x64-gnu" "15.2.3"
"@next/swc-linux-x64-musl" "15.2.3"
"@next/swc-win32-arm64-msvc" "15.2.3"
"@next/swc-win32-x64-msvc" "15.2.3"
sharp "^0.33.5"
nextjs-routes@^1.0.8:
......@@ -15385,16 +15373,7 @@ postcss@8.4.31:
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.4.19:
version "8.4.32"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.32.tgz#1dac6ac51ab19adb21b8b34fd2d93a86440ef6c9"
integrity sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==
dependencies:
nanoid "^3.3.7"
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.4.43:
postcss@^8.4.19, postcss@^8.4.43:
version "8.4.47"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365"
integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==
......
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