1
+ import useAuthentication from "@/hooks/use-authentication" ;
2
+ import { useTokens , UseTokensParams } from "@/hooks/use-tokens" ;
3
+ import { IToken } from "@/types" ;
4
+ import {
5
+ abbreviateNumber ,
6
+ formatNumber ,
7
+ formatNumberSubscriptSmart ,
8
+ } from "@/utils" ;
9
+ import { env } from "@/utils/env" ;
10
+
1
11
const GridViewStats = ( {
2
12
title,
3
13
iframes,
@@ -22,7 +32,126 @@ const GridViewStats = ({
22
32
</ div >
23
33
) ;
24
34
} ;
35
+
36
+ import { useCallback , useRef } from "react" ;
37
+ import { Link } from "react-router" ;
38
+
39
+ const BondedTokenRow = ( { token } : { token : IToken } ) => {
40
+ const priceChangeColor =
41
+ token . priceChange24h && token . priceChange24h > 0
42
+ ? "text-green-500"
43
+ : token . priceChange24h && token . priceChange24h < 0
44
+ ? "text-red-500"
45
+ : "text-gray-400" ;
46
+
47
+ return (
48
+ < Link
49
+ to = { `/token/${ token . mint } ` }
50
+ className = "flex flex-col sm:flex-row items-center justify-between p-4 border-b border-gray-700 hover:bg-gray-800 transition-colors duration-150"
51
+ >
52
+ < div className = "flex items-center mb-4 sm:mb-0 sm:w-1/3 lg:w-1/4" >
53
+ < img
54
+ src = { token . image || "/user-placeholder.png" }
55
+ alt = { token . name || "Token" }
56
+ className = "w-10 h-10 rounded-full mr-3 object-cover"
57
+ onError = { ( e ) => ( e . currentTarget . src = "/user-placeholder.png" ) }
58
+ />
59
+ < div className = "flex flex-col" >
60
+ < span
61
+ className = "font-semibold text-white truncate max-w-[150px] sm:max-w-[200px]"
62
+ title = { token . name || "N/A" }
63
+ >
64
+ { token . name || "N/A" }
65
+ </ span >
66
+ < span className = "text-xs text-gray-400 uppercase" >
67
+ { token . ticker || "N/A" }
68
+ </ span >
69
+ </ div >
70
+ </ div >
71
+
72
+ < div className = "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-x-4 gap-y-2 text-sm w-full sm:w-2/3 lg:w-3/4 text-right" >
73
+ < div className = "flex flex-col items-end" >
74
+ < span className = "text-xs text-gray-400" > Price</ span >
75
+ < span className = "text-white font-medium" >
76
+ { formatNumberSubscriptSmart ( token . tokenPriceUSD , 3 ) }
77
+ </ span >
78
+ </ div >
79
+ < div className = "flex flex-col items-end" >
80
+ < span className = "text-xs text-gray-400" > 24h Change</ span >
81
+ < span className = { `${ priceChangeColor } font-medium` } >
82
+ { token . priceChange24h ?. toFixed ( 2 ) || "0.00" } %
83
+ </ span >
84
+ </ div >
85
+ < div className = "flex flex-col items-end" >
86
+ < span className = "text-xs text-gray-400" > Market Cap</ span >
87
+ < span className = "text-white font-medium" >
88
+ { formatNumber ( token . marketCapUSD ) }
89
+ </ span >
90
+ </ div >
91
+ < div className = "flex flex-col items-end" >
92
+ < span className = "text-xs text-gray-400" > 24h Volume</ span >
93
+ < span className = "text-white font-medium" >
94
+ { formatNumber ( token . volume24h ) }
95
+ </ span >
96
+ </ div >
97
+ < div className = "flex flex-col items-end col-span-2 sm:col-span-1" >
98
+ < span className = "text-xs text-gray-400" > Holders</ span >
99
+ < span className = "text-white font-medium" >
100
+ { abbreviateNumber ( token . holderCount , true ) }
101
+ </ span >
102
+ </ div >
103
+ { /* Optional: Created At
104
+ <div className="flex flex-col items-end">
105
+ <span className="text-xs text-gray-400">Created</span>
106
+ <span className="text-white font-medium">
107
+ {token.createdAt ? new Date(token.createdAt).toLocaleDateString() : 'N/A'}
108
+ </span>
109
+ </div>
110
+ */ }
111
+ </ div >
112
+ </ Link >
113
+ ) ;
114
+ } ;
115
+
25
116
export default function StatsPage ( ) {
117
+ const { walletAddress } = useAuthentication ( ) ;
118
+ const isAdmin =
119
+ ( walletAddress && env . adminAddresses . includes ( walletAddress ) ) || false ;
120
+
121
+ const params : UseTokensParams = {
122
+ hideImported : 1 ,
123
+ sortBy : "createdAt" ,
124
+ sortOrder : "desc" ,
125
+ status : "locked" ,
126
+ } ;
127
+
128
+ const query = useTokens ( params ) ;
129
+
130
+ const observer = useRef < IntersectionObserver | null > ( null ) ;
131
+ const lastElementRef = useCallback (
132
+ ( node : HTMLDivElement ) => {
133
+ if ( query . isLoading || query . isFetchingNextPage ) return ;
134
+ if ( observer . current ) observer . current . disconnect ( ) ;
135
+ observer . current = new IntersectionObserver ( ( entries ) => {
136
+ if ( entries [ 0 ] . isIntersecting && query . hasNextPage ) {
137
+ ( query . fetchNextPage as any ) ( ) ;
138
+ }
139
+ } ) ;
140
+ if ( node ) observer . current . observe ( node ) ;
141
+ } ,
142
+ [
143
+ query . isLoading ,
144
+ query . isFetchingNextPage ,
145
+ query . hasNextPage ,
146
+ query . fetchNextPage ,
147
+ ] ,
148
+ ) ;
149
+
150
+ if ( ! isAdmin ) {
151
+ window . location . href = "/" ;
152
+ return null ;
153
+ }
154
+
26
155
return (
27
156
< div className = "mt-4 mx-4" >
28
157
< GridViewStats
@@ -47,6 +176,16 @@ export default function StatsPage() {
47
176
"https://dune.com/embeds/5133685/8465708?darkMode=true" ,
48
177
] }
49
178
/>
179
+
180
+ < div className = "flex flex-col items-center mt-12" >
181
+ < h1 className = "text-3xl font-bold mb-6" > Bonded Tokens</ h1 >
182
+ < div className = "max-w-[1600px]" >
183
+ { query . items . map ( ( token ) => (
184
+ < BondedTokenRow key = { token . mint } token = { token } />
185
+ ) ) }
186
+ </ div >
187
+ </ div >
188
+ < div ref = { lastElementRef } className = "h-10 w-full" />
50
189
</ div >
51
190
) ;
52
191
}
0 commit comments