Spaces:
Running
Running
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; | |