@@ -19,7 +19,10 @@ import {
1919 Eye ,
2020 Twitter ,
2121 Linkedin ,
22+ Clock ,
2223} from "lucide-react" ;
24+
25+ const REQUEST_INDEXING_FORM_URL = "https://forms.gle/N4QkRo9UR45xeTTp9" ;
2326import l1ChainsData from "@/constants/l1-chains.json" ;
2427import { L1Chain } from "@/types/stats" ;
2528import { AvalancheLogo } from "@/components/navigation/avalanche-logo" ;
@@ -56,8 +59,10 @@ export default function ChainListPage() {
5659 // Load custom chains from localStorage
5760 const [ customChains , setCustomChains ] = useState < L1Chain [ ] > ( [ ] ) ;
5861
59- // Track Glacier support for each chain
60- const [ glacierSupportMap , setGlacierSupportMap ] = useState < Map < string , boolean > > ( new Map ( ) ) ;
62+ // Track whether the Stats page is available for each chain (i.e. Glacier supports it)
63+ const [ statsSupportMap , setStatsSupportMap ] = useState < Map < string , boolean > > ( new Map ( ) ) ;
64+ // Track whether each chain has any indexed metrics activity. Undefined while loading.
65+ const [ isIndexedMap , setIsIndexedMap ] = useState < Map < string , boolean > > ( new Map ( ) ) ;
6166
6267 useEffect ( ( ) => {
6368 setIsMounted ( true ) ;
@@ -125,41 +130,46 @@ export default function ChainListPage() {
125130 return { total : allChains . length , mainnet, testnet, console } ;
126131 } , [ allChains ] ) ;
127132
128- // Fetch Glacier support for all chains
133+ // Fetch Glacier support + indexing status for all chains
129134 useEffect ( ( ) => {
130- const fetchGlacierSupport = async ( ) => {
135+ const fetchChainStatus = async ( ) => {
131136 const supportMap = new Map < string , boolean > ( ) ;
132-
133- // Fetch support for all chains in parallel (with batching to avoid overwhelming the API)
137+ const indexedMap = new Map < string , boolean > ( ) ;
138+
139+ // Fetch status for all chains in parallel (with batching to avoid overwhelming the API)
134140 const chainsToCheck = allChains . filter ( chain => chain . chainId ) ;
135141 const batchSize = 10 ;
136-
142+
137143 for ( let i = 0 ; i < chainsToCheck . length ; i += batchSize ) {
138144 const batch = chainsToCheck . slice ( i , i + batchSize ) ;
139-
145+
140146 await Promise . all (
141147 batch . map ( async ( chain ) => {
142148 try {
143149 const response = await fetch ( `/api/explorer/${ chain . chainId } ?priceOnly=true` ) ;
144150 if ( response . ok ) {
145151 const data = await response . json ( ) ;
146152 supportMap . set ( chain . chainId , data . glacierSupported === true ) ;
153+ indexedMap . set ( chain . chainId , data . isIndexed === true ) ;
147154 } else {
148155 supportMap . set ( chain . chainId , false ) ;
156+ indexedMap . set ( chain . chainId , false ) ;
149157 }
150158 } catch ( error ) {
151- console . warn ( `Failed to check Glacier support for chain ${ chain . chainId } :` , error ) ;
159+ console . warn ( `Failed to check chain status for ${ chain . chainId } :` , error ) ;
152160 supportMap . set ( chain . chainId , false ) ;
161+ indexedMap . set ( chain . chainId , false ) ;
153162 }
154163 } )
155164 ) ;
156165 }
157-
158- setGlacierSupportMap ( supportMap ) ;
166+
167+ setStatsSupportMap ( supportMap ) ;
168+ setIsIndexedMap ( indexedMap ) ;
159169 } ;
160170
161171 if ( allChains . length > 0 ) {
162- fetchGlacierSupport ( ) ;
172+ fetchChainStatus ( ) ;
163173 }
164174 } , [ allChains ] ) ;
165175
@@ -222,12 +232,17 @@ export default function ChainListPage() {
222232 chain . networkToken ?. symbol ?. toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ||
223233 chain . slug . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ;
224234
225- // RPC URL filter
226- const matchesRpcFilter = ! hideWithoutRpc || ! ! chain . rpcUrl ;
235+ // RPC URL filter — never hide chains we've confirmed are not indexed,
236+ // since their card itself acts as a "request indexing" CTA. The
237+ // chain.isIndexed === false manual override also forces visibility.
238+ const isManuallyNotIndexed = chain . isIndexed === false ;
239+ const isConfirmedNotIndexed = isManuallyNotIndexed
240+ || ( isIndexedMap . has ( chain . chainId ) && ! isIndexedMap . get ( chain . chainId ) ) ;
241+ const matchesRpcFilter = ! hideWithoutRpc || ! ! chain . rpcUrl || isConfirmedNotIndexed ;
227242
228243 return matchesNetwork && matchesCategory && matchesSearch && matchesRpcFilter ;
229244 } ) ;
230- } , [ allChains , selectedNetwork , selectedCategory , searchTerm , hideWithoutRpc ] ) ;
245+ } , [ allChains , selectedNetwork , selectedCategory , searchTerm , hideWithoutRpc , isIndexedMap ] ) ;
231246
232247 const getThemedLogoUrl = ( logoUrl : string ) : string => {
233248 if ( ! isMounted || ! logoUrl ) return logoUrl ;
@@ -477,7 +492,11 @@ export default function ChainListPage() {
477492 < div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6" >
478493 { filteredChains . map ( ( chain ) => {
479494 const chainIdHex = formatChainIdHex ( chain . chainId ) ;
480-
495+ // Manual override from l1-chains.json wins; otherwise wait for the live probe
496+ // so we don't briefly show the banner during initial load.
497+ const isNotIndexed = chain . isIndexed === false
498+ || ( isIndexedMap . has ( chain . chainId ) && ! isIndexedMap . get ( chain . chainId ) ) ;
499+
481500 return (
482501 < Card
483502 key = { `${ chain . chainId } -${ chain . slug } ` }
@@ -628,50 +647,74 @@ export default function ChainListPage() {
628647 </ div >
629648 </ div >
630649
631- { /* Action Buttons - pushed to bottom */ }
632- < div className = "space-y-2.5 mt-auto" >
633- { /* Connect Wallet Button */ }
634- { chain . rpcUrl ? (
635- < AddToWalletButton
636- rpcUrl = { chain . rpcUrl }
637- chainName = { chain . chainName }
638- chainId = { parseInt ( chain . chainId ) }
639- tokenSymbol = { chain . networkToken ?. symbol }
640- variant = "default"
641- className = "w-full h-10 text-sm font-semibold shadow-sm hover:shadow-md transition-all"
642- />
643- ) : (
644- < div className = "w-full h-10 flex items-center justify-center text-xs text-zinc-400 dark:text-zinc-500 bg-zinc-100 dark:bg-zinc-800 rounded-md" >
645- No RPC URL available
646- </ div >
647- ) }
648-
649- { /* Stats and Explorer Buttons */ }
650- { chain . slug && (
651- < div className = "grid grid-cols-2 gap-2.5" >
652- < Button
653- variant = "outline"
654- size = "sm"
655- onClick = { ( ) => glacierSupportMap . get ( chain . chainId ) && ( window . location . href = `/stats/l1/${ chain . slug } ` ) }
656- disabled = { glacierSupportMap . size > 0 && ! glacierSupportMap . get ( chain . chainId ) }
657- className = "h-10 gap-2 font-medium border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-700 hover:text-zinc-900 dark:hover:text-zinc-100 transition-all shadow-sm hover:shadow-md disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-white dark:disabled:hover:bg-zinc-900 disabled:hover:border-zinc-200 dark:disabled:hover:border-zinc-800"
658- >
659- < BarChart3 className = "w-4 h-4" />
660- < span className = "text-sm" > Stats</ span >
661- </ Button >
662- < Button
663- variant = "outline"
664- size = "sm"
665- onClick = { ( ) => chain . rpcUrl && ( window . location . href = `/explorer/${ chain . slug } ` ) }
666- disabled = { ! chain . rpcUrl }
667- className = "h-10 gap-2 font-medium border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-700 hover:text-zinc-900 dark:hover:text-zinc-100 transition-all shadow-sm hover:shadow-md disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-white dark:disabled:hover:bg-zinc-900 disabled:hover:border-zinc-200 dark:disabled:hover:border-zinc-800"
668- >
669- < Eye className = "w-4 h-4" />
670- < span className = "text-sm" > Explorer</ span >
671- </ Button >
650+ { /* Bottom slot — for not-indexed chains, replace action buttons with a CTA card */ }
651+ { isNotIndexed ? (
652+ < div className = "mt-auto rounded-lg border border-dashed border-zinc-300 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/40 px-4 py-4 flex flex-col items-center justify-center text-center gap-2" >
653+ < div className = "flex items-center gap-1.5" >
654+ < Clock className = "w-4 h-4 text-amber-500 dark:text-amber-400" />
655+ < span className = "text-sm font-medium text-zinc-700 dark:text-zinc-200" >
656+ Not indexed yet
657+ </ span >
672658 </ div >
673- ) }
674- </ div >
659+ < p className = "text-xs text-zinc-500 dark:text-zinc-400 leading-snug" >
660+ Activity, stats, and explorer data will be available once this chain is indexed.
661+ </ p >
662+ < a
663+ href = { REQUEST_INDEXING_FORM_URL }
664+ target = "_blank"
665+ rel = "noopener noreferrer"
666+ onClick = { ( e ) => e . stopPropagation ( ) }
667+ className = "mt-1 inline-flex items-center gap-1 px-3 py-1.5 rounded-md border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-xs font-medium text-zinc-700 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-600 transition-colors shadow-sm"
668+ >
669+ Request indexing
670+ < ChevronRight className = "w-3 h-3" />
671+ </ a >
672+ </ div >
673+ ) : (
674+ < div className = "space-y-2.5 mt-auto" >
675+ { /* Connect Wallet Button */ }
676+ { chain . rpcUrl ? (
677+ < AddToWalletButton
678+ rpcUrl = { chain . rpcUrl }
679+ chainName = { chain . chainName }
680+ chainId = { parseInt ( chain . chainId ) }
681+ tokenSymbol = { chain . networkToken ?. symbol }
682+ variant = "default"
683+ className = "w-full h-10 text-sm font-semibold shadow-sm hover:shadow-md transition-all"
684+ />
685+ ) : (
686+ < div className = "w-full h-10 flex items-center justify-center text-xs text-zinc-400 dark:text-zinc-500 bg-zinc-100 dark:bg-zinc-800 rounded-md" >
687+ No RPC URL available
688+ </ div >
689+ ) }
690+
691+ { /* Stats and Explorer Buttons */ }
692+ { chain . slug && (
693+ < div className = "grid grid-cols-2 gap-2.5" >
694+ < Button
695+ variant = "outline"
696+ size = "sm"
697+ onClick = { ( ) => statsSupportMap . get ( chain . chainId ) && ( window . location . href = `/stats/l1/${ chain . slug } ` ) }
698+ disabled = { statsSupportMap . size > 0 && ! statsSupportMap . get ( chain . chainId ) }
699+ className = "h-10 gap-2 font-medium border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-700 hover:text-zinc-900 dark:hover:text-zinc-100 transition-all shadow-sm hover:shadow-md disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-white dark:disabled:hover:bg-zinc-900 disabled:hover:border-zinc-200 dark:disabled:hover:border-zinc-800"
700+ >
701+ < BarChart3 className = "w-4 h-4" />
702+ < span className = "text-sm" > Stats</ span >
703+ </ Button >
704+ < Button
705+ variant = "outline"
706+ size = "sm"
707+ onClick = { ( ) => chain . rpcUrl && ( window . location . href = `/explorer/${ chain . slug } ` ) }
708+ disabled = { ! chain . rpcUrl }
709+ className = "h-10 gap-2 font-medium border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-700 hover:text-zinc-900 dark:hover:text-zinc-100 transition-all shadow-sm hover:shadow-md disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-white dark:disabled:hover:bg-zinc-900 disabled:hover:border-zinc-200 dark:disabled:hover:border-zinc-800"
710+ >
711+ < Eye className = "w-4 h-4" />
712+ < span className = "text-sm" > Explorer</ span >
713+ </ Button >
714+ </ div >
715+ ) }
716+ </ div >
717+ ) }
675718 </ div >
676719 </ div >
677720 </ Card >
0 commit comments