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;