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; | |