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