find-a-leaderboard / client /src /context /LeaderboardContext.jsx
tfrere's picture
update readme and cleanup
8d4d46d
raw
history blame
9.16 kB
import React, {
createContext,
useContext,
useState,
useCallback,
useMemo,
useEffect,
} from "react";
import { normalizeTags } from "../utils/tagFilters";
import { useUrlState } from "../hooks/useUrlState";
import {
CATEGORIZATION_TAGS,
isUncategorized,
filterLeaderboards as filterLeaderboardsUtil,
generateSections,
} from "../utils/filterUtils";
const LeaderboardContext = createContext();
// Helper pour déterminer si un leaderboard est non catégorisé
const isUncategorizedBoard = isUncategorized;
export const LeaderboardProvider = ({ children }) => {
const { params, updateParams } = useUrlState();
const [leaderboards, setLeaderboards] = useState([]);
const [searchQuery, setSearchQuery] = useState(params.search || "");
const [arenaOnly, setArenaOnly] = useState(params.arena === "true");
const [selectedCategories, setSelectedCategories] = useState(
new Set(params.categories || [])
);
const [selectedLanguage, setSelectedLanguage] = useState(
params.language ? new Set(params.language.split(",")) : new Set()
);
const [expandedSections, setExpandedSections] = useState(
new Set(params.categories || [])
);
const [isLanguageExpanded, setIsLanguageExpanded] = useState(
params.languageExpanded === "true"
);
// Mettre à jour l'URL quand les filtres changent
useEffect(() => {
updateParams({
categories: Array.from(selectedCategories),
search: searchQuery,
arena: arenaOnly ? "true" : null,
language:
selectedLanguage.size > 0
? Array.from(selectedLanguage).join(",")
: null,
languageExpanded: isLanguageExpanded ? "true" : null,
});
}, [
selectedCategories,
searchQuery,
arenaOnly,
selectedLanguage,
isLanguageExpanded,
updateParams,
]);
// Logger les leaderboards non catégorisés
useEffect(() => {
if (leaderboards.length > 0) {
const uncategorizedLeaderboards = leaderboards.filter(
(board) =>
board.approval_status === "approved" && isUncategorizedBoard(board)
);
if (uncategorizedLeaderboards.length > 0) {
console.log("=== LEADERBOARDS NON CATÉGORISÉS ===");
console.log(
`Total: ${uncategorizedLeaderboards.length} leaderboards non catégorisés`
);
console.table(
uncategorizedLeaderboards.map((board) => ({
id: board.id,
uid: board.uid,
title: board.card_data?.title || "Sans titre",
tags: (board.tags || []).join(", "),
url: board.card_data?.url || "",
}))
);
}
}
}, [leaderboards]);
// Wrapper pour setSelectedCategories qui gère aussi l'expansion des sections
const handleCategorySelection = useCallback((categoryId) => {
setSelectedCategories((prev) => {
const newCategories = new Set(prev);
if (newCategories.has(categoryId)) {
newCategories.delete(categoryId);
// Si on désélectionne, on replie la section
setExpandedSections((prev) => {
const newExpanded = new Set(prev);
newExpanded.delete(categoryId);
return newExpanded;
});
} else {
newCategories.add(categoryId);
// Si on sélectionne, on déploie la section
setExpandedSections((prev) => {
const newExpanded = new Set(prev);
newExpanded.add(categoryId);
return newExpanded;
});
}
return newCategories;
});
// On réinitialise la langue sélectionnée seulement si on désélectionne "language"
if (categoryId === "language") {
setSelectedLanguage(new Set());
}
}, []);
// Wrapper pour la sélection de langue
const handleLanguageSelection = useCallback((language) => {
setSelectedLanguage((prev) => {
const newSet = new Set(prev);
if (newSet.has(language)) {
newSet.delete(language);
} else {
newSet.add(language);
}
return newSet;
});
}, []);
// Filter leaderboards based on search query and arena toggle, ignoring category selection
const filterLeaderboardsForCount = useCallback(
(boards) => {
return filterLeaderboardsUtil({
boards,
searchQuery,
arenaOnly,
});
},
[searchQuery, arenaOnly]
);
// Filter leaderboards based on all criteria including categories and language selection
const filterLeaderboards = useCallback(
(boards) => {
return filterLeaderboardsUtil({
boards,
searchQuery,
arenaOnly,
selectedCategories,
selectedLanguage,
});
},
[searchQuery, arenaOnly, selectedCategories, selectedLanguage]
);
// Fonction pour obtenir les leaderboards bruts d'une section
const getSectionLeaderboards = useCallback((boards) => {
if (!boards) return [];
return [...boards];
}, []);
// Filter functions for categories
const filterByTag = useCallback((tag, boards) => {
const searchTag = tag.toLowerCase();
return (
boards?.filter((board) =>
board.tags?.some((t) => t.toLowerCase() === searchTag)
) || []
);
}, []);
const filterByLanguage = useCallback((boards) => {
return (
boards?.filter((board) =>
board.tags?.some((tag) => tag.startsWith("language:"))
) || []
);
}, []);
// Define sections with raw data
const allSections = useMemo(() => {
if (!leaderboards) return [];
return generateSections(leaderboards, filterByTag, filterByLanguage);
}, [leaderboards, filterByTag, filterByLanguage]);
// Get sections with data
const sections = useMemo(() => {
return allSections.filter((section) => section.data.length > 0);
}, [allSections]);
// Sections triées par nombre de leaderboards (pour l'affichage sur la page d'accueil)
const sectionsSortedByCount = useMemo(() => {
return [...sections].sort((a, b) => {
// Filtrer pour n'avoir que les leaderboards approuvés
const approvedA = a.data.filter(
(board) => board.approval_status === "approved"
);
const approvedB = b.data.filter(
(board) => board.approval_status === "approved"
);
return approvedB.length - approvedA.length; // Tri décroissant
});
}, [sections]);
// Calculate total number of unique leaderboards (excluding duplicates)
const totalLeaderboards = useMemo(() => {
const uniqueIds = new Set(
leaderboards
.filter((board) => board.approval_status === "approved")
.map((board) => board.id)
);
return uniqueIds.size;
}, [leaderboards]);
// Get filtered count
const filteredCount = useMemo(() => {
return filterLeaderboards(leaderboards).length;
}, [filterLeaderboards, leaderboards]);
// Function to get highlighted parts of text
const getHighlightedText = useCallback((text, searchTerm) => {
if (!searchTerm || !text) return { text, shouldHighlight: false };
const query = searchTerm.toLowerCase();
const searchableTagPrefixes = [
"domain:",
"language:",
"judge:",
"test:",
"modality:",
"submission:",
"eval:",
];
// Si c'est une recherche par tag, on ne highlight rien
if (searchableTagPrefixes.some((prefix) => query.startsWith(prefix))) {
return { text, shouldHighlight: false };
}
// Sinon on highlight les parties qui matchent
const index = text.toLowerCase().indexOf(query);
if (index === -1) return { text, shouldHighlight: false };
return {
text,
shouldHighlight: true,
highlightStart: index,
highlightEnd: index + query.length,
};
}, []);
// Wrapper pour setLeaderboards qui normalise les tags
const setNormalizedLeaderboards = useCallback((boards) => {
const normalizedBoards = boards.map((board) => ({
...board,
tags: normalizeTags(board.tags),
}));
setLeaderboards(normalizedBoards);
}, []);
const resetState = useCallback(() => {
setSearchQuery("");
setArenaOnly(false);
setSelectedCategories(new Set());
setSelectedLanguage(new Set());
setExpandedSections(new Set());
setIsLanguageExpanded(false);
}, []);
const value = {
leaderboards,
setLeaderboards: setNormalizedLeaderboards,
searchQuery,
setSearchQuery,
arenaOnly,
setArenaOnly,
totalLeaderboards,
filteredCount,
filterLeaderboards,
filterLeaderboardsForCount,
sections,
sectionsSortedByCount,
allSections,
getHighlightedText,
selectedCategories,
setSelectedCategories: handleCategorySelection,
selectedLanguage,
setSelectedLanguage: handleLanguageSelection,
expandedSections,
setExpandedSections,
resetState,
isLanguageExpanded,
setIsLanguageExpanded,
};
return (
<LeaderboardContext.Provider value={value}>
{children}
</LeaderboardContext.Provider>
);
};
export const useLeaderboard = () => {
const context = useContext(LeaderboardContext);
if (!context) {
throw new Error("useLeaderboard must be used within a LeaderboardProvider");
}
return context;
};