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(() => {
// Initial height calculation
const timer = setTimeout(updateHeight, 100);
// Resize handler with debounce
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]);
// Update height when expanded state changes
React.useEffect(() => {
updateHeight();
}, [expanded, updateHeight]);
return (
{quickFilters}
{advancedFilters}
);
};
const Leaderboard = () => {
const { state, actions } = useLeaderboard();
const [searchParams, setSearchParams] = useSearchParams();
const {
data,
isLoading: dataLoading,
error: dataError,
} = useLeaderboardData();
const {
table,
filteredData,
error: processingError,
} = useLeaderboardProcessing();
// Memoize filtered data
const memoizedFilteredData = useMemo(() => filteredData, [filteredData]);
const memoizedTable = useMemo(() => table, [table]);
// Memoize table options
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]);
// Memoize callbacks
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]);
// Memoize loading states
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,
]);
// Memoize child components
const memoizedSearchBar = useMemo(
() => (
),
[
onToggleFilters,
state.filtersExpanded,
loadingStates.showTableSkeleton,
memoizedFilteredData,
table,
]
);
const memoizedQuickFilters = useMemo(
() => (
),
[state.models.length, memoizedFilteredData, memoizedTable]
);
const memoizedLeaderboardFilters = useMemo(
() => (
),
[
memoizedFilteredData,
loadingStates.showFiltersSkeleton,
state.filters.precisions,
state.filters.types,
state.filters.paramsRange,
state.filters.booleanFilters,
onPrecisionsChange,
onTypesChange,
onParamsRangeChange,
onBooleanFiltersChange,
onReset,
]
);
// No need to memoize LeaderboardTable as it handles its own sorting state
const tableComponent = (
);
// Update context with loaded data
useEffect(() => {
if (data) {
actions.setModels(data);
}
}, [data, actions]);
// Log to understand loading state
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 an error occurred, display it
if (dataError || processingError) {
return (
{(dataError || processingError)?.message ||
"An error occurred while loading the data"}
);
}
return (
{loadingStates.showSearchSkeleton ? (
) : (
memoizedSearchBar
)}
{loadingStates.showFiltersSkeleton ? (
) : (
)}
{tableComponent}
);
};
export default Leaderboard;