mirror of
https://github.com/luxfi/explore.git
synced 2026-07-27 03:13:50 +00:00
feat: horizontal blocks layout, stats grid, network overview fallback chains
- LatestBlocks: full-width layout instead of fixed 280px sidebar - Stats: 4-column grid on desktop, 2-column on mobile - NetworkOverview: fallback known L1 chains when P-chain API unreachable - NetworkOverview: expose isError from useCurrentValidators - P-chain hooks: add retry:2 for resilience - UserProfile: use address_hash instead of id for display - Replace middleware.ts with proxy.ts (unused in production Docker) - Add toolkit/chakra/separator re-export - tsconfig: jsx react-jsx, formatting cleanup - dev.sh: disable turbopack (stability)
This commit is contained in:
+1
-1
@@ -458,7 +458,7 @@ export default tseslint.config(
|
||||
'playwright/**',
|
||||
'deploy/scripts/**',
|
||||
'deploy/tools/**',
|
||||
'middleware.ts',
|
||||
'proxy.ts',
|
||||
'instrumentation*.ts',
|
||||
'*.config.ts',
|
||||
'*.config.js',
|
||||
|
||||
@@ -22,6 +22,7 @@ export function useBlockchains() {
|
||||
queryKey: [ BLOCKCHAINS_QUERY_KEY ],
|
||||
queryFn: fetchBlockchains,
|
||||
staleTime: BLOCKCHAINS_STALE_TIME_MS,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
const blockchains = React.useMemo(
|
||||
|
||||
@@ -75,6 +75,7 @@ export function useCurrentValidators() {
|
||||
queryKey: [ VALIDATORS_QUERY_KEY ],
|
||||
queryFn: fetchCurrentValidators,
|
||||
staleTime: VALIDATORS_STALE_TIME_MS,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
const validators = React.useMemo(
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
import './.next/types/routes.d.ts';
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
|
||||
|
||||
+10
-10
@@ -11,8 +11,8 @@
|
||||
"dev": "./tools/scripts/dev.sh",
|
||||
"dev:preset": "./tools/scripts/dev.preset.sh",
|
||||
"dev:preset:sync": "tsc -p ./tools/preset-sync/tsconfig.json && node ./tools/preset-sync/index.js",
|
||||
"build": "next build",
|
||||
"build:next": "./deploy/scripts/download_assets.sh ./public/assets/configs && pnpm svg:build-sprite && ./deploy/scripts/make_envs_script.sh && next build",
|
||||
"build": "next build --webpack",
|
||||
"build:next": "./deploy/scripts/download_assets.sh ./public/assets/configs && pnpm svg:build-sprite && ./deploy/scripts/make_envs_script.sh && next build --webpack",
|
||||
"build:docker": "./tools/scripts/build.docker.sh",
|
||||
"start": "next start",
|
||||
"start:docker:local": "docker run -p 3000:3000 --env-file .env.local blockscout-frontend:local",
|
||||
@@ -102,7 +102,7 @@
|
||||
"magic-bytes.js": "1.8.0",
|
||||
"mixpanel-browser": "2.67.0",
|
||||
"monaco-editor": "^0.34.1",
|
||||
"next": "15.5.10",
|
||||
"next": "16.1.6",
|
||||
"next-themes": "0.4.4",
|
||||
"nextjs-routes": "^1.0.8",
|
||||
"node-fetch": "^3.2.9",
|
||||
@@ -113,9 +113,9 @@
|
||||
"pino-pretty": "^9.1.1",
|
||||
"prom-client": "15.1.1",
|
||||
"qrcode": "^1.5.1",
|
||||
"react": "19.1.4",
|
||||
"react": "19.2.4",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "19.1.4",
|
||||
"react-dom": "19.2.4",
|
||||
"react-google-recaptcha": "3.1.0",
|
||||
"react-hook-form": "7.52.1",
|
||||
"react-icons": "5.4.0",
|
||||
@@ -136,8 +136,8 @@
|
||||
"@chakra-ui/cli": "3.30.0",
|
||||
"@eslint/compat": "1.2.2",
|
||||
"@eslint/js": "9.14.0",
|
||||
"@next/bundle-analyzer": "15.5.9",
|
||||
"@next/eslint-plugin-next": "15.0.3",
|
||||
"@next/bundle-analyzer": "16.1.6",
|
||||
"@next/eslint-plugin-next": "16.1.6",
|
||||
"@openzeppelin/contracts": "5.4.0",
|
||||
"@playwright/experimental-ct-react": "1.57.0",
|
||||
"@playwright/test": "1.57.0",
|
||||
@@ -156,8 +156,8 @@
|
||||
"@types/papaparse": "^5.3.5",
|
||||
"@types/phoenix": "^1.5.4",
|
||||
"@types/qrcode": "^1.5.0",
|
||||
"@types/react": "19.1.7",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/react-google-recaptcha": "^2.1.5",
|
||||
"@types/react-scroll": "^1.8.4",
|
||||
"@types/swagger-ui-react": "5.18.0",
|
||||
@@ -169,7 +169,7 @@
|
||||
"dotenv-cli": "^6.0.0",
|
||||
"esbuild": "0.25.12",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-next": "15.4.10",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"eslint-plugin-consistent-default-export-name": "^0.0.15",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-import-helpers": "2.0.1",
|
||||
|
||||
Generated
+1350
-972
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ import { NextResponse } from 'next/server';
|
||||
import * as csp from 'nextjs/csp/index';
|
||||
import * as middlewares from 'nextjs/middlewares/index';
|
||||
|
||||
export async function middleware(req: NextRequest) {
|
||||
export async function proxy(req: NextRequest) {
|
||||
const isPageRequest = req.headers.get('accept')?.includes('text/html');
|
||||
const start = Date.now();
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { Separator } from '@chakra-ui/react';
|
||||
export type { SeparatorProps } from '@chakra-ui/react';
|
||||
@@ -26,5 +26,5 @@ dotenv \
|
||||
-e .env.local \
|
||||
-e .env.development \
|
||||
-e .env \
|
||||
-- bash -c './deploy/scripts/make_envs_script.sh && next dev --turbopack -p $NEXT_PUBLIC_APP_PORT' |
|
||||
-- bash -c './deploy/scripts/make_envs_script.sh && next dev -p $NEXT_PUBLIC_APP_PORT' |
|
||||
pino-pretty
|
||||
+28
-5
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -12,11 +16,30 @@
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"baseUrl": ".",
|
||||
"types": ["node"],
|
||||
"types": [
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.node.ts", "**/*.tsx", "**/*.pw.tsx", "decs.d.ts", "global.d.ts"],
|
||||
"exclude": ["node_modules", "node_modules_linux", "./deploy/tools/envs-validator", "./deploy/tools/favicon-generator", "./deploy/tools/multichain-config-generator", "./deploy/tools/essential-dapps-chains-config-generator", "./toolkit/package", "./deploy/tools/llms-txt-generator"],
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.node.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.pw.tsx",
|
||||
"decs.d.ts",
|
||||
"global.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"node_modules_linux",
|
||||
"./deploy/tools/envs-validator",
|
||||
"./deploy/tools/favicon-generator",
|
||||
"./deploy/tools/multichain-config-generator",
|
||||
"./deploy/tools/essential-dapps-chains-config-generator",
|
||||
"./toolkit/package",
|
||||
"./deploy/tools/llms-txt-generator"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ const LatestBlocks = () => {
|
||||
const networkUtilization = getNetworkUtilizationParams(statsQueryResult.data?.network_utilization_percentage ?? 0);
|
||||
|
||||
return (
|
||||
<Box width={{ base: '100%', lg: '280px' }} flexShrink={ 0 }>
|
||||
<Box width="100%">
|
||||
<Heading level="3">Latest blocks</Heading>
|
||||
{ statsQueryResult.data?.network_utilization_percentage !== undefined && (
|
||||
<Skeleton loading={ statsQueryResult.isPlaceholderData } mt={ 2 } display="inline-block" textStyle="sm">
|
||||
|
||||
+1
-1
@@ -214,7 +214,7 @@ const Stats = () => {
|
||||
|
||||
return (
|
||||
<Grid
|
||||
gridTemplateColumns="1fr 1fr"
|
||||
gridTemplateColumns={{ base: '1fr 1fr', lg: 'repeat(4, 1fr)' }}
|
||||
gridGap={{ base: 1, lg: 2 }}
|
||||
flexBasis="50%"
|
||||
flexGrow={ 1 }
|
||||
|
||||
+171
-135
@@ -3,7 +3,6 @@ import React from 'react';
|
||||
|
||||
import { useBlockchains, useChainHeights, useCurrentValidators } from 'lib/api/pchain';
|
||||
import type { PChainBlockchain } from 'lib/api/pchain';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import { Heading } from 'toolkit/chakra/heading';
|
||||
import { Link } from 'toolkit/chakra/link';
|
||||
import { Skeleton } from 'toolkit/chakra/skeleton';
|
||||
@@ -33,6 +32,14 @@ const PRIMARY_CHAINS = [
|
||||
{ id: 'I', name: 'I-Chain', fullName: 'Identity Chain', vm: 'IdentityVM', href: '/chain/i-chain' },
|
||||
] as const;
|
||||
|
||||
// Known L1 chains (fallback when P-chain API is unreachable from browser)
|
||||
const KNOWN_L1_CHAINS: ReadonlyArray<{ readonly name: string; readonly href: string }> = [
|
||||
{ name: 'Zoo', href: 'https://explore-zoo.lux.network' },
|
||||
{ name: 'Hanzo', href: 'https://explore-hanzo.lux.network' },
|
||||
{ name: 'SPC', href: 'https://explore-spc.lux.network' },
|
||||
{ name: 'Pars', href: 'https://explore-pars.lux.network' },
|
||||
];
|
||||
|
||||
const L1_EXPLORER_URLS: Readonly<Record<string, string>> = {
|
||||
Zoo: 'https://explore-zoo.lux.network',
|
||||
Hanzo: 'https://explore-hanzo.lux.network',
|
||||
@@ -166,6 +173,36 @@ const L1ChainRow = ({ chain }: L1ChainRowProps) => {
|
||||
return <Link href={ `/chains/${ slug }` } variant="plain">{ content }</Link>;
|
||||
};
|
||||
|
||||
// ── Known L1 chain row (fallback when API unavailable) ──
|
||||
|
||||
interface KnownL1RowProps {
|
||||
readonly name: string;
|
||||
readonly href: string;
|
||||
}
|
||||
|
||||
const KnownL1Row = ({ name, href }: KnownL1RowProps) => (
|
||||
<Link href={ href } variant="plain" target="_blank">
|
||||
<Flex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
py={ 2 }
|
||||
px={ 3 }
|
||||
borderRadius="md"
|
||||
cursor="pointer"
|
||||
_hover={{ bg: CARD_HOVER }}
|
||||
transition="background 0.15s"
|
||||
>
|
||||
<Text fontWeight={ 600 } color="text.primary" fontSize="sm">
|
||||
{ name }
|
||||
</Text>
|
||||
<Flex align="center" gap={ 1.5 }>
|
||||
<Tag size="sm" variant="subtle">L1</Tag>
|
||||
<Text color="text.secondary" fontSize="xs">{ '\u2192' }</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Link>
|
||||
);
|
||||
|
||||
// ── Sidebar card ──
|
||||
|
||||
interface SidebarCardProps {
|
||||
@@ -206,8 +243,7 @@ const SidebarCard = ({ title, count, isLoading, action, children }: SidebarCardP
|
||||
// ── Main page ──
|
||||
|
||||
const NetworkOverview = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const { stats, isLoading: validatorsLoading } = useCurrentValidators();
|
||||
const { stats, isLoading: validatorsLoading, isError: validatorsError } = useCurrentValidators();
|
||||
const { blockchains, isLoading: chainsLoading } = useBlockchains();
|
||||
const { pChainHeight, cChainHeight, isLoading: heightsLoading } = useChainHeights();
|
||||
|
||||
@@ -216,9 +252,16 @@ const NetworkOverview = () => {
|
||||
[ blockchains ],
|
||||
);
|
||||
|
||||
const totalChains = PRIMARY_CHAINS.length + l1Chains.length;
|
||||
// Use known L1 chains as fallback when P-chain API is unreachable
|
||||
const hasL1Data = l1Chains.length > 0;
|
||||
const showFallbackL1 = !chainsLoading && !hasL1Data;
|
||||
|
||||
const totalChains = PRIMARY_CHAINS.length + (hasL1Data ? l1Chains.length : KNOWN_L1_CHAINS.length);
|
||||
const isLoading = validatorsLoading || chainsLoading;
|
||||
|
||||
// Hide validator metrics when API returned an error (show dashes instead of 0)
|
||||
const hasValidatorData = !validatorsError && stats.validatorCount > 0;
|
||||
|
||||
return (
|
||||
<Box as="main">
|
||||
{ /* ── Hero search ── */ }
|
||||
@@ -238,164 +281,157 @@ const NetworkOverview = () => {
|
||||
flexWrap="wrap"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Metric label="C-Chain" value={ cChainHeight > 0 ? `#${ cChainHeight.toLocaleString() }` : '—' } isLoading={ heightsLoading }/>
|
||||
<Metric label="C-Chain" value={ cChainHeight > 0 ? `#${ cChainHeight.toLocaleString() }` : '\u2014' } isLoading={ heightsLoading }/>
|
||||
<Box w="1px" h="28px" bgColor="border.divider" display={{ base: 'none', md: 'block' }}/>
|
||||
<Metric label="P-Chain" value={ pChainHeight > 0 ? `#${ pChainHeight.toLocaleString() }` : '—' } isLoading={ heightsLoading }/>
|
||||
<Metric label="P-Chain" value={ pChainHeight > 0 ? `#${ pChainHeight.toLocaleString() }` : '\u2014' } isLoading={ heightsLoading }/>
|
||||
<Box w="1px" h="28px" bgColor="border.divider" display={{ base: 'none', md: 'block' }}/>
|
||||
<Metric label="Chains" value={ String(totalChains) } isLoading={ isLoading }/>
|
||||
<Box w="1px" h="28px" bgColor="border.divider" display={{ base: 'none', md: 'block' }}/>
|
||||
<Metric label="Validators" value={ String(stats.validatorCount) } isLoading={ isLoading }/>
|
||||
<Metric label="Validators" value={ hasValidatorData ? String(stats.validatorCount) : '\u2014' } isLoading={ validatorsLoading }/>
|
||||
<Box w="1px" h="28px" bgColor="border.divider" display={{ base: 'none', md: 'block' }}/>
|
||||
<Metric label="Staked" value={ `${ formatStake(stats.totalStake) } LUX` } isLoading={ isLoading }/>
|
||||
<Metric label="Staked" value={ hasValidatorData ? `${ formatStake(stats.totalStake) } LUX` : '\u2014' } isLoading={ validatorsLoading }/>
|
||||
<Box w="1px" h="28px" bgColor="border.divider" display={{ base: 'none', md: 'block' }}/>
|
||||
<Metric label="Uptime" value={ `${ stats.averageUptime.toFixed(1) }%` } isLoading={ isLoading }/>
|
||||
<Metric label="Uptime" value={ hasValidatorData ? `${ stats.averageUptime.toFixed(1) }%` : '\u2014' } isLoading={ validatorsLoading }/>
|
||||
<Box w="1px" h="28px" bgColor="border.divider" display={{ base: 'none', md: 'block' }}/>
|
||||
<Metric label="Connected" value={ `${ stats.connectedCount }/${ stats.validatorCount }` } isLoading={ isLoading }/>
|
||||
<Metric
|
||||
label="Connected"
|
||||
value={ hasValidatorData ? `${ stats.connectedCount }/${ stats.validatorCount }` : '\u2014' }
|
||||
isLoading={ validatorsLoading }
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
{ /* ── Two-column: Activity (primary) + Chain Health (secondary) ── */ }
|
||||
<Grid
|
||||
templateColumns={{ base: '1fr', lg: '1fr 340px' }}
|
||||
gap={ 5 }
|
||||
mt={ 5 }
|
||||
>
|
||||
{ /* ── Left: Activity ── */ }
|
||||
<Box>
|
||||
{ /* Stats grid */ }
|
||||
<Stats/>
|
||||
{ /* ── Stats widgets ── */ }
|
||||
<Box mt={ 5 }>
|
||||
<Stats/>
|
||||
</Box>
|
||||
|
||||
{ /* Latest blocks */ }
|
||||
<Box mt={ 5 }>
|
||||
<LatestBlocks/>
|
||||
</Box>
|
||||
|
||||
{ /* Transactions */ }
|
||||
<Box mt={ 5 }>
|
||||
<Transactions/>
|
||||
</Box>
|
||||
{ /* ── Two-column: Latest blocks (left) + Latest transactions (right) ── */ }
|
||||
<Flex mt={ 8 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 6 }>
|
||||
<Box flexGrow={ 1 } flexBasis="50%" overflow="hidden">
|
||||
<LatestBlocks/>
|
||||
</Box>
|
||||
<Box flexGrow={ 1 } flexBasis="50%" overflow="hidden">
|
||||
<Transactions/>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
{ /* ── Right: Chain Health sidebar ── */ }
|
||||
<Flex direction="column" gap={ 4 } display={{ base: 'none', lg: 'flex' }}>
|
||||
{ /* Primary Network chains */ }
|
||||
<SidebarCard
|
||||
title="Primary Network"
|
||||
count={ PRIMARY_CHAINS.length }
|
||||
>
|
||||
<Flex direction="column" gap={ 0 }>
|
||||
{ PRIMARY_CHAINS.map((chain) => (
|
||||
{ /* ── Chain Health section (below blocks/txns) ── */ }
|
||||
<Grid
|
||||
templateColumns={{ base: '1fr', lg: 'repeat(3, 1fr)' }}
|
||||
gap={ 4 }
|
||||
mt={ 8 }
|
||||
>
|
||||
{ /* Primary Network chains */ }
|
||||
<SidebarCard
|
||||
title="Primary Network"
|
||||
count={ PRIMARY_CHAINS.length }
|
||||
>
|
||||
<Flex direction="column" gap={ 0 }>
|
||||
{ PRIMARY_CHAINS.map((chain) => {
|
||||
const chainHeight = (() => {
|
||||
if (chain.id === 'C') return cChainHeight;
|
||||
if (chain.id === 'P') return pChainHeight;
|
||||
return undefined;
|
||||
})();
|
||||
return (
|
||||
<ChainRow
|
||||
key={ chain.id }
|
||||
name={ chain.name }
|
||||
fullName={ chain.fullName }
|
||||
vm={ chain.vm }
|
||||
href={ chain.href }
|
||||
height={ chain.id === 'C' ? cChainHeight : chain.id === 'P' ? pChainHeight : undefined }
|
||||
height={ chainHeight }
|
||||
heightLoading={ heightsLoading }
|
||||
/>
|
||||
);
|
||||
}) }
|
||||
</Flex>
|
||||
</SidebarCard>
|
||||
|
||||
{ /* Subnet / L1 chains */ }
|
||||
<SidebarCard
|
||||
title="Chains"
|
||||
count={ hasL1Data ? l1Chains.length : KNOWN_L1_CHAINS.length }
|
||||
isLoading={ chainsLoading }
|
||||
action={{ label: 'View all', href: '/chains' }}
|
||||
>
|
||||
{ chainsLoading && (
|
||||
<Flex direction="column" gap={ 1 }>
|
||||
{ Array.from({ length: 4 }).map((_, i) => (
|
||||
<Skeleton key={ i } loading h="40px" borderRadius="md"/>
|
||||
)) }
|
||||
</Flex>
|
||||
</SidebarCard>
|
||||
|
||||
{ /* Subnet / L1 chains — renamed to just "Chains" */ }
|
||||
<SidebarCard
|
||||
title="Chains"
|
||||
count={ l1Chains.length }
|
||||
isLoading={ chainsLoading }
|
||||
action={{ label: 'View all', href: '/chains' }}
|
||||
>
|
||||
{ chainsLoading && (
|
||||
<Flex direction="column" gap={ 1 }>
|
||||
{ Array.from({ length: 4 }).map((_, i) => (
|
||||
<Skeleton key={ i } loading h="40px" borderRadius="md"/>
|
||||
)) }
|
||||
</Flex>
|
||||
) }
|
||||
{ !chainsLoading && l1Chains.length === 0 && (
|
||||
<Text color="text.secondary" fontSize="sm" py={ 2 }>
|
||||
No chains registered yet.
|
||||
</Text>
|
||||
) }
|
||||
{ !chainsLoading && l1Chains.length > 0 && (
|
||||
<Flex direction="column" gap={ 0 }>
|
||||
{ l1Chains.map((chain) => (
|
||||
<L1ChainRow key={ chain.id } chain={ chain }/>
|
||||
)) }
|
||||
</Flex>
|
||||
) }
|
||||
</SidebarCard>
|
||||
|
||||
{ /* Validators summary card */ }
|
||||
<SidebarCard title="Validators">
|
||||
<Grid templateColumns="1fr 1fr" gap={ 3 }>
|
||||
<Box>
|
||||
<Skeleton loading={ isLoading }>
|
||||
<Text fontWeight={ 700 } fontFamily="mono" fontSize="md">
|
||||
{ stats.validatorCount }
|
||||
</Text>
|
||||
</Skeleton>
|
||||
<Text fontSize="2xs" color="text.secondary">Active</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Skeleton loading={ isLoading }>
|
||||
<Text fontWeight={ 700 } fontFamily="mono" fontSize="md">
|
||||
{ formatStake(stats.totalStake) }
|
||||
</Text>
|
||||
</Skeleton>
|
||||
<Text fontSize="2xs" color="text.secondary">Total Stake (LUX)</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Skeleton loading={ isLoading }>
|
||||
<Text fontWeight={ 700 } fontFamily="mono" fontSize="md">
|
||||
{ stats.delegatorCount }
|
||||
</Text>
|
||||
</Skeleton>
|
||||
<Text fontSize="2xs" color="text.secondary">Delegators</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Skeleton loading={ isLoading }>
|
||||
<Text fontWeight={ 700 } fontFamily="mono" fontSize="md">
|
||||
{ `${ stats.connectedCount }/${ stats.validatorCount }` }
|
||||
</Text>
|
||||
</Skeleton>
|
||||
<Text fontSize="2xs" color="text.secondary">Connected</Text>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Flex justify="center" mt={ 3 }>
|
||||
<Link href="/validators" textStyle="xs" color="text.secondary" _hover={{ color: 'text.primary' }}>
|
||||
View validators
|
||||
</Link>
|
||||
</Flex>
|
||||
</SidebarCard>
|
||||
</Flex>
|
||||
</Grid>
|
||||
|
||||
{ /* ── Mobile-only chains section ── */ }
|
||||
{ isMobile && (
|
||||
<Box mt={ 5 }>
|
||||
<SidebarCard
|
||||
title="Chains"
|
||||
count={ totalChains }
|
||||
isLoading={ isLoading }
|
||||
action={{ label: 'View all', href: '/chains' }}
|
||||
>
|
||||
) }
|
||||
{ !chainsLoading && hasL1Data && (
|
||||
<Flex direction="column" gap={ 0 }>
|
||||
{ PRIMARY_CHAINS.slice(0, 5).map((chain) => (
|
||||
<ChainRow
|
||||
key={ chain.id }
|
||||
name={ chain.name }
|
||||
fullName={ chain.fullName }
|
||||
vm={ chain.vm }
|
||||
href={ chain.href }
|
||||
/>
|
||||
)) }
|
||||
{ l1Chains.map((chain) => (
|
||||
<L1ChainRow key={ chain.id } chain={ chain }/>
|
||||
)) }
|
||||
</Flex>
|
||||
</SidebarCard>
|
||||
</Box>
|
||||
) }
|
||||
) }
|
||||
{ showFallbackL1 && (
|
||||
<Flex direction="column" gap={ 0 }>
|
||||
{ KNOWN_L1_CHAINS.map((chain) => (
|
||||
<KnownL1Row key={ chain.name } name={ chain.name } href={ chain.href }/>
|
||||
)) }
|
||||
</Flex>
|
||||
) }
|
||||
</SidebarCard>
|
||||
|
||||
{ /* Validators summary card */ }
|
||||
<SidebarCard title="Validators">
|
||||
{ validatorsError ? (
|
||||
<Flex direction="column" align="center" py={ 3 }>
|
||||
<Text color="text.secondary" fontSize="sm">Unable to fetch validator data.</Text>
|
||||
<Link href="/validators" textStyle="xs" color="text.secondary" _hover={{ color: 'text.primary' }} mt={ 2 }>
|
||||
View validators
|
||||
</Link>
|
||||
</Flex>
|
||||
) : (
|
||||
<>
|
||||
<Grid templateColumns="1fr 1fr" gap={ 3 }>
|
||||
<Box>
|
||||
<Skeleton loading={ validatorsLoading }>
|
||||
<Text fontWeight={ 700 } fontFamily="mono" fontSize="md">
|
||||
{ stats.validatorCount }
|
||||
</Text>
|
||||
</Skeleton>
|
||||
<Text fontSize="2xs" color="text.secondary">Active</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Skeleton loading={ validatorsLoading }>
|
||||
<Text fontWeight={ 700 } fontFamily="mono" fontSize="md">
|
||||
{ formatStake(stats.totalStake) }
|
||||
</Text>
|
||||
</Skeleton>
|
||||
<Text fontSize="2xs" color="text.secondary">Total Stake (LUX)</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Skeleton loading={ validatorsLoading }>
|
||||
<Text fontWeight={ 700 } fontFamily="mono" fontSize="md">
|
||||
{ stats.delegatorCount }
|
||||
</Text>
|
||||
</Skeleton>
|
||||
<Text fontSize="2xs" color="text.secondary">Delegators</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Skeleton loading={ validatorsLoading }>
|
||||
<Text fontWeight={ 700 } fontFamily="mono" fontSize="md">
|
||||
{ `${ stats.connectedCount }/${ stats.validatorCount }` }
|
||||
</Text>
|
||||
</Skeleton>
|
||||
<Text fontSize="2xs" color="text.secondary">Connected</Text>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Flex justify="center" mt={ 3 }>
|
||||
<Link href="/validators" textStyle="xs" color="text.secondary" _hover={{ color: 'text.primary' }}>
|
||||
View validators
|
||||
</Link>
|
||||
</Flex>
|
||||
</>
|
||||
) }
|
||||
</SidebarCard>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,9 +8,9 @@ import React from 'react';
|
||||
import config from 'configs/app';
|
||||
import shortenString from 'lib/shortenString';
|
||||
import { Button } from 'toolkit/chakra/button';
|
||||
import { Link } from 'toolkit/chakra/link';
|
||||
import { PopoverBody, PopoverContent, PopoverRoot, PopoverTrigger } from 'toolkit/chakra/popover';
|
||||
import { Separator } from 'toolkit/chakra/separator';
|
||||
import { Link } from 'toolkit/chakra/link';
|
||||
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import useLogout from 'ui/snippets/auth/useLogout';
|
||||
@@ -112,7 +112,7 @@ const UserProfileOidc = ({ buttonSize, buttonVariant = 'header' }: Props) => {
|
||||
<PopoverBody>
|
||||
<Flex direction="column" gap={ 2 } py={ 1 }>
|
||||
<Box fontSize="sm" color="text.secondary" px={ 1 }>
|
||||
{ data.email ?? shortenString(String(data.id ?? '')) }
|
||||
{ data.email ?? shortenString(String(data.address_hash ?? '')) }
|
||||
</Box>
|
||||
<Separator/>
|
||||
<Link href="/auth/profile" textStyle="sm" color="text.primary" px={ 1 }>
|
||||
|
||||
Reference in New Issue
Block a user