Spaces:
Running
Running
update filterTag count on show arena toggle | update iflterTag disable state
Browse files
client/src/components/LeaderboardFilters/LeaderboardFilters.jsx
CHANGED
@@ -1,9 +1,10 @@
|
|
1 |
import React, { useState, useMemo } from "react";
|
2 |
-
import { Box, Stack,
|
3 |
import { useLeaderboard } from "../../context/LeaderboardContext";
|
4 |
import { useDebounce } from "../../hooks/useDebounce";
|
5 |
import { alpha, lighten, darken } from "@mui/material/styles";
|
6 |
import SearchBar from "./SearchBar";
|
|
|
7 |
|
8 |
// Constantes pour les tags de catégorisation
|
9 |
const CATEGORIZATION_TAGS = [
|
@@ -100,6 +101,18 @@ const LeaderboardFilters = ({ allSections = [] }) => {
|
|
100 |
const [totalArenaCount, setTotalArenaCount] = useState(0);
|
101 |
const debouncedSearch = useDebounce(inputValue, 200);
|
102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
103 |
// Update the search query after debounce
|
104 |
React.useEffect(() => {
|
105 |
setSearchQuery(debouncedSearch);
|
@@ -342,8 +355,8 @@ const LeaderboardFilters = ({ allSections = [] }) => {
|
|
342 |
justifyContent="center"
|
343 |
sx={{ pb: 2 }}
|
344 |
>
|
345 |
-
{(allSections || []).map(({ id, title
|
346 |
-
const count =
|
347 |
const currentGroup = getSectionGroup(id);
|
348 |
const prevGroup =
|
349 |
index > 0 ? getSectionGroup(allSections[index - 1].id) : null;
|
@@ -355,93 +368,24 @@ const LeaderboardFilters = ({ allSections = [] }) => {
|
|
355 |
{needsSpacing && index > 0 && (
|
356 |
<Box sx={{ width: "0.5rem", display: "inline-block" }} />
|
357 |
)}
|
358 |
-
<
|
|
|
|
|
|
|
359 |
onClick={() => {
|
360 |
if (isSelected || count > 0) {
|
361 |
setSelectedCategories(id);
|
362 |
}
|
363 |
}}
|
364 |
-
|
365 |
-
|
366 |
-
disabled={count === 0 && !isSelected}
|
367 |
-
sx={{
|
368 |
-
textTransform: "none",
|
369 |
-
cursor: count === 0 && !isSelected ? "default" : "pointer",
|
370 |
-
mb: 1,
|
371 |
-
backgroundColor: (theme) => {
|
372 |
-
if (isSelected) {
|
373 |
-
return groupColors[currentGroup]?.main;
|
374 |
-
}
|
375 |
-
return theme.palette.mode === "dark"
|
376 |
-
? "background.paper"
|
377 |
-
: groupColors[currentGroup]?.light;
|
378 |
-
},
|
379 |
-
color: (theme) => {
|
380 |
-
if (isSelected) {
|
381 |
-
return "white";
|
382 |
-
}
|
383 |
-
return groupColors[currentGroup]?.main;
|
384 |
-
},
|
385 |
-
borderColor: (theme) => {
|
386 |
-
return groupColors[currentGroup]?.main;
|
387 |
-
},
|
388 |
-
"&:hover": {
|
389 |
-
backgroundColor: (theme) => {
|
390 |
-
if (isSelected) {
|
391 |
-
return groupColors[currentGroup]?.main;
|
392 |
-
}
|
393 |
-
return groupColors[currentGroup]?.light;
|
394 |
-
},
|
395 |
-
},
|
396 |
-
"& .MuiTouchRipple-root": {
|
397 |
-
transition: "none",
|
398 |
-
},
|
399 |
-
transition: "none",
|
400 |
-
}}
|
401 |
-
>
|
402 |
-
{title}
|
403 |
-
<Box
|
404 |
-
component="span"
|
405 |
-
sx={{
|
406 |
-
display: "inline-flex",
|
407 |
-
alignItems: "center",
|
408 |
-
gap: 0.75,
|
409 |
-
color: (theme) => {
|
410 |
-
if (isSelected) {
|
411 |
-
return "white";
|
412 |
-
}
|
413 |
-
// Assombrir la couleur principale du groupe
|
414 |
-
return theme.palette.mode === "dark"
|
415 |
-
? lighten(groupColors[currentGroup]?.main, 0.2)
|
416 |
-
: darken(groupColors[currentGroup]?.main, 0.2);
|
417 |
-
},
|
418 |
-
ml: 0.75,
|
419 |
-
}}
|
420 |
-
>
|
421 |
-
<Box
|
422 |
-
component="span"
|
423 |
-
sx={(theme) => ({
|
424 |
-
width: "4px",
|
425 |
-
height: "4px",
|
426 |
-
borderRadius: "100%",
|
427 |
-
backgroundColor: isSelected
|
428 |
-
? "rgba(255, 255, 255, 0.5)"
|
429 |
-
: alpha(
|
430 |
-
groupColors[currentGroup]?.main,
|
431 |
-
theme.palette.mode === "dark" ? 0.4 : 0.3
|
432 |
-
),
|
433 |
-
})}
|
434 |
-
/>
|
435 |
-
{count}
|
436 |
-
</Box>
|
437 |
-
</Button>
|
438 |
</React.Fragment>
|
439 |
);
|
440 |
})}
|
441 |
</Stack>
|
442 |
</Box>
|
443 |
|
444 |
-
{/* Search
|
445 |
<SearchBar
|
446 |
searchQuery={searchQuery}
|
447 |
setSearchQuery={setSearchQuery}
|
|
|
1 |
import React, { useState, useMemo } from "react";
|
2 |
+
import { Box, Stack, useMediaQuery } from "@mui/material";
|
3 |
import { useLeaderboard } from "../../context/LeaderboardContext";
|
4 |
import { useDebounce } from "../../hooks/useDebounce";
|
5 |
import { alpha, lighten, darken } from "@mui/material/styles";
|
6 |
import SearchBar from "./SearchBar";
|
7 |
+
import FilterTag from "../common/FilterTag";
|
8 |
|
9 |
// Constantes pour les tags de catégorisation
|
10 |
const CATEGORIZATION_TAGS = [
|
|
|
101 |
const [totalArenaCount, setTotalArenaCount] = useState(0);
|
102 |
const debouncedSearch = useDebounce(inputValue, 200);
|
103 |
|
104 |
+
// Calculate section counts based on arena filter
|
105 |
+
const sectionCounts = useMemo(() => {
|
106 |
+
const counts = new Map();
|
107 |
+
allSections.forEach(({ id, data = [] }) => {
|
108 |
+
const filteredData = arenaOnly
|
109 |
+
? data.filter((board) => board.tags?.includes("judge:humans"))
|
110 |
+
: data;
|
111 |
+
counts.set(id, filteredData.length);
|
112 |
+
});
|
113 |
+
return counts;
|
114 |
+
}, [allSections, arenaOnly]);
|
115 |
+
|
116 |
// Update the search query after debounce
|
117 |
React.useEffect(() => {
|
118 |
setSearchQuery(debouncedSearch);
|
|
|
355 |
justifyContent="center"
|
356 |
sx={{ pb: 2 }}
|
357 |
>
|
358 |
+
{(allSections || []).map(({ id, title }, index) => {
|
359 |
+
const count = sectionCounts.get(id) || 0;
|
360 |
const currentGroup = getSectionGroup(id);
|
361 |
const prevGroup =
|
362 |
index > 0 ? getSectionGroup(allSections[index - 1].id) : null;
|
|
|
368 |
{needsSpacing && index > 0 && (
|
369 |
<Box sx={{ width: "0.5rem", display: "inline-block" }} />
|
370 |
)}
|
371 |
+
<FilterTag
|
372 |
+
label={title}
|
373 |
+
count={count}
|
374 |
+
isActive={isSelected}
|
375 |
onClick={() => {
|
376 |
if (isSelected || count > 0) {
|
377 |
setSelectedCategories(id);
|
378 |
}
|
379 |
}}
|
380 |
+
colors={groupColors[currentGroup]}
|
381 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
382 |
</React.Fragment>
|
383 |
);
|
384 |
})}
|
385 |
</Stack>
|
386 |
</Box>
|
387 |
|
388 |
+
{/* Search Bar */}
|
389 |
<SearchBar
|
390 |
searchQuery={searchQuery}
|
391 |
setSearchQuery={setSearchQuery}
|
client/src/components/LeaderboardSection/components/LanguageList.jsx
CHANGED
@@ -2,16 +2,15 @@ import React, { useMemo } from "react";
|
|
2 |
import {
|
3 |
Typography,
|
4 |
Box,
|
5 |
-
Button,
|
6 |
Accordion,
|
7 |
AccordionSummary,
|
8 |
AccordionDetails,
|
9 |
} from "@mui/material";
|
10 |
-
import { alpha } from "@mui/material/styles";
|
11 |
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
12 |
import { useLeaderboard } from "../../../context/LeaderboardContext";
|
|
|
13 |
|
14 |
-
const
|
15 |
main: "#F44336",
|
16 |
light: "#FFEBEE",
|
17 |
};
|
@@ -24,18 +23,42 @@ const LanguageList = ({
|
|
24 |
LANGUAGE_FAMILIES,
|
25 |
findLanguageFamily,
|
26 |
}) => {
|
27 |
-
const { isLanguageExpanded, setIsLanguageExpanded } =
|
|
|
28 |
|
29 |
-
// Store initial language stats in a memo to keep them constant
|
30 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
|
32 |
// Group languages by family
|
33 |
const groupedLanguages = languages.reduce((acc, lang) => {
|
34 |
-
|
35 |
-
if (
|
36 |
-
|
|
|
|
|
|
|
|
|
37 |
}
|
38 |
-
acc[family].push(lang);
|
39 |
return acc;
|
40 |
}, {});
|
41 |
|
@@ -56,7 +79,6 @@ const LanguageList = ({
|
|
56 |
"&:before": {
|
57 |
display: "none",
|
58 |
},
|
59 |
-
|
60 |
bgcolor: "transparent",
|
61 |
}}
|
62 |
>
|
@@ -105,76 +127,17 @@ const LanguageList = ({
|
|
105 |
>
|
106 |
{familyLanguages.map((lang) => {
|
107 |
const isActive = selectedLanguage === lang;
|
108 |
-
const count =
|
109 |
|
110 |
return (
|
111 |
-
<
|
112 |
key={lang}
|
|
|
|
|
|
|
113 |
onClick={() => onLanguageSelect(isActive ? null : lang)}
|
114 |
-
|
115 |
-
|
116 |
-
sx={{
|
117 |
-
textTransform: "none",
|
118 |
-
cursor: "pointer",
|
119 |
-
mb: 0.75,
|
120 |
-
backgroundColor: (theme) => {
|
121 |
-
if (isActive) {
|
122 |
-
return TAG_COLOR.main;
|
123 |
-
}
|
124 |
-
return theme.palette.mode === "dark"
|
125 |
-
? "background.paper"
|
126 |
-
: TAG_COLOR.light;
|
127 |
-
},
|
128 |
-
color: (theme) => {
|
129 |
-
if (isActive) {
|
130 |
-
return "white";
|
131 |
-
}
|
132 |
-
return TAG_COLOR.main;
|
133 |
-
},
|
134 |
-
borderColor: TAG_COLOR.main,
|
135 |
-
"&:hover": {
|
136 |
-
backgroundColor: (theme) => {
|
137 |
-
if (isActive) {
|
138 |
-
return TAG_COLOR.main;
|
139 |
-
}
|
140 |
-
return TAG_COLOR.light;
|
141 |
-
},
|
142 |
-
opacity: 0.8,
|
143 |
-
},
|
144 |
-
"& .MuiTouchRipple-root": {
|
145 |
-
transition: "none",
|
146 |
-
},
|
147 |
-
transition: "none",
|
148 |
-
}}
|
149 |
-
>
|
150 |
-
{capitalize(lang)}
|
151 |
-
<Box
|
152 |
-
component="span"
|
153 |
-
sx={{
|
154 |
-
display: "inline-flex",
|
155 |
-
alignItems: "center",
|
156 |
-
gap: 0.75,
|
157 |
-
color: isActive ? "white" : "inherit",
|
158 |
-
ml: 0.75,
|
159 |
-
}}
|
160 |
-
>
|
161 |
-
<Box
|
162 |
-
component="span"
|
163 |
-
sx={(theme) => ({
|
164 |
-
width: "4px",
|
165 |
-
height: "4px",
|
166 |
-
borderRadius: "100%",
|
167 |
-
backgroundColor: isActive
|
168 |
-
? "rgba(255, 255, 255, 0.5)"
|
169 |
-
: alpha(
|
170 |
-
TAG_COLOR.main,
|
171 |
-
theme.palette.mode === "dark" ? 0.4 : 0.3
|
172 |
-
),
|
173 |
-
})}
|
174 |
-
/>
|
175 |
-
{count}
|
176 |
-
</Box>
|
177 |
-
</Button>
|
178 |
);
|
179 |
})}
|
180 |
</Box>
|
|
|
2 |
import {
|
3 |
Typography,
|
4 |
Box,
|
|
|
5 |
Accordion,
|
6 |
AccordionSummary,
|
7 |
AccordionDetails,
|
8 |
} from "@mui/material";
|
|
|
9 |
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
10 |
import { useLeaderboard } from "../../../context/LeaderboardContext";
|
11 |
+
import FilterTag from "../../common/FilterTag";
|
12 |
|
13 |
+
const LANGUAGE_COLORS = {
|
14 |
main: "#F44336",
|
15 |
light: "#FFEBEE",
|
16 |
};
|
|
|
23 |
LANGUAGE_FAMILIES,
|
24 |
findLanguageFamily,
|
25 |
}) => {
|
26 |
+
const { isLanguageExpanded, setIsLanguageExpanded, arenaOnly } =
|
27 |
+
useLeaderboard();
|
28 |
|
29 |
+
// Store initial language stats in a memo to keep them constant and filter by arena if needed
|
30 |
+
const filteredLanguageStats = useMemo(() => {
|
31 |
+
if (!arenaOnly) return languageStats;
|
32 |
+
|
33 |
+
// Create a new Map with only arena leaderboards
|
34 |
+
const filteredStats = new Map();
|
35 |
+
languageStats.forEach((count, lang) => {
|
36 |
+
// Si nous avons déjà un nombre, nous le gardons tel quel
|
37 |
+
if (typeof count === "number") {
|
38 |
+
filteredStats.set(lang, count);
|
39 |
+
} else {
|
40 |
+
// Si nous avons un tableau de leaderboards, nous filtrons par arena
|
41 |
+
const arenaCount = count.filter((board) =>
|
42 |
+
board.tags?.includes("judge:humans")
|
43 |
+
).length;
|
44 |
+
if (arenaCount > 0) {
|
45 |
+
filteredStats.set(lang, arenaCount);
|
46 |
+
}
|
47 |
+
}
|
48 |
+
});
|
49 |
+
return filteredStats;
|
50 |
+
}, [languageStats, arenaOnly]);
|
51 |
|
52 |
// Group languages by family
|
53 |
const groupedLanguages = languages.reduce((acc, lang) => {
|
54 |
+
// Only include languages that have stats after filtering
|
55 |
+
if (filteredLanguageStats.has(lang)) {
|
56 |
+
const { family } = findLanguageFamily(lang);
|
57 |
+
if (!acc[family]) {
|
58 |
+
acc[family] = [];
|
59 |
+
}
|
60 |
+
acc[family].push(lang);
|
61 |
}
|
|
|
62 |
return acc;
|
63 |
}, {});
|
64 |
|
|
|
79 |
"&:before": {
|
80 |
display: "none",
|
81 |
},
|
|
|
82 |
bgcolor: "transparent",
|
83 |
}}
|
84 |
>
|
|
|
127 |
>
|
128 |
{familyLanguages.map((lang) => {
|
129 |
const isActive = selectedLanguage === lang;
|
130 |
+
const count = filteredLanguageStats.get(lang) || 0;
|
131 |
|
132 |
return (
|
133 |
+
<FilterTag
|
134 |
key={lang}
|
135 |
+
label={capitalize(lang)}
|
136 |
+
count={count}
|
137 |
+
isActive={isActive}
|
138 |
onClick={() => onLanguageSelect(isActive ? null : lang)}
|
139 |
+
colors={LANGUAGE_COLORS}
|
140 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
141 |
);
|
142 |
})}
|
143 |
</Box>
|
client/src/components/LeaderboardSection/hooks/useLanguageStats.js
CHANGED
@@ -99,7 +99,9 @@ export const useLanguageStats = (leaderboards, filteredLeaderboards) => {
|
|
99 |
const statsRef = useRef(null);
|
100 |
const languagesRef = useRef(null);
|
101 |
|
102 |
-
|
|
|
|
|
103 |
const langMap = new Map();
|
104 |
const langFamilyMap = new Map();
|
105 |
|
@@ -135,10 +137,10 @@ export const useLanguageStats = (leaderboards, filteredLeaderboards) => {
|
|
135 |
})
|
136 |
.map(([lang]) => lang);
|
137 |
|
138 |
-
//
|
139 |
statsRef.current = new Map();
|
140 |
languagesRef.current.forEach((lang) => {
|
141 |
-
const count =
|
142 |
board.tags?.some(
|
143 |
(tag) => tag.toLowerCase() === `language:${lang.toLowerCase()}`
|
144 |
)
|
|
|
99 |
const statsRef = useRef(null);
|
100 |
const languagesRef = useRef(null);
|
101 |
|
102 |
+
// Reset stats when leaderboards or filteredLeaderboards change
|
103 |
+
if (leaderboards && filteredLeaderboards) {
|
104 |
+
// Calculate unique languages from all leaderboards
|
105 |
const langMap = new Map();
|
106 |
const langFamilyMap = new Map();
|
107 |
|
|
|
137 |
})
|
138 |
.map(([lang]) => lang);
|
139 |
|
140 |
+
// Calculate stats based on filtered leaderboards
|
141 |
statsRef.current = new Map();
|
142 |
languagesRef.current.forEach((lang) => {
|
143 |
+
const count = filteredLeaderboards.filter((board) =>
|
144 |
board.tags?.some(
|
145 |
(tag) => tag.toLowerCase() === `language:${lang.toLowerCase()}`
|
146 |
)
|
client/src/components/common/FilterTag.jsx
CHANGED
@@ -2,44 +2,53 @@ import React from "react";
|
|
2 |
import { Button, Box } from "@mui/material";
|
3 |
import { alpha } from "@mui/material/styles";
|
4 |
|
5 |
-
const
|
6 |
main: "#F44336",
|
7 |
light: "#FFEBEE",
|
8 |
};
|
9 |
|
10 |
-
const FilterTag = ({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
return (
|
12 |
<Button
|
13 |
onClick={onClick}
|
14 |
variant={isActive ? "contained" : "outlined"}
|
15 |
size="small"
|
|
|
16 |
sx={{
|
17 |
textTransform: "none",
|
18 |
-
cursor: "pointer",
|
19 |
mb: 0.75,
|
20 |
backgroundColor: (theme) => {
|
21 |
if (isActive) {
|
22 |
-
return
|
23 |
}
|
24 |
return theme.palette.mode === "dark"
|
25 |
? "background.paper"
|
26 |
-
:
|
27 |
},
|
28 |
color: (theme) => {
|
29 |
if (isActive) {
|
30 |
return "white";
|
31 |
}
|
32 |
-
return
|
33 |
},
|
34 |
-
borderColor:
|
35 |
"&:hover": {
|
36 |
backgroundColor: (theme) => {
|
37 |
if (isActive) {
|
38 |
-
return
|
39 |
}
|
40 |
-
return
|
41 |
},
|
42 |
-
opacity: 0.8,
|
43 |
},
|
44 |
"& .MuiTouchRipple-root": {
|
45 |
transition: "none",
|
@@ -67,10 +76,7 @@ const FilterTag = ({ label, count, isActive, onClick }) => {
|
|
67 |
borderRadius: "100%",
|
68 |
backgroundColor: isActive
|
69 |
? "rgba(255, 255, 255, 0.5)"
|
70 |
-
: alpha(
|
71 |
-
TAG_COLOR.main,
|
72 |
-
theme.palette.mode === "dark" ? 0.4 : 0.3
|
73 |
-
),
|
74 |
})}
|
75 |
/>
|
76 |
{count}
|
|
|
2 |
import { Button, Box } from "@mui/material";
|
3 |
import { alpha } from "@mui/material/styles";
|
4 |
|
5 |
+
const DEFAULT_COLORS = {
|
6 |
main: "#F44336",
|
7 |
light: "#FFEBEE",
|
8 |
};
|
9 |
|
10 |
+
const FilterTag = ({
|
11 |
+
label,
|
12 |
+
count,
|
13 |
+
isActive,
|
14 |
+
onClick,
|
15 |
+
colors = DEFAULT_COLORS,
|
16 |
+
}) => {
|
17 |
+
const isDisabled = count === 0;
|
18 |
+
|
19 |
return (
|
20 |
<Button
|
21 |
onClick={onClick}
|
22 |
variant={isActive ? "contained" : "outlined"}
|
23 |
size="small"
|
24 |
+
disabled={isDisabled}
|
25 |
sx={{
|
26 |
textTransform: "none",
|
27 |
+
cursor: isDisabled ? "default" : "pointer",
|
28 |
mb: 0.75,
|
29 |
backgroundColor: (theme) => {
|
30 |
if (isActive) {
|
31 |
+
return colors.main;
|
32 |
}
|
33 |
return theme.palette.mode === "dark"
|
34 |
? "background.paper"
|
35 |
+
: colors.light;
|
36 |
},
|
37 |
color: (theme) => {
|
38 |
if (isActive) {
|
39 |
return "white";
|
40 |
}
|
41 |
+
return colors.main;
|
42 |
},
|
43 |
+
borderColor: colors.main,
|
44 |
"&:hover": {
|
45 |
backgroundColor: (theme) => {
|
46 |
if (isActive) {
|
47 |
+
return colors.main;
|
48 |
}
|
49 |
+
return colors.light;
|
50 |
},
|
51 |
+
opacity: isDisabled ? 0.6 : 0.8,
|
52 |
},
|
53 |
"& .MuiTouchRipple-root": {
|
54 |
transition: "none",
|
|
|
76 |
borderRadius: "100%",
|
77 |
backgroundColor: isActive
|
78 |
? "rgba(255, 255, 255, 0.5)"
|
79 |
+
: alpha(colors.main, theme.palette.mode === "dark" ? 0.4 : 0.3),
|
|
|
|
|
|
|
80 |
})}
|
81 |
/>
|
82 |
{count}
|