find-a-leaderboard / client /src /components /LeaderboardSection.jsx
tfrere's picture
update modality:agent
00c453b
raw
history blame
12.1 kB
import React, { useState, useMemo } from "react";
import {
Typography,
Grid,
Box,
Button,
Collapse,
Stack,
Accordion,
AccordionSummary,
AccordionDetails,
} from "@mui/material";
import { alpha } from "@mui/material/styles";
import LeaderboardCard from "./LeaderboardCard";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { useLeaderboard } from "../context/LeaderboardContext";
import SearchOffIcon from "@mui/icons-material/SearchOff";
const ITEMS_PER_PAGE = 3;
const LeaderboardSection = ({
title,
leaderboards,
filteredLeaderboards,
id,
}) => {
const {
expandedSections,
setExpandedSections,
selectedLanguage,
setSelectedLanguage,
searchQuery,
selectedCategory,
} = useLeaderboard();
const isExpanded = expandedSections.has(id);
// Extraire la liste des langues si c'est la section Language Specific
// Cette liste ne doit JAMAIS changer, peu importe les filtres
const languages = useMemo(() => {
if (id !== "language") return null;
const langSet = new Set();
leaderboards.forEach((board) => {
board.tags?.forEach((tag) => {
if (tag.startsWith("language:")) {
const language = tag.split(":")[1];
const capitalizedLang =
language.charAt(0).toUpperCase() + language.slice(1);
langSet.add(capitalizedLang);
}
});
});
return Array.from(langSet).sort();
}, [id, leaderboards]);
// Calculer le nombre de leaderboards par langue
// On utilise les leaderboards bruts pour avoir toutes les langues
const languageStats = useMemo(() => {
if (!languages) return null;
const stats = new Map();
// Compter les leaderboards pour chaque langue
languages.forEach((lang) => {
const count = leaderboards.filter((board) =>
board.tags?.some(
(tag) => tag.toLowerCase() === `language:${lang.toLowerCase()}`
)
).length;
stats.set(lang, count);
});
return stats;
}, [languages, leaderboards]);
// Filtrer pour n'avoir que les leaderboards approuvés
const approvedLeaderboards = filteredLeaderboards.filter(
(leaderboard) => leaderboard.approval_status === "approved"
);
// On ne retourne null que si on n'a pas de leaderboards bruts
if (!leaderboards) return null;
// On affiche toujours les 3 premiers
const displayedLeaderboards = approvedLeaderboards.slice(0, ITEMS_PER_PAGE);
// Le reste sera dans le Collapse
const remainingLeaderboards = approvedLeaderboards.slice(ITEMS_PER_PAGE);
// Calculate how many skeletons we need
const skeletonsNeeded = Math.max(0, 3 - approvedLeaderboards.length);
// On affiche le bouton seulement si aucune catégorie n'est sélectionnée
const showExpandButton = !selectedCategory;
// Le bouton est actif seulement s'il y a plus de 3 leaderboards
const isExpandButtonEnabled = approvedLeaderboards.length > ITEMS_PER_PAGE;
const toggleExpanded = () => {
setExpandedSections((prev) => {
const newSet = new Set(prev);
if (isExpanded) {
newSet.delete(id);
} else {
newSet.add(id);
}
return newSet;
});
};
return (
<Box sx={{ mb: 6 }}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
mb: languageStats ? 2 : 4,
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Typography
variant="h4"
sx={{
color: "text.primary",
fontWeight: 600,
fontSize: { xs: "1.5rem", md: "2rem" },
}}
>
{title}
</Typography>
<Box
sx={(theme) => ({
width: "4px",
height: "4px",
borderRadius: "100%",
backgroundColor: alpha(
theme.palette.text.primary,
theme.palette.mode === "dark" ? 0.2 : 0.15
),
})}
/>
<Typography
variant="h4"
sx={{
color: "text.secondary",
fontWeight: 400,
fontSize: { xs: "1.25rem", md: "1.5rem" },
}}
>
{approvedLeaderboards.length}
</Typography>
</Box>
{showExpandButton && (
<Button
onClick={toggleExpanded}
size="small"
disabled={!isExpandButtonEnabled}
sx={{
color: "text.secondary",
fontSize: "0.875rem",
textTransform: "none",
opacity: isExpandButtonEnabled ? 1 : 0.5,
"&:hover": {
backgroundColor: (theme) =>
isExpandButtonEnabled
? alpha(
theme.palette.text.primary,
theme.palette.mode === "dark" ? 0.1 : 0.06
)
: "transparent",
},
}}
endIcon={
<ExpandMoreIcon
sx={{
transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)",
transition: "transform 300ms",
}}
/>
}
>
{isExpanded ? "Show less" : "Show more"}
</Button>
)}
</Box>
{languages && selectedCategory === "language" && (
<Accordion
defaultExpanded={false}
elevation={0}
sx={{
mb: approvedLeaderboards.length > 0 ? 4 : 2,
"&:before": {
display: "none",
},
backgroundColor: "transparent",
}}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon sx={{ fontSize: 20 }} />}
sx={{
padding: 0,
"& .MuiAccordionSummary-content": {
margin: 0,
},
"& .MuiAccordionSummary-expandIconWrapper": {
position: "relative",
right: "unset",
marginLeft: 1,
transform: "none",
},
}}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography
variant="body2"
color="text.secondary"
sx={{ fontWeight: 500 }}
>
Languages represented in this category
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails sx={{ padding: 0 }}>
<Box
sx={{
display: "flex",
flexWrap: "wrap",
gap: 1,
mx: -0.5,
}}
>
{languages.map((lang) => {
const isActive = selectedLanguage === lang;
const count = languageStats?.get(lang) || 0;
return (
<Button
key={lang}
onClick={() => setSelectedLanguage(isActive ? null : lang)}
variant={isActive ? "contained" : "outlined"}
size="small"
sx={{
textTransform: "none",
m: 0.125,
backgroundColor: (theme) =>
isActive
? undefined
: theme.palette.mode === "dark"
? "background.paper"
: "white",
"&:hover": {
backgroundColor: (theme) =>
isActive
? undefined
: theme.palette.mode === "dark"
? "background.paper"
: "white",
},
"& .MuiTouchRipple-root": {
transition: "none",
},
transition: "none",
}}
>
{lang}
<Box
component="span"
sx={{
display: "inline-flex",
alignItems: "center",
gap: 0.75,
color: isActive ? "inherit" : "text.secondary",
ml: 0.75,
}}
>
<Box
component="span"
sx={(theme) => ({
width: "4px",
height: "4px",
borderRadius: "100%",
backgroundColor: alpha(
theme.palette.text.primary,
theme.palette.mode === "dark" ? 0.2 : 0.15
),
})}
/>
{count}
</Box>
</Button>
);
})}
</Box>
</AccordionDetails>
</Accordion>
)}
{approvedLeaderboards.length === 0 ? (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 2,
py: 7,
bgcolor: (theme) =>
theme.palette.mode === "dark"
? "background.paper"
: "background.default",
borderRadius: 2,
}}
>
<SearchOffIcon
sx={{
fontSize: 64,
color: "text.secondary",
opacity: 0.5,
}}
/>
<Typography variant="h5" color="text.secondary" align="center">
{searchQuery ? (
<>
No {title.toLowerCase()} leaderboard matches{" "}
<Box
component="span"
sx={{
bgcolor: "primary.main",
color: "primary.contrastText",
px: 1,
borderRadius: 1,
}}
>
{searchQuery}
</Box>
</>
) : (
`No ${title.toLowerCase()} leaderboard matches your criteria`
)}
</Typography>
<Typography variant="body1" color="text.secondary" align="center">
Try adjusting your search filters
</Typography>
</Box>
) : (
<>
<Grid container spacing={3}>
{displayedLeaderboards.map((leaderboard, index) => (
<Grid item xs={12} sm={6} md={4} key={index}>
<LeaderboardCard leaderboard={leaderboard} />
</Grid>
))}
{/* Add skeletons if needed */}
{Array.from({ length: skeletonsNeeded }).map((_, index) => (
<Grid item xs={12} sm={6} md={4} key={`skeleton-${index}`}>
<Box
sx={{
height: "180px",
borderRadius: 2,
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.15),
opacity: 1,
transition: "opacity 0.3s ease-in-out",
"&:hover": {
opacity: 0.8,
},
}}
/>
</Grid>
))}
</Grid>
<Collapse in={isExpanded} timeout={300} unmountOnExit>
<Grid container spacing={3} sx={{ mt: 0 }}>
{remainingLeaderboards.map((leaderboard, index) => (
<Grid item xs={12} sm={6} md={4} key={index + ITEMS_PER_PAGE}>
<LeaderboardCard leaderboard={leaderboard} />
</Grid>
))}
</Grid>
</Collapse>
</>
)}
</Box>
);
};
export default LeaderboardSection;