@@ -30,6 +30,8 @@ import {
3030import { formatCurrency , formatNumber } from "@/lib/utils"
3131import { trimWalletAddress } from "@/lib/analytics/services/leaderboard-transform"
3232import { FEATURE_FLAGS } from "@/lib/feature-flags"
33+ import { useFuulMilesLeaderboard } from "@/hooks/use-fuul-miles-leaderboard"
34+ import { useUserPoints } from "@/hooks/use-user-points"
3335import {
3436 TIER_THRESHOLDS ,
3537 getTierFromVolume ,
@@ -98,63 +100,59 @@ export const LeaderboardTable = ({
98100
99101 const [ leaderboardMode , setLeaderboardMode ] = useState < "volume" | "miles" | "stats" > ( "miles" )
100102
101- // Single fetch for all Fuul leaderboard data (miles + referral cards)
102- const [ milesLeaderboard , setMilesLeaderboard ] = useState <
103- { wallet : string ; points : number ; referrals : number ; rank : number } [ ]
104- > ( [ ] )
105- const [ referralData , setReferralData ] = useState < {
103+ // Shared Fuul miles leaderboard query. Listens for `refetch-user-miles`
104+ // events so that when a swap transitions from pending → processed the
105+ // miles tables refresh automatically, and the AppHeader stays in sync.
106+ const { data : fuulMilesData , isLoading : isMilesLoading } = useFuulMilesLeaderboard ( )
107+ const milesLeaderboard = useMemo ( ( ) => fuulMilesData ?. entries ?? [ ] , [ fuulMilesData ?. entries ] )
108+ const totalParticipants = fuulMilesData ?. totalParticipants ?? 0
109+ const totalMiles = fuulMilesData ?. totalMiles ?? 0
110+ const referralData = useMemo < {
106111 byPoints : ReferralLeaderEntry [ ]
107112 byRefs : ReferralLeaderEntry [ ]
108- } | null > ( null )
109- const [ isMilesLoading , setIsMilesLoading ] = useState ( false )
110- // Server-provided totals (covers ALL participants, not just page 1)
111- const [ totalParticipants , setTotalParticipants ] = useState ( 0 )
112- const [ totalMiles , setTotalMiles ] = useState ( 0 )
113- useEffect ( ( ) => {
114- setIsMilesLoading ( true )
115- fetch ( "/api/fuul/leaderboard?limit=100&page=1&sort=miles" )
116- . then ( ( res ) => ( res . ok ? res . json ( ) : null ) )
117- . then ( ( json ) => {
118- if ( ! json ) return
119- const entries : { wallet : string ; points : number ; referrals : number ; rank : number } [ ] =
120- json . entries ||
121- json . byPoints ?. map ( ( e : ReferralLeaderEntry , i : number ) => ( { ...e , rank : i + 1 } ) ) ||
122- [ ]
123- setMilesLeaderboard ( entries )
124- // Use server-provided totals (computed from full dataset)
125- if ( json . totalParticipants != null ) setTotalParticipants ( json . totalParticipants )
126- else setTotalParticipants ( entries . length )
127- if ( json . totalMiles != null ) setTotalMiles ( json . totalMiles )
128- else
129- setTotalMiles ( entries . reduce ( ( sum : number , e : { points : number } ) => sum + e . points , 0 ) )
130- // Derive referral card data from the same dataset
131- const byPoints = [ ...entries ]
132- . sort ( ( a , b ) => b . points - a . points )
133- . slice ( 0 , 10 )
134- . map ( ( e , i ) => ( { ...e , rank : i + 1 } ) )
135- const byRefs = [ ...entries ]
136- . sort ( ( a , b ) => b . referrals - a . referrals )
137- . slice ( 0 , 10 )
138- . map ( ( e , i ) => ( { ...e , rank : i + 1 } ) )
139- setReferralData ( { byPoints, byRefs } )
140- } )
141- . catch ( ( ) => { } )
142- . finally ( ( ) => setIsMilesLoading ( false ) )
143- } , [ ] )
113+ } | null > ( ( ) => {
114+ if ( ! milesLeaderboard . length ) return null
115+ const byPoints = [ ...milesLeaderboard ]
116+ . sort ( ( a , b ) => b . points - a . points )
117+ . slice ( 0 , 10 )
118+ . map ( ( e , i ) => ( { ...e , rank : i + 1 } ) )
119+ const byRefs = [ ...milesLeaderboard ]
120+ . sort ( ( a , b ) => b . referrals - a . referrals )
121+ . slice ( 0 , 10 )
122+ . map ( ( e , i ) => ( { ...e , rank : i + 1 } ) )
123+ return { byPoints, byRefs }
124+ } , [ milesLeaderboard ] )
125+
126+ // AppHeader badge value (per-user Fuul totals — updates faster than the
127+ // leaderboard dataset). When the leaderboard hasn't caught up yet we
128+ // surface the header value as the floor for the connected user's row
129+ // so the leaderboard never shows a smaller number than the badge.
130+ const { points : headerPoints } = useUserPoints ( )
144131
145132 // Find user in miles leaderboard
146133 const userMilesEntry = useMemo ( ( ) => {
147134 if ( ! userAddr || ! milesLeaderboard . length ) return null
148135 const trimmed = trimWalletAddress ( userAddr . toLowerCase ( ) )
149- return milesLeaderboard . find ( ( e ) => e . wallet === trimmed ) ?? null
150- } , [ userAddr , milesLeaderboard ] )
136+ const found = milesLeaderboard . find ( ( e ) => e . wallet === trimmed ) ?? null
137+ if ( ! found ) return null
138+ if ( headerPoints > found . points ) {
139+ return { ...found , points : headerPoints }
140+ }
141+ return found
142+ } , [ userAddr , milesLeaderboard , headerPoints ] )
151143
152- // Wallet-to-miles lookup for volume leaderboard rows
144+ // Wallet-to-miles lookup for volume leaderboard rows. Override the
145+ // connected user's entry with the AppHeader value when it's higher.
153146 const milesByWallet = useMemo ( ( ) => {
154147 const map = new Map < string , number > ( )
155148 for ( const e of milesLeaderboard ) map . set ( e . wallet , e . points )
149+ if ( userAddr && headerPoints > 0 ) {
150+ const trimmed = trimWalletAddress ( userAddr . toLowerCase ( ) )
151+ const current = map . get ( trimmed ) ?? 0
152+ if ( headerPoints > current ) map . set ( trimmed , headerPoints )
153+ }
156154 return map
157- } , [ milesLeaderboard ] )
155+ } , [ milesLeaderboard , userAddr , headerPoints ] )
158156
159157 // Next rank miles (person above user)
160158 const nextMilesRankEntry = useMemo ( ( ) => {
0 commit comments