|
import React, { useMemo, useEffect, useCallback } from "react"; |
|
import { Box, Typography } from "@mui/material"; |
|
import { useSearchParams } from "react-router-dom"; |
|
|
|
import { TABLE_DEFAULTS } from "./constants/defaults"; |
|
import { useLeaderboard } from "./context/LeaderboardContext"; |
|
import { useLeaderboardProcessing } from "./hooks/useLeaderboardData"; |
|
import { useLeaderboardData } from "./hooks/useLeaderboardData"; |
|
|
|
import LeaderboardFilters from "./components/Filters/Filters"; |
|
import LeaderboardTable from "./components/Table/Table"; |
|
import SearchBar, { SearchBarSkeleton } from "./components/Filters/SearchBar"; |
|
import PerformanceMonitor from "./components/PerformanceMonitor"; |
|
import QuickFilters, { |
|
QuickFiltersSkeleton, |
|
} from "./components/Filters/QuickFilters"; |
|
|
|
const FilterAccordion = ({ expanded, quickFilters, advancedFilters }) => { |
|
const advancedFiltersRef = React.useRef(null); |
|
const quickFiltersRef = React.useRef(null); |
|
const [height, setHeight] = React.useState("auto"); |
|
const resizeTimeoutRef = React.useRef(null); |
|
|
|
const updateHeight = React.useCallback(() => { |
|
if (expanded && advancedFiltersRef.current) { |
|
setHeight(`${advancedFiltersRef.current.scrollHeight}px`); |
|
} else if (!expanded && quickFiltersRef.current) { |
|
setHeight(`${quickFiltersRef.current.scrollHeight}px`); |
|
} |
|
}, [expanded]); |
|
|
|
React.useEffect(() => { |
|
|
|
const timer = setTimeout(updateHeight, 100); |
|
|
|
|
|
const handleResize = () => { |
|
if (resizeTimeoutRef.current) { |
|
clearTimeout(resizeTimeoutRef.current); |
|
} |
|
resizeTimeoutRef.current = setTimeout(updateHeight, 150); |
|
}; |
|
|
|
window.addEventListener("resize", handleResize); |
|
|
|
return () => { |
|
clearTimeout(timer); |
|
window.removeEventListener("resize", handleResize); |
|
if (resizeTimeoutRef.current) { |
|
clearTimeout(resizeTimeoutRef.current); |
|
} |
|
}; |
|
}, [updateHeight]); |
|
|
|
|
|
React.useEffect(() => { |
|
updateHeight(); |
|
}, [expanded, updateHeight]); |
|
|
|
return ( |
|
<Box |
|
sx={{ |
|
position: "relative", |
|
width: "100%", |
|
height, |
|
transition: "height 0.3s ease", |
|
mb: 0.5, |
|
overflow: "hidden", |
|
}} |
|
> |
|
<Box |
|
ref={quickFiltersRef} |
|
sx={{ |
|
position: expanded ? "absolute" : "relative", |
|
top: 0, |
|
left: 0, |
|
right: 0, |
|
opacity: expanded ? 0 : 1, |
|
visibility: expanded ? "hidden" : "visible", |
|
transition: "opacity 0.3s ease", |
|
mb: 0, |
|
}} |
|
> |
|
{quickFilters} |
|
</Box> |
|
<Box |
|
ref={advancedFiltersRef} |
|
sx={{ |
|
position: !expanded ? "absolute" : "relative", |
|
top: 0, |
|
left: 0, |
|
right: 0, |
|
opacity: expanded ? 1 : 0, |
|
visibility: !expanded ? "hidden" : "visible", |
|
transition: "opacity 0.3s ease", |
|
mt: 0, |
|
}} |
|
> |
|
{advancedFilters} |
|
</Box> |
|
</Box> |
|
); |
|
}; |
|
|
|
const Leaderboard = () => { |
|
const { state, actions } = useLeaderboard(); |
|
const [searchParams, setSearchParams] = useSearchParams(); |
|
const { |
|
data, |
|
isLoading: dataLoading, |
|
error: dataError, |
|
} = useLeaderboardData(); |
|
const { |
|
table, |
|
filteredData, |
|
error: processingError, |
|
} = useLeaderboardProcessing(); |
|
|
|
|
|
const memoizedFilteredData = useMemo(() => filteredData, [filteredData]); |
|
const memoizedTable = useMemo(() => table, [table]); |
|
|
|
|
|
const hasTableOptionsChanges = useMemo(() => { |
|
return ( |
|
state.display.rowSize !== TABLE_DEFAULTS.ROW_SIZE || |
|
JSON.stringify(state.display.scoreDisplay) !== |
|
JSON.stringify(TABLE_DEFAULTS.SCORE_DISPLAY) || |
|
state.display.averageMode !== TABLE_DEFAULTS.AVERAGE_MODE || |
|
state.display.rankingMode !== TABLE_DEFAULTS.RANKING_MODE |
|
); |
|
}, [state.display]); |
|
|
|
const hasColumnFilterChanges = useMemo(() => { |
|
return ( |
|
JSON.stringify([...state.display.visibleColumns].sort()) !== |
|
JSON.stringify([...TABLE_DEFAULTS.COLUMNS.DEFAULT_VISIBLE].sort()) |
|
); |
|
}, [state.display.visibleColumns]); |
|
|
|
|
|
const onToggleFilters = useCallback(() => { |
|
actions.toggleFiltersExpanded(); |
|
}, [actions]); |
|
|
|
const onColumnVisibilityChange = useCallback( |
|
(newVisibility) => { |
|
actions.setDisplayOption( |
|
"visibleColumns", |
|
Object.keys(newVisibility).filter((key) => newVisibility[key]) |
|
); |
|
}, |
|
[actions] |
|
); |
|
|
|
const onRowSizeChange = useCallback( |
|
(size) => { |
|
actions.setDisplayOption("rowSize", size); |
|
}, |
|
[actions] |
|
); |
|
|
|
const onScoreDisplayChange = useCallback( |
|
(display) => { |
|
actions.setDisplayOption("scoreDisplay", display); |
|
}, |
|
[actions] |
|
); |
|
|
|
const onAverageModeChange = useCallback( |
|
(mode) => { |
|
actions.setDisplayOption("averageMode", mode); |
|
}, |
|
[actions] |
|
); |
|
|
|
const onRankingModeChange = useCallback( |
|
(mode) => { |
|
actions.setDisplayOption("rankingMode", mode); |
|
}, |
|
[actions] |
|
); |
|
|
|
const onPrecisionsChange = useCallback( |
|
(precisions) => { |
|
actions.setFilter("precisions", precisions); |
|
}, |
|
[actions] |
|
); |
|
|
|
const onTypesChange = useCallback( |
|
(types) => { |
|
actions.setFilter("types", types); |
|
}, |
|
[actions] |
|
); |
|
|
|
const onParamsRangeChange = useCallback( |
|
(range) => { |
|
actions.setFilter("paramsRange", range); |
|
}, |
|
[actions] |
|
); |
|
|
|
const onBooleanFiltersChange = useCallback( |
|
(filters) => { |
|
actions.setFilter("booleanFilters", filters); |
|
}, |
|
[actions] |
|
); |
|
|
|
const onReset = useCallback(() => { |
|
actions.resetFilters(); |
|
}, [actions]); |
|
|
|
|
|
const loadingStates = useMemo(() => { |
|
const isInitialLoading = dataLoading || !data; |
|
const isProcessingData = !memoizedTable || !memoizedFilteredData; |
|
const isApplyingFilters = state.models.length > 0 && !memoizedFilteredData; |
|
const hasValidFilterCounts = |
|
state.countsReady && |
|
state.filterCounts && |
|
state.filterCounts.normal && |
|
state.filterCounts.officialOnly; |
|
|
|
return { |
|
isInitialLoading, |
|
isProcessingData, |
|
isApplyingFilters, |
|
showSearchSkeleton: isInitialLoading || !hasValidFilterCounts, |
|
showFiltersSkeleton: isInitialLoading || !hasValidFilterCounts, |
|
showTableSkeleton: |
|
isInitialLoading || |
|
isProcessingData || |
|
isApplyingFilters || |
|
!hasValidFilterCounts, |
|
}; |
|
}, [ |
|
dataLoading, |
|
data, |
|
memoizedTable, |
|
memoizedFilteredData, |
|
state.models.length, |
|
state.filterCounts, |
|
state.countsReady, |
|
]); |
|
|
|
|
|
const memoizedSearchBar = useMemo( |
|
() => ( |
|
<SearchBar |
|
onToggleFilters={onToggleFilters} |
|
filtersOpen={state.filtersExpanded} |
|
loading={loadingStates.showTableSkeleton} |
|
data={memoizedFilteredData} |
|
table={table} |
|
/> |
|
), |
|
[ |
|
onToggleFilters, |
|
state.filtersExpanded, |
|
loadingStates.showTableSkeleton, |
|
memoizedFilteredData, |
|
table, |
|
] |
|
); |
|
|
|
const memoizedQuickFilters = useMemo( |
|
() => ( |
|
<QuickFilters |
|
totalCount={state.models.length} |
|
filteredCount={memoizedFilteredData?.length || 0} |
|
data={memoizedFilteredData} |
|
table={memoizedTable} |
|
/> |
|
), |
|
[state.models.length, memoizedFilteredData, memoizedTable] |
|
); |
|
|
|
const memoizedLeaderboardFilters = useMemo( |
|
() => ( |
|
<LeaderboardFilters |
|
data={memoizedFilteredData} |
|
loading={loadingStates.showFiltersSkeleton} |
|
selectedPrecisions={state.filters.precisions} |
|
onPrecisionsChange={onPrecisionsChange} |
|
selectedTypes={state.filters.types} |
|
onTypesChange={onTypesChange} |
|
paramsRange={state.filters.paramsRange} |
|
onParamsRangeChange={onParamsRangeChange} |
|
selectedBooleanFilters={state.filters.booleanFilters} |
|
onBooleanFiltersChange={onBooleanFiltersChange} |
|
onReset={onReset} |
|
/> |
|
), |
|
[ |
|
memoizedFilteredData, |
|
loadingStates.showFiltersSkeleton, |
|
state.filters.precisions, |
|
state.filters.types, |
|
state.filters.paramsRange, |
|
state.filters.booleanFilters, |
|
onPrecisionsChange, |
|
onTypesChange, |
|
onParamsRangeChange, |
|
onBooleanFiltersChange, |
|
onReset, |
|
] |
|
); |
|
|
|
|
|
const tableComponent = ( |
|
<LeaderboardTable |
|
table={table} |
|
loading={loadingStates.showTableSkeleton} |
|
onColumnVisibilityChange={onColumnVisibilityChange} |
|
hasTableOptionsChanges={hasTableOptionsChanges} |
|
hasColumnFilterChanges={hasColumnFilterChanges} |
|
rowSize={state.display.rowSize} |
|
onRowSizeChange={onRowSizeChange} |
|
scoreDisplay={state.display.scoreDisplay} |
|
onScoreDisplayChange={onScoreDisplayChange} |
|
averageMode={state.display.averageMode} |
|
onAverageModeChange={onAverageModeChange} |
|
rankingMode={state.display.rankingMode} |
|
onRankingModeChange={onRankingModeChange} |
|
searchParams={searchParams} |
|
setSearchParams={setSearchParams} |
|
pinnedModels={state.pinnedModels} |
|
/> |
|
); |
|
|
|
|
|
useEffect(() => { |
|
if (data) { |
|
actions.setModels(data); |
|
} |
|
}, [data, actions]); |
|
|
|
|
|
useEffect(() => { |
|
if (process.env.NODE_ENV === "development") { |
|
console.log("Loading state:", { |
|
dataLoading, |
|
hasData: !!data, |
|
hasTable: !!table, |
|
hasFilteredData: !!filteredData, |
|
filteredDataLength: filteredData?.length, |
|
stateModelsLength: state.models.length, |
|
hasFilters: Object.keys(state.filters).some((key) => { |
|
if (Array.isArray(state.filters[key])) { |
|
return state.filters[key].length > 0; |
|
} |
|
return !!state.filters[key]; |
|
}), |
|
}); |
|
} |
|
}, [ |
|
dataLoading, |
|
data, |
|
table, |
|
filteredData?.length, |
|
state.models.length, |
|
filteredData, |
|
state.filters, |
|
]); |
|
|
|
|
|
if (dataError || processingError) { |
|
return ( |
|
<Box sx={{ p: 3, textAlign: "center" }}> |
|
<Typography color="error"> |
|
{(dataError || processingError)?.message || |
|
"An error occurred while loading the data"} |
|
</Typography> |
|
</Box> |
|
); |
|
} |
|
|
|
return ( |
|
<Box |
|
sx={{ |
|
width: "100%", |
|
display: "flex", |
|
flexDirection: "column", |
|
}} |
|
> |
|
<PerformanceMonitor /> |
|
<Box |
|
sx={{ |
|
display: "flex", |
|
flexDirection: "column", |
|
gap: 0, |
|
alignItems: "center", |
|
}} |
|
> |
|
<Box |
|
sx={{ |
|
width: { |
|
xs: "100%", |
|
sm: "100%", |
|
md: "80%", |
|
}, |
|
maxWidth: "1200px", |
|
}} |
|
> |
|
{loadingStates.showSearchSkeleton ? ( |
|
<SearchBarSkeleton /> |
|
) : ( |
|
memoizedSearchBar |
|
)} |
|
<Box sx={{ mt: 1 }}> |
|
{loadingStates.showFiltersSkeleton ? ( |
|
<QuickFiltersSkeleton /> |
|
) : ( |
|
<FilterAccordion |
|
expanded={state.filtersExpanded} |
|
quickFilters={memoizedQuickFilters} |
|
advancedFilters={memoizedLeaderboardFilters} |
|
/> |
|
)} |
|
</Box> |
|
</Box> |
|
|
|
<Box |
|
sx={{ |
|
display: "flex", |
|
alignItems: "flex-start", |
|
justifyContent: "center", |
|
width: "100%", |
|
overflow: "hidden", |
|
}} |
|
> |
|
<Box |
|
sx={{ |
|
width: "100%", |
|
px: 1, |
|
}} |
|
> |
|
{tableComponent} |
|
</Box> |
|
</Box> |
|
</Box> |
|
</Box> |
|
); |
|
}; |
|
|
|
export default Leaderboard; |
|
|