11"use client" ;
22
3- import { useState , useEffect , useRef } from "react" ;
3+ import { useState , useEffect , useMemo , useRef } from "react" ;
44import { motion , AnimatePresence } from "framer-motion" ;
55import { Trophy , DollarSign , Zap , Calendar , Share2 , X , BadgeCheck , Loader2 } from "lucide-react" ;
66import { useSession } from "next-auth/react" ;
@@ -13,6 +13,7 @@ import SponsorSlot from "./SponsorSlot";
1313import TierBadge from "./TierBadge" ;
1414import { TIERS } from "@/lib/tiers" ;
1515import { formatNumber , formatCurrency , toolLabel } from "@/lib/utils" ;
16+ import { isLeaderboardResultForRequest , shouldUseServerSeededLeaderboard } from "@/lib/leaderboard-view" ;
1617import { useLeaderboard , useLeaderboardByDateRange } from "@/lib/data/hooks/useSubmissions" ;
1718import { useGlobalStats } from "@/lib/data/hooks/useStats" ;
1819import type { Submission , GlobalStats } from "@/lib/data/types" ;
@@ -60,59 +61,83 @@ export default function Leaderboard({ initialItems, initialStats, initialHasMore
6061 const [ verifiedOnly , setVerifiedOnly ] = useState ( false ) ;
6162 const [ page , setPage ] = useState ( 0 ) ;
6263 const [ allItems , setAllItems ] = useState < Submission [ ] > ( initialItems ?? [ ] ) ;
64+ const [ hasUserInteracted , setHasUserInteracted ] = useState ( false ) ;
6365 const { data : session } = useSession ( ) ;
6466 const loadMoreRef = useRef < HTMLDivElement > ( null ) ;
65- const firstRender = useRef ( true ) ;
6667 const { data : liveStats } = useGlobalStats ( ) ;
6768 const globalStats = liveStats ?? initialStats ;
6869
6970 const ITEMS_PER_PAGE = 25 ;
7071 const isDateFiltered = dateFrom && dateTo ;
7172
7273 // The server already rendered page 0 of the default view — don't re-fetch
73- // it on mount. The hook only runs for non-default filters or later pages.
74- const isSeededDefaultView =
75- ( initialItems ?. length ?? 0 ) > 0 &&
76- page === 0 &&
77- sortBy === "cost" &&
78- ! tool &&
79- ! verifiedOnly &&
80- ! isDateFiltered ;
74+ // it on mount. After any user sort/filter click, page 0 must be fetched
75+ // fresh; otherwise stale seeded or later-page results can masquerade as top.
76+ const isSeededDefaultView = shouldUseServerSeededLeaderboard ( {
77+ hasInitialItems : ( initialItems ?. length ?? 0 ) > 0 ,
78+ hasUserInteracted,
79+ page,
80+ sortBy,
81+ hasToolFilter : ! ! tool ,
82+ verifiedOnly,
83+ isDateFiltered : ! ! isDateFiltered ,
84+ } ) ;
85+
86+ const regularParams = useMemo ( ( ) => ( {
87+ sortBy,
88+ page,
89+ pageSize : ITEMS_PER_PAGE ,
90+ tool : tool ?? undefined ,
91+ verifiedOnly : verifiedOnly || undefined ,
92+ } ) , [ sortBy , page , tool , verifiedOnly ] ) ;
93+ const dateRangeParams = useMemo ( ( ) => ( {
94+ dateFrom,
95+ dateTo,
96+ sortBy,
97+ limit : 100 ,
98+ tool : tool ?? undefined ,
99+ verifiedOnly : verifiedOnly || undefined ,
100+ } ) , [ dateFrom , dateTo , sortBy , tool , verifiedOnly ] ) ;
101+ const dateRangeRequestKey = useMemo ( ( ) => JSON . stringify ( {
102+ dateFrom,
103+ dateTo,
104+ sortBy,
105+ limit : 100 ,
106+ cursor : null ,
107+ tool : tool ?? null ,
108+ verifiedOnly : Boolean ( verifiedOnly ) ,
109+ } ) , [ dateFrom , dateTo , sortBy , tool , verifiedOnly ] ) ;
81110
82111 const { data : regularResult , isLoading } = useLeaderboard (
83- ! isDateFiltered && ! isSeededDefaultView
84- ? { sortBy, page, pageSize : ITEMS_PER_PAGE , tool : tool ?? undefined , verifiedOnly : verifiedOnly || undefined }
85- : "skip"
112+ ! isDateFiltered && ! isSeededDefaultView ? regularParams : "skip"
86113 ) ;
87114
88115 const hasMore = regularResult ?. hasMore ?? ( isSeededDefaultView ? initialHasMore ?? false : false ) ;
89116
90117 const { data : dateFilteredResult } = useLeaderboardByDateRange (
91- isDateFiltered
92- ? { dateFrom, dateTo, sortBy, limit : 100 , tool : tool ?? undefined , verifiedOnly : verifiedOnly || undefined }
93- : "skip"
118+ isDateFiltered ? dateRangeParams : "skip"
94119 ) ;
95120
96121 // Tools available to filter by, sourced from the global per-tool stats.
97122 const availableTools = globalStats ?. modelUsage
98123 ? Object . keys ( globalStats . modelUsage ) . sort ( )
99124 : [ ] ;
100125
101- useEffect ( ( ) => {
102- // Keep the server-seeded items on first render; only reset when a filter
103- // actually changes (avoids clearing the SSR'd rows during hydration).
104- if ( firstRender . current ) {
105- firstRender . current = false ;
106- return ;
107- }
126+ const resetBoardForUserChange = ( ) => {
127+ setHasUserInteracted ( true ) ;
108128 setAllItems ( [ ] ) ;
109129 setPage ( 0 ) ;
110- } , [ sortBy , dateFrom , dateTo , tool , verifiedOnly ] ) ;
130+ } ;
131+
132+ const updateSort = ( nextSort : SortBy ) => {
133+ if ( nextSort !== sortBy ) resetBoardForUserChange ( ) ;
134+ setSortBy ( nextSort ) ;
135+ } ;
111136
112137 useEffect ( ( ) => {
113- if ( isDateFiltered && dateFilteredResult ?. items ) {
138+ if ( isDateFiltered && dateFilteredResult ?. items && dateFilteredResult . requestKey === dateRangeRequestKey ) {
114139 setAllItems ( dateFilteredResult . items ) ;
115- } else if ( ! isDateFiltered && regularResult ?. items ) {
140+ } else if ( ! isDateFiltered && regularResult ?. items && isLeaderboardResultForRequest ( regularResult , regularParams ) ) {
116141 if ( page === 0 ) {
117142 setAllItems ( regularResult . items ) ;
118143 } else {
@@ -123,7 +148,7 @@ export default function Leaderboard({ initialItems, initialStats, initialHasMore
123148 } ) ;
124149 }
125150 }
126- } , [ regularResult , dateFilteredResult , page , isDateFiltered ] ) ;
151+ } , [ regularResult , regularParams , dateFilteredResult , dateRangeRequestKey , page , isDateFiltered ] ) ;
127152
128153 useEffect ( ( ) => {
129154 if ( isDateFiltered || ! hasMore ) return ;
@@ -145,6 +170,7 @@ export default function Leaderboard({ initialItems, initialStats, initialHasMore
145170 } , [ hasMore , isLoading , isDateFiltered , allItems . length ] ) ;
146171
147172 const setQuickFilter = ( days : number | null ) => {
173+ resetBoardForUserChange ( ) ;
148174 if ( days === null ) {
149175 setDateFrom ( "" ) ;
150176 setDateTo ( "" ) ;
@@ -196,7 +222,10 @@ export default function Leaderboard({ initialItems, initialStats, initialHasMore
196222 </ button >
197223
198224 < button
199- onClick = { ( ) => setVerifiedOnly ( v => ! v ) }
225+ onClick = { ( ) => {
226+ resetBoardForUserChange ( ) ;
227+ setVerifiedOnly ( v => ! v ) ;
228+ } }
200229 aria-pressed = { verifiedOnly }
201230 title = "Only show GitHub-verified submissions"
202231 className = { `px-2 py-1 text-xs font-mono font-medium rounded flex items-center gap-1 transition-colors ${
@@ -210,7 +239,10 @@ export default function Leaderboard({ initialItems, initialStats, initialHasMore
210239 { availableTools . length > 1 && (
211240 < select
212241 value = { tool ?? "" }
213- onChange = { ( e ) => setTool ( e . target . value || null ) }
242+ onChange = { ( e ) => {
243+ resetBoardForUserChange ( ) ;
244+ setTool ( e . target . value || null ) ;
245+ } }
214246 aria-label = "Filter by tool"
215247 className = { `px-2 py-1 text-xs font-mono font-medium rounded bg-surface-2 border border-border transition-colors focus:outline-none focus:ring-1 focus:ring-accent ${
216248 tool ? "text-accent" : "text-muted hover:text-foreground"
@@ -228,7 +260,7 @@ export default function Leaderboard({ initialItems, initialStats, initialHasMore
228260
229261 < div className = "flex items-center gap-1" >
230262 < button
231- onClick = { ( ) => setSortBy ( "cost" ) }
263+ onClick = { ( ) => updateSort ( "cost" ) }
232264 className = { `px-2.5 py-1 text-xs font-mono font-medium rounded flex items-center gap-1 transition-colors ${
233265 sortBy === "cost" ? "bg-accent text-white" : "text-muted hover:text-foreground hover:bg-surface-2"
234266 } `}
@@ -237,7 +269,7 @@ export default function Leaderboard({ initialItems, initialStats, initialHasMore
237269 < span className = "hidden sm:inline" > Cost</ span >
238270 </ button >
239271 < button
240- onClick = { ( ) => setSortBy ( "tokens" ) }
272+ onClick = { ( ) => updateSort ( "tokens" ) }
241273 className = { `px-2.5 py-1 text-xs font-mono font-medium rounded flex items-center gap-1 transition-colors ${
242274 sortBy === "tokens" ? "bg-accent text-white" : "text-muted hover:text-foreground hover:bg-surface-2"
243275 } `}
@@ -260,18 +292,31 @@ export default function Leaderboard({ initialItems, initialStats, initialHasMore
260292 < input
261293 type = "date"
262294 value = { dateFrom }
263- onChange = { ( e ) => setDateFrom ( e . target . value ) }
295+ onChange = { ( e ) => {
296+ resetBoardForUserChange ( ) ;
297+ setDateFrom ( e . target . value ) ;
298+ } }
264299 className = "px-3 py-1.5 bg-background border border-border rounded-md"
265300 />
266301 < span className = "text-muted" > →</ span >
267302 < input
268303 type = "date"
269304 value = { dateTo }
270- onChange = { ( e ) => setDateTo ( e . target . value ) }
305+ onChange = { ( e ) => {
306+ resetBoardForUserChange ( ) ;
307+ setDateTo ( e . target . value ) ;
308+ } }
271309 className = "px-3 py-1.5 bg-background border border-border rounded-md"
272310 />
273311 { ( dateFrom || dateTo ) && (
274- < button onClick = { ( ) => { setDateFrom ( "" ) ; setDateTo ( "" ) ; } } className = "text-muted hover:text-foreground" >
312+ < button
313+ onClick = { ( ) => {
314+ resetBoardForUserChange ( ) ;
315+ setDateFrom ( "" ) ;
316+ setDateTo ( "" ) ;
317+ } }
318+ className = "text-muted hover:text-foreground"
319+ >
275320 < X className = "w-4 h-4" />
276321 </ button >
277322 ) }
0 commit comments